diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e074083 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +*.csv binary +*.txt binary +*.json binary \ No newline at end of file diff --git a/.gitignore b/.gitignore index 825729f..b5a0e6e 100644 --- a/.gitignore +++ b/.gitignore @@ -158,5 +158,6 @@ cert/* !cert/server.pem config/* deliver/* +*.gz dbdump-*.json \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..81252b1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM python:3.9.15-slim-bullseye + +RUN apt update && apt install default-libmysqlclient-dev build-essential libtk nodejs npm pkg-config -y + +WORKDIR /app +COPY requirements.txt requirements.txt +RUN pip3 install -r requirements.txt +RUN npm i -g nodemon + +COPY entrypoint.sh entrypoint.sh +RUN chmod +x entrypoint.sh + +COPY index.py index.py +COPY dbutils.py dbutils.py +COPY read.py read.py +ADD core core +ADD titles titles +ADD logs logs +ADD cert cert + +ENTRYPOINT [ "/app/entrypoint.sh" ] diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..8f088ff --- /dev/null +++ b/changelog.md @@ -0,0 +1,194 @@ +# Changelog +Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to. + +## 20240109 +### System ++ Removed `ADD config config` from dockerfile + +### Aimedb ++ Fixed an error that resulted from trying to scan a banned or locked card + +## 20240108 +### System ++ Change how the underlying system handles URLs + + This can now allow for things like version-specific, or even keychip-specific URLs + + Specific changes to games are noted below ++ Fix docker files [#60](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/60) (Thanks Rylie!) ++ Fix support for python 3.8 - 3.10 + +### Aimedb ++ Add support for SegaAuth key in games that support it (for now only Chunithm) + + This is a JWT that is sent to games, by Aimedb, that the games send to their game server, to verify that the access code the game is sending to the server was obtained via aimedb. + + Requires a base64-encoded secret to be set in the `core.yaml` + +### Chunithm ++ Fix Air support ++ Add saving for userRecentPlayerList ++ Add support for SegaAuthKey ++ Fix a bug arising if a user set their name to be 'true' or 'false' ++ Add support for Sun+ [#78](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/78) (Thanks EmmyHeart!) ++ Add `matching` section to `chuni.yaml` ++ ~~Change `udpHolePunchUri` and `reflectorUri` to be STUN and TURN servers~~ Reverted ++ Imrpove `GetGameSetting` request handling for different versions ++ Fix issue where songs would not always return all scores [#92](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/92) (Thanks Kumubou!) + +### maimai DX ++ Fix user charges failing to save + +### maimai ++ Made it functional + +### CXB ++ Improvements to request dispatching ++ Add support for non-omnimix music lists + + +### IDZ ++ Fix news urls in accordance with the system change to URLs + +### Initial D THE ARCADE ++ Added support for Initial D THE ARCADE S2 + + Story mode progress added + + Bunta Challenge/Touhou Project modes added + + Time Trials added + + Leaderboards added, but doesn't refresh sometimes + + Theory of Street mode added (with CPUs) + + Play Stamp/Timetrial events added + + Frontend to download profile added + + Importer to import profiles added + +### ONGEKI ++ Now supports HTTPS on a per-version basis ++ Merg PR [#61](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/61) (Thanks phantomlan!) + + Add Ranking Event Support + + Add reward list support + + Add version segregation to Event Ranking, Tech Challenge, and Music Ranking + + Now stores ClientTestmode and ClientSetting data ++ Fix mission points not adding correctly [#68](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/68) (Thanks phantomlan!) ++ Fix tech challenge [#70](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/70) (Thanks phantomlan!) + +### SAO ++ Change endpoint in accordance with the system change to URLs ++ Update request header class to be more accurate ++ Encrypted requests are now supported ++ Change to using handler classes instead of raw structs for simplicity + +### Wacca ++ Fix a server error causing a seperate error that casued issues ++ Add better error printing ++ Add better request validation ++ Fix HousingStartV2 ++ Fix Lily's housing/get handler + +## 20231107 +### CXB ++ Hotfix `render_POST` sometimes failing to read the request body on large requests + +## 20231106 +### CXB ++ Hotfix `render_POST` function signature signature ++ Hotfix `handle_action_addenergy_request` hard failing if `get_energy` returns None + +## 20231015 +### maimai DX ++ Added support for FESTiVAL PLUS + +### Card Maker ++ Added support for maimai DX FESTiVAL PLUS + +## 20230716 +### General ++ Docker files added (#19) ++ Added support for threading + + This comes with the caviat that enabling it will not allow you to use Ctrl + C to stop the server. + +### Webui ++ Small improvements ++ Add card display + +### Allnet ++ Billing format validation ++ Fix naomitest.html endpoint ++ Add event logging for auths and billing ++ LoaderStateRecorder endpoint handler added + +### Mucha ++ Fixed log level always being "Info" ++ Add stub handler for DownloadState + +### Sword Art Online ++ Support added + +### Crossbeats ++ Added threading to profile loading + + This should cause a noticeable speed-up + +### Card Maker ++ DX Passes fixed ++ Various improvements + +### Diva ++ Added clear status calculation ++ Various minor fixes and improvements + +### Maimai ++ Added support for memorial photo uploads ++ Added support for the following versions + + Festival + + FiNALE ++ Various bug fixes and improvements + +### Wacca ++ Fixed an error that sometimes occoured when trying to unlock songs (#22) + +### Pokken ++ Profile saving added (loading TBA) ++ Use external STUN server for matching by default + + Matching still not working + +## 2023042300 +### Wacca ++ Time free now works properly ++ Fix reverse gate mission causing a fatal error ++ Other misc. fixes ++ Latest DB: 5 + +### Pokken ++ Added preliminary support + + Nothing saves currently, but the game will boot and function properly. + +### Initial D Zero ++ Added preliminary support + + Nothing saves currently, but the game will boot and function for the most part. + +### Mai2 ++ Added support for Festival ++ Lasted DB Version: 4 + +### Ongeki ++ Misc fixes ++ Lasted DB Version: 4 + +### Diva ++ Misc fixes ++ Lasted DB Version: 4 + +### Chuni ++ Fix network encryption ++ Add `handle_remove_token_api_request` for event mode + +### Allnet ++ Added download order support + + It is up to the sysop to provide the INI file, and host the files. + + ONLY for use with cabs. It's not checked currently, which it's why it's default disabled + + YMMV, use at your own risk ++ When running develop mode, games that are not recognised will still be able to authenticate. + +### Database ++ Add autoupgrade command + + Invoke to automatically upgrade all schemas to their latest versions + ++ `version` arg no longer required, leave it blank to update the game schema to latest if it isn't already + +### Misc ++ Update example nginx config file diff --git a/contributing.md b/contributing.md new file mode 100644 index 0000000..5397f70 --- /dev/null +++ b/contributing.md @@ -0,0 +1,8 @@ +# 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. + +## Adding games +Guide WIP + +## Adding game versions +Guide WIP \ No newline at end of file diff --git a/core/__init__.py b/core/__init__.py index c72ba0a..185d9bc 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -4,3 +4,4 @@ from core.aimedb import AimedbFactory from core.title import TitleServlet from core.utils import Utils from core.mucha import MuchaServlet +from core.frontend import FrontendServlet diff --git a/core/adb_handlers/__init__.py b/core/adb_handlers/__init__.py new file mode 100644 index 0000000..0c96baf --- /dev/null +++ b/core/adb_handlers/__init__.py @@ -0,0 +1,6 @@ +from .base import ADBBaseRequest, ADBBaseResponse, ADBHeader, ADBHeaderException, PortalRegStatus, LogStatus, ADBStatus +from .base import CompanyCodes, ReaderFwVer, CMD_CODE_GOODBYE, HEADER_SIZE +from .lookup import ADBLookupRequest, ADBLookupResponse, ADBLookupExResponse +from .campaign import ADBCampaignClearRequest, ADBCampaignClearResponse, ADBCampaignResponse, ADBOldCampaignRequest, ADBOldCampaignResponse +from .felica import ADBFelicaLookupRequest, ADBFelicaLookupResponse, ADBFelicaLookup2Request, ADBFelicaLookup2Response +from .log import ADBLogExRequest, ADBLogRequest, ADBStatusLogRequest, ADBLogExResponse diff --git a/core/adb_handlers/base.py b/core/adb_handlers/base.py new file mode 100644 index 0000000..0f208dd --- /dev/null +++ b/core/adb_handlers/base.py @@ -0,0 +1,170 @@ +import struct +from construct import Struct, Int16ul, Int32ul, PaddedString +from enum import Enum +import re +from typing import Union, Final + +class LogStatus(Enum): + NONE = 0 + START = 1 + CONTINUE = 2 + END = 3 + OTHER = 4 + +class PortalRegStatus(Enum): + NO_REG = 0 + PORTAL = 1 + SEGA_ID = 2 + +class ADBStatus(Enum): + UNKNOWN = 0 + GOOD = 1 + BAD_AMIE_ID = 2 + ALREADY_REG = 3 + BAN_SYS_USER = 4 + BAN_SYS = 5 + BAN_USER = 6 + BAN_GEN = 7 + LOCK_SYS_USER = 8 + LOCK_SYS = 9 + LOCK_USER = 10 + +class CompanyCodes(Enum): + NONE = 0 + SEGA = 1 + BAMCO = 2 + KONAMI = 3 + TAITO = 4 + +class ReaderFwVer(Enum): # Newer readers use a singly byte value + NONE = 0 + TN32_10 = 1 + TN32_12 = 2 + OTHER = 9 + + def __str__(self) -> str: + if self == self.TN32_10: + return "TN32MSEC003S F/W Ver1.0" + elif self == self.TN32_12: + return "TN32MSEC003S F/W Ver1.2" + elif self == self.NONE: + return "Not Specified" + elif self == self.OTHER: + return "Unknown/Other" + else: + raise ValueError(f"Bad ReaderFwVer value {self.value}") + + @classmethod + def from_byte(self, byte: bytes) -> Union["ReaderFwVer", int]: + try: + i = int.from_bytes(byte, 'little') + try: + return ReaderFwVer(i) + except ValueError: + return i + except TypeError: + return 0 + +class ADBHeaderException(Exception): + pass + +HEADER_SIZE: Final[int] = 0x20 +CMD_CODE_GOODBYE: Final[int] = 0x66 + +# everything is LE +class ADBHeader: + def __init__(self, magic: int, protocol_ver: int, cmd: int, length: int, status: int, game_id: Union[str, bytes], store_id: int, keychip_id: Union[str, bytes]) -> None: + self.magic = magic # u16 + self.protocol_ver = protocol_ver # u16 + self.cmd = cmd # u16 + self.length = length # u16 + try: + self.status = ADBStatus(status) # u16 + except ValueError as e: + raise ADBHeaderException(f"Status is incorrect! {e}") + self.game_id = game_id # 4 char + \x00 + self.store_id = store_id # u32 + self.keychip_id = keychip_id# 11 char + \x00 + + if type(self.game_id) == bytes: + self.game_id = self.game_id.decode() + + if type(self.keychip_id) == bytes: + self.keychip_id = self.keychip_id.decode() + + self.game_id = self.game_id.replace("\0", "") + self.keychip_id = self.keychip_id.replace("\0", "") + if self.cmd != CMD_CODE_GOODBYE: # Games for some reason send no data with goodbye + self.validate() + + @classmethod + def from_data(cls, data: bytes) -> "ADBHeader": + magic, protocol_ver, cmd, length, status, game_id, store_id, keychip_id = struct.unpack_from("<5H6sI12s", data) + head = cls(magic, protocol_ver, cmd, length, status, game_id, store_id, keychip_id) + + if head.length != len(data): + raise ADBHeaderException(f"Length is incorrect! Expect {head.length}, got {len(data)}") + + return head + + def validate(self) -> bool: + if self.magic != 0xa13e: + raise ADBHeaderException(f"Magic {self.magic} != 0xa13e") + + if self.protocol_ver < 0x1000: + raise ADBHeaderException(f"Protocol version {hex(self.protocol_ver)} is invalid!") + + if re.fullmatch(r"^S[0-9A-Z]{3}[P]?$", self.game_id) is None: + raise ADBHeaderException(f"Game ID {self.game_id} is invalid!") + + if self.store_id == 0: + raise ADBHeaderException(f"Store ID cannot be 0!") + + if re.fullmatch(r"^A[0-9]{2}[E|X][0-9]{2}[A-HJ-NP-Z][0-9]{4}$", self.keychip_id) is None: + raise ADBHeaderException(f"Keychip ID {self.keychip_id} is invalid!") + + return True + + def make(self) -> bytes: + resp_struct = Struct( + "magic" / Int16ul, + "unknown" / Int16ul, + "response_code" / Int16ul, + "length" / Int16ul, + "status" / Int16ul, + "game_id" / PaddedString(6, 'utf_8'), + "store_id" / Int32ul, + "keychip_id" / PaddedString(12, 'utf_8'), + ) + + return resp_struct.build(dict( + magic=self.magic, + unknown=self.protocol_ver, + response_code=self.cmd, + length=self.length, + status=self.status.value, + game_id = self.game_id, + store_id = self.store_id, + keychip_id = self.keychip_id, + )) + +class ADBBaseRequest: + def __init__(self, data: bytes) -> None: + self.head = ADBHeader.from_data(data) + +class ADBBaseResponse: + def __init__(self, code: int = 0, length: int = 0x20, status: int = 1, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", protocol_ver: int = 0x3087) -> None: + self.head = ADBHeader(0xa13e, protocol_ver, code, length, status, game_id, store_id, keychip_id) + + @classmethod + def from_req(cls, req: ADBHeader, cmd: int, length: int = 0x20, status: int = 1) -> "ADBBaseResponse": + return cls(cmd, length, status, req.game_id, req.store_id, req.keychip_id, req.protocol_ver) + + def append_padding(self, data: bytes): + """Appends 0s to the end of the data until it's at the correct size""" + padding_size = self.head.length - len(data) + data += bytes(padding_size) + return data + + def make(self) -> bytes: + return self.head.make() diff --git a/core/adb_handlers/campaign.py b/core/adb_handlers/campaign.py new file mode 100644 index 0000000..a1a372e --- /dev/null +++ b/core/adb_handlers/campaign.py @@ -0,0 +1,132 @@ +from construct import Struct, Int16ul, Padding, Bytes, Int32ul, Int32sl + +from .base import * + +class Campaign: + def __init__(self) -> None: + self.id = 0 + self.name = "" + self.announce_date = 0 + self.start_date = 0 + self.end_date = 0 + self.distrib_start_date = 0 + self.distrib_end_date = 0 + + def make(self) -> bytes: + name_padding = bytes(128 - len(self.name)) + return Struct( + "id" / Int32ul, + "name" / Bytes(128), + "announce_date" / Int32ul, + "start_date" / Int32ul, + "end_date" / Int32ul, + "distrib_start_date" / Int32ul, + "distrib_end_date" / Int32ul, + Padding(8), + ).build(dict( + id = self.id, + name = self.name.encode() + name_padding, + announce_date = self.announce_date, + start_date = self.start_date, + end_date = self.end_date, + distrib_start_date = self.distrib_start_date, + distrib_end_date = self.distrib_end_date, + )) + +class CampaignClear: + def __init__(self) -> None: + self.id = 0 + self.entry_flag = 0 + self.clear_flag = 0 + + def make(self) -> bytes: + return Struct( + "id" / Int32ul, + "entry_flag" / Int32ul, + "clear_flag" / Int32ul, + Padding(4), + ).build(dict( + id = self.id, + entry_flag = self.entry_flag, + clear_flag = self.clear_flag, + )) + +class ADBCampaignResponse(ADBBaseResponse): + def __init__(self, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x0C, length: int = 0x200, status: int = 1) -> None: + super().__init__(code, length, status, game_id, store_id, keychip_id) + self.campaigns = [Campaign(), Campaign(), Campaign()] + + @classmethod + def from_req(cls, req: ADBHeader) -> "ADBCampaignResponse": + c = cls(req.game_id, req.store_id, req.keychip_id) + c.head.protocol_ver = req.protocol_ver + return c + + def make(self) -> bytes: + body = b"" + + for c in self.campaigns: + body += c.make() + + self.head.length = HEADER_SIZE + len(body) + return self.head.make() + body + +class ADBOldCampaignRequest(ADBBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + self.campaign_id = struct.unpack_from(" None: + super().__init__(code, length, status, game_id, store_id, keychip_id) + self.info0 = 0 + self.info1 = 0 + self.info2 = 0 + self.info3 = 0 + + @classmethod + def from_req(cls, req: ADBHeader) -> "ADBCampaignResponse": + c = cls(req.game_id, req.store_id, req.keychip_id) + c.head.protocol_ver = req.protocol_ver + return c + + def make(self) -> bytes: + resp_struct = Struct( + "info0" / Int32sl, + "info1" / Int32sl, + "info2" / Int32sl, + "info3" / Int32sl, + ).build( + info0 = self.info0, + info1 = self.info1, + info2 = self.info2, + info3 = self.info3, + ) + + self.head.length = HEADER_SIZE + len(resp_struct) + return self.head.make() + resp_struct + +class ADBCampaignClearRequest(ADBBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + self.aime_id = struct.unpack_from(" None: + super().__init__(code, length, status, game_id, store_id, keychip_id) + self.campaign_clear_status = [CampaignClear(), CampaignClear(), CampaignClear()] + + @classmethod + def from_req(cls, req: ADBHeader) -> "ADBCampaignResponse": + c = cls(req.game_id, req.store_id, req.keychip_id) + c.head.protocol_ver = req.protocol_ver + return c + + def make(self) -> bytes: + body = b"" + + for c in self.campaign_clear_status: + body += c.make() + + self.head.length = HEADER_SIZE + len(body) + return self.head.make() + body diff --git a/core/adb_handlers/felica.py b/core/adb_handlers/felica.py new file mode 100644 index 0000000..479e84d --- /dev/null +++ b/core/adb_handlers/felica.py @@ -0,0 +1,85 @@ +from construct import Struct, Int32sl, Padding, Int8ub, Int16sl +from typing import Union +from .base import * + +class ADBFelicaLookupRequest(ADBBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + idm, pmm = struct.unpack_from(">QQ", data, 0x20) + self.idm = hex(idm)[2:].upper() + 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: + super().__init__(code, length, status, game_id, store_id, keychip_id) + self.access_code = access_code if access_code is not None else "00000000000000000000" + + @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) + c.head.protocol_ver = req.protocol_ver + return c + + def make(self) -> bytes: + resp_struct = Struct( + "felica_idx" / Int32ul, + "access_code" / Int8ub[10], + Padding(2) + ).build(dict( + felica_idx = 0, + access_code = bytes.fromhex(self.access_code) + )) + + self.head.length = HEADER_SIZE + len(resp_struct) + + return self.head.make() + resp_struct + +class ADBFelicaLookup2Request(ADBBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + self.random = struct.unpack_from("<16s", data, 0x20)[0] + idm, pmm = struct.unpack_from(">QQ", data, 0x30) + self.card_key_ver, self.write_ct, self.maca, company, fw_ver, self.dfc = struct.unpack_from("<16s16sQccH", data, 0x40) + self.idm = hex(idm)[2:].upper() + self.pmm = hex(pmm)[2:].upper() + self.company = CompanyCodes(int.from_bytes(company, 'little')) + self.fw_ver = ReaderFwVer.from_byte(fw_ver) + +class ADBFelicaLookup2Response(ADBBaseResponse): + def __init__(self, user_id: Union[int, None] = None, access_code: Union[str, None] = None, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x12, length: int = 0x130, status: int = 1) -> None: + super().__init__(code, length, status, game_id, store_id, keychip_id) + self.user_id = user_id if user_id is not None else -1 + self.access_code = access_code if access_code is not None else "00000000000000000000" + self.company = CompanyCodes.SEGA + self.portal_status = PortalRegStatus.NO_REG + self.auth_key = [0] * 256 + + @classmethod + def from_req(cls, req: ADBHeader, user_id: Union[int, None] = None, access_code: Union[str, None] = None) -> "ADBFelicaLookup2Response": + c = cls(user_id, access_code, req.game_id, req.store_id, req.keychip_id) + c.head.protocol_ver = req.protocol_ver + return c + + def make(self) -> bytes: + resp_struct = Struct( + "user_id" / Int32sl, + "relation1" / Int32sl, + "relation2" / Int32sl, + "access_code" / Int8ub[10], + "portal_status" / Int8ub, + "company_code" / Int8ub, + Padding(8), + "auth_key" / Int8ub[256], + ).build(dict( + user_id = self.user_id, + relation1 = -1, # Unsupported + relation2 = -1, # Unsupported + access_code = bytes.fromhex(self.access_code), + portal_status = self.portal_status.value, + company_code = self.company.value, + auth_key = self.auth_key + )) + + self.head.length = HEADER_SIZE + len(resp_struct) + + return self.head.make() + resp_struct diff --git a/core/adb_handlers/log.py b/core/adb_handlers/log.py new file mode 100644 index 0000000..28fbdf3 --- /dev/null +++ b/core/adb_handlers/log.py @@ -0,0 +1,56 @@ +from construct import Struct, Padding, Int8sl +from typing import Final, List + +from .base import * +NUM_LOGS: Final[int] = 20 +NUM_LEN_LOG_EX: Final[int] = 48 + +class AmLogEx: + def __init__(self, data: bytes) -> None: + self.aime_id, status, self.user_id, self.credit_ct, self.bet_ct, self.won_ct, self.local_time, \ + self.tseq, self.place_id = struct.unpack(" None: + super().__init__(data) + self.aime_id, status = struct.unpack_from(" None: + super().__init__(data) + self.aime_id, status, self.user_id, self.credit_ct, self.bet_ct, self.won_ct = struct.unpack_from(" None: + super().__init__(data) + self.logs: List[AmLogEx] = [] + + for x in range(NUM_LOGS): + self.logs.append(AmLogEx(data[0x20 + (NUM_LEN_LOG_EX * x): 0x50 + (NUM_LEN_LOG_EX * x)])) + + self.num_logs = struct.unpack_from(" None: + super().__init__(code, length, status, game_id, store_id, keychip_id, protocol_ver) + + @classmethod + def from_req(cls, req: ADBHeader) -> "ADBLogExResponse": + c = cls(req.game_id, req.store_id, req.keychip_id, req.protocol_ver) + return c + + def make(self) -> bytes: + resp_struct = Struct( + "log_result" / Int8sl[NUM_LOGS], + Padding(12) + ) + + body = resp_struct.build(dict( + log_result = [1] * NUM_LOGS + )) + + self.head.length = HEADER_SIZE + len(body) + return self.head.make() + body diff --git a/core/adb_handlers/lookup.py b/core/adb_handlers/lookup.py new file mode 100644 index 0000000..0640493 --- /dev/null +++ b/core/adb_handlers/lookup.py @@ -0,0 +1,82 @@ +from construct import Struct, Int32sl, Padding, Int8sl +from typing import Union + +from .base import * + +class ADBLookupException(Exception): + pass + +class ADBLookupRequest(ADBBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + self.access_code = data[0x20:0x2A].hex() + company_code, fw_version, self.serial_number = struct.unpack_from(" None: + super().__init__(code, length, status, game_id, store_id, keychip_id) + self.user_id = user_id if user_id is not None else -1 + self.portal_reg = PortalRegStatus.NO_REG + + @classmethod + def from_req(cls, req: ADBHeader, user_id: Union[int, None]) -> "ADBLookupResponse": + c = cls(user_id, req.game_id, req.store_id, req.keychip_id) + c.head.protocol_ver = req.protocol_ver + return c + + def make(self): + resp_struct = Struct( + "user_id" / Int32sl, + "portal_reg" / Int8sl, + Padding(11) + ) + + body = resp_struct.build(dict( + user_id = self.user_id, + portal_reg = self.portal_reg.value + )) + + self.head.length = HEADER_SIZE + len(body) + return self.head.make() + body + +class ADBLookupExResponse(ADBBaseResponse): + def __init__(self, user_id: Union[int, None], game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", + code: int = 0x10, length: int = 0x130, status: int = 1) -> None: + super().__init__(code, length, status, game_id, store_id, keychip_id) + self.user_id = user_id if user_id is not None else -1 + self.portal_reg = PortalRegStatus.NO_REG + self.auth_key = [0] * 256 + + @classmethod + def from_req(cls, req: ADBHeader, user_id: Union[int, None]) -> "ADBLookupExResponse": + c = cls(user_id, req.game_id, req.store_id, req.keychip_id) + c.head.protocol_ver = req.protocol_ver + return c + + def make(self): + resp_struct = Struct( + "user_id" / Int32sl, + "portal_reg" / Int8sl, + Padding(3), + "auth_key" / Int8sl[256], + "relation1" / Int32sl, + "relation2" / Int32sl, + ) + + body = resp_struct.build(dict( + user_id = self.user_id, + portal_reg = self.portal_reg.value, + auth_key = self.auth_key, + relation1 = -1, + relation2 = -1 + )) + + self.head.length = HEADER_SIZE + len(body) + return self.head.make() + body diff --git a/core/aimedb.py b/core/aimedb.py index b8ce3b7..6d6615a 100644 --- a/core/aimedb.py +++ b/core/aimedb.py @@ -2,26 +2,18 @@ from twisted.internet.protocol import Factory, Protocol import logging, coloredlogs from Crypto.Cipher import AES import struct -from typing import Dict, Any +from typing import Dict, Tuple, Callable, Union +from typing_extensions import Final from logging.handlers import TimedRotatingFileHandler from core.config import CoreConfig +from core.utils import create_sega_auth_key from core.data import Data +from .adb_handlers import * + class AimedbProtocol(Protocol): - AIMEDB_RESPONSE_CODES = { - "felica_lookup": 0x03, - "lookup": 0x06, - "log": 0x0a, - "campaign": 0x0c, - "touch": 0x0e, - "lookup2": 0x10, - "felica_lookup2": 0x12, - "log2": 0x14, - "hello": 0x65 - } - - request_list: Dict[int, Any] = {} + request_list: Dict[int, Tuple[Callable[[bytes, int], Union[ADBBaseResponse, bytes]], int, str]] = {} def __init__(self, core_cfg: CoreConfig) -> None: self.logger = logging.getLogger("aimedb") @@ -30,17 +22,28 @@ class AimedbProtocol(Protocol): if core_cfg.aimedb.key == "": self.logger.error("!!!KEY NOT SET!!!") exit(1) - - self.request_list[0x01] = self.handle_felica_lookup - self.request_list[0x04] = self.handle_lookup - self.request_list[0x05] = self.handle_register - self.request_list[0x09] = self.handle_log - self.request_list[0x0b] = self.handle_campaign - self.request_list[0x0d] = self.handle_touch - self.request_list[0x0f] = self.handle_lookup2 - self.request_list[0x11] = self.handle_felica_lookup2 - self.request_list[0x13] = self.handle_log2 - self.request_list[0x64] = self.handle_hello + + self.register_handler(0x01, 0x03, self.handle_felica_lookup, 'felica_lookup') + self.register_handler(0x02, 0x03, self.handle_felica_register, 'felica_register') + + self.register_handler(0x04, 0x06, self.handle_lookup, 'lookup') + self.register_handler(0x05, 0x06, self.handle_register, 'register') + + self.register_handler(0x07, 0x08, self.handle_status_log, 'status_log') + self.register_handler(0x09, 0x0A, self.handle_log, 'aime_log') + + self.register_handler(0x0B, 0x0C, self.handle_campaign, 'campaign') + self.register_handler(0x0D, 0x0E, self.handle_campaign_clear, 'campaign_clear') + + self.register_handler(0x0F, 0x10, self.handle_lookup_ex, 'lookup_ex') + self.register_handler(0x11, 0x12, self.handle_felica_lookup_ex, 'felica_lookup_ex') + + self.register_handler(0x13, 0x14, self.handle_log_ex, 'aime_log_ex') + self.register_handler(0x64, 0x65, self.handle_hello, 'hello') + self.register_handler(0x66, 0, self.handle_goodbye, 'goodbye') + + def register_handler(self, cmd: int, resp:int, handler: Callable[[bytes, int], Union[ADBBaseResponse, bytes]], name: str) -> None: + self.request_list[cmd] = (handler, resp, name) def append_padding(self, data: bytes): """Appends 0s to the end of the data until it's at the correct size""" @@ -53,182 +56,311 @@ class AimedbProtocol(Protocol): self.logger.debug(f"{self.transport.getPeer().host} Connected") def connectionLost(self, reason) -> None: - self.logger.debug(f"{self.transport.getPeer().host} Disconnected - {reason.value}") - + self.logger.debug( + f"{self.transport.getPeer().host} Disconnected - {reason.value}" + ) + def dataReceived(self, data: bytes) -> None: cipher = AES.new(self.config.aimedb.key.encode(), AES.MODE_ECB) try: decrypted = cipher.decrypt(data) - except: - self.logger.error(f"Failed to decrypt {data.hex()}") + + except Exception as e: + self.logger.error(f"Failed to decrypt {data.hex()} because {e}") return None self.logger.debug(f"{self.transport.getPeer().host} wrote {decrypted.hex()}") - if not decrypted[1] == 0xa1 and not decrypted[0] == 0x3e: - self.logger.error(f"Bad magic") - return None + try: + head = ADBHeader.from_data(decrypted) + + except ADBHeaderException as e: + self.logger.error(f"Error parsing ADB header: {e}") + try: + encrypted = cipher.encrypt(ADBBaseResponse().make()) + self.transport.write(encrypted) - req_code = decrypted[4] - - if req_code == 0x66: - self.logger.info(f"goodbye from {self.transport.getPeer().host}") - self.transport.loseConnection() + except Exception as e: + self.logger.error(f"Failed to encrypt default response because {e}") + return - try: - resp = self.request_list[req_code](decrypted) - encrypted = cipher.encrypt(resp) - self.logger.debug(f"Response {resp.hex()}") - self.transport.write(encrypted) + if head.keychip_id == "ABCD1234567" or head.store_id == 0xfff0: + self.logger.warning(f"Request from uninitialized AMLib: {vars(head)}") - except KeyError: - self.logger.error(f"Unknown command code {hex(req_code)}") - return None + handler, resp_code, name = self.request_list.get(head.cmd, (self.handle_default, None, 'default')) - except ValueError as e: - self.logger.error(f"Failed to encrypt {resp.hex()} because {e}") - return None - - def handle_campaign(self, data: bytes) -> bytes: - self.logger.info(f"campaign from {self.transport.getPeer().host}") - ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["campaign"], 0x0200, 0x0001) - return self.append_padding(ret) - - def handle_hello(self, data: bytes) -> bytes: - self.logger.info(f"hello from {self.transport.getPeer().host}") - ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["hello"], 0x0020, 0x0001) - return self.append_padding(ret) - - def handle_lookup(self, data: bytes) -> bytes: - luid = data[0x20: 0x2a].hex() - user_id = self.data.card.get_user_id_from_card(access_code=luid) - - if user_id is None: user_id = -1 - - self.logger.info(f"lookup from {self.transport.getPeer().host}: luid {luid} -> user_id {user_id}") - - ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["lookup"], 0x0130, 0x0001) - ret += bytes(0x20 - len(ret)) - - if user_id is None: ret += struct.pack(" bytes: - self.logger.info(f"lookup2") - - ret = bytearray(self.handle_lookup(data)) - ret[4] = self.AIMEDB_RESPONSE_CODES["lookup2"] - - return bytes(ret) - - def handle_felica_lookup(self, data: bytes) -> bytes: - idm = data[0x20: 0x28].hex() - pmm = data[0x28: 0x30].hex() - access_code = self.data.card.to_access_code(idm) - self.logger.info(f"felica_lookup from {self.transport.getPeer().host}: idm {idm} pmm {pmm} -> access_code {access_code}") - - ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["felica_lookup"], 0x0030, 0x0001) - ret += bytes(26) - ret += bytes.fromhex(access_code) - - return self.append_padding(ret) - - def handle_felica_lookup2(self, data: bytes) -> bytes: - idm = data[0x30: 0x38].hex() - pmm = data[0x38: 0x40].hex() - access_code = self.data.card.to_access_code(idm) - user_id = self.data.card.get_user_id_from_card(access_code=access_code) - - if user_id is None: user_id = -1 - - self.logger.info(f"felica_lookup2 from {self.transport.getPeer().host}: idm {idm} ipm {pmm} -> access_code {access_code} user_id {user_id}") - - ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["felica_lookup2"], 0x0140, 0x0001) - ret += bytes(22) - ret += struct.pack(" bytes: - self.logger.info(f"touch from {self.transport.getPeer().host}") - ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["touch"], 0x0050, 0x0001) - ret += bytes(5) - ret += struct.pack("<3H", 0x6f, 0, 1) + elif resp_code > 0: + self.logger.info(f"{name} from {head.keychip_id} ({head.game_id}) @ {self.transport.getPeer().host}") + + resp = handler(decrypted, resp_code) - return self.append_padding(ret) + if type(resp) == ADBBaseResponse or issubclass(type(resp), ADBBaseResponse): + resp_bytes = resp.make() + if len(resp_bytes) != resp.head.length: + resp_bytes = self.append_padding(resp_bytes) - def handle_register(self, data: bytes) -> bytes: - luid = data[0x20: 0x2a].hex() - if self.config.server.allow_registration: - user_id = self.data.user.create_user() - - if user_id is None: - user_id = -1 - self.logger.error("Failed to register user!") - - else: - card_id = self.data.card.create_card(user_id, luid) - - if card_id is None: - user_id = -1 - self.logger.error("Failed to register card!") - - self.logger.info(f"register from {self.transport.getPeer().host}: luid {luid} -> user_id {user_id}") + elif type(resp) == bytes: + resp_bytes = resp + + elif resp is None: # Nothing to send, probably a goodbye + return else: - self.logger.info(f"register from {self.transport.getPeer().host} blocked!: luid {luid}") + raise TypeError(f"Unsupported type returned by ADB handler for {name}: {type(resp)}") + + try: + encrypted = cipher.encrypt(resp_bytes) + self.logger.debug(f"Response {resp_bytes.hex()}") + self.transport.write(encrypted) + + except Exception as e: + self.logger.error(f"Failed to encrypt {resp_bytes.hex()} because {e}") + + def handle_default(self, data: bytes, resp_code: int, length: int = 0x20) -> ADBBaseResponse: + req = ADBHeader.from_data(data) + return ADBBaseResponse(resp_code, length, 1, req.game_id, req.store_id, req.keychip_id, req.protocol_ver) + + def handle_hello(self, data: bytes, resp_code: int) -> ADBBaseResponse: + return self.handle_default(data, resp_code) + + def handle_campaign(self, data: bytes, resp_code: int) -> ADBBaseResponse: + h = ADBHeader.from_data(data) + if h.protocol_ver >= 0x3030: + req = h + resp = ADBCampaignResponse.from_req(req) + + else: + req = ADBOldCampaignRequest(data) + + self.logger.info(f"Legacy campaign request for campaign {req.campaign_id} (protocol version {hex(h.protocol_ver)})") + resp = ADBOldCampaignResponse.from_req(req.head) + + # We don't currently support campaigns + return resp + + def handle_lookup(self, data: bytes, resp_code: int) -> ADBBaseResponse: + req = ADBLookupRequest(data) + user_id = self.data.card.get_user_id_from_card(req.access_code) + is_banned = self.data.card.get_card_banned(req.access_code) + is_locked = self.data.card.get_card_locked(req.access_code) + + ret = ADBLookupResponse.from_req(req.head, user_id) + if is_banned and is_locked: + ret.head.status = ADBStatus.BAN_SYS_USER + elif is_banned: + ret.head.status = ADBStatus.BAN_SYS + elif is_locked: + ret.head.status = ADBStatus.LOCK_USER + + self.logger.info( + f"access_code {req.access_code} -> user_id {ret.user_id}" + ) + return ret + + def handle_lookup_ex(self, data: bytes, resp_code: int) -> ADBBaseResponse: + req = ADBLookupRequest(data) + user_id = self.data.card.get_user_id_from_card(req.access_code) + + is_banned = self.data.card.get_card_banned(req.access_code) + is_locked = self.data.card.get_card_locked(req.access_code) + + ret = ADBLookupExResponse.from_req(req.head, user_id) + if is_banned and is_locked: + ret.head.status = ADBStatus.BAN_SYS_USER + elif is_banned: + ret.head.status = ADBStatus.BAN_SYS + elif is_locked: + ret.head.status = ADBStatus.LOCK_USER + + self.logger.info( + f"access_code {req.access_code} -> user_id {ret.user_id}" + ) + + 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) + if auth_key is not None: + auth_key_extra_len = 256 - len(auth_key) + auth_key_full = auth_key.encode() + (b"\0" * auth_key_extra_len) + self.logger.debug(f"Generated auth token {auth_key}") + ret.auth_key = auth_key_full + + return ret + + 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. + """ + req = ADBFelicaLookupRequest(data) + ac = self.data.card.to_access_code(req.idm) + self.logger.info( + f"idm {req.idm} ipm {req.pmm} -> access_code {ac}" + ) + return ADBFelicaLookupResponse.from_req(req.head, ac) + + def handle_felica_register(self, data: bytes, resp_code: int) -> bytes: + """ + I've never seen this used. + """ + req = ADBFelicaLookupRequest(data) + ac = self.data.card.to_access_code(req.idm) + + if self.config.server.allow_user_registration: + user_id = self.data.user.create_user() + + if user_id is None: + self.logger.error("Failed to register user!") + user_id = -1 + + else: + card_id = self.data.card.create_card(user_id, ac) + + if card_id is None: + self.logger.error("Failed to register card!") + user_id = -1 + + self.logger.info( + f"Register access code {ac} (IDm: {req.idm} PMm: {req.pmm}) -> user_id {user_id}" + ) + + else: + self.logger.info( + f"Registration blocked!: access code {ac} (IDm: {req.idm} PMm: {req.pmm})" + ) + + return ADBFelicaLookupResponse.from_req(req.head, ac) + + 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 = self.data.card.get_user_id_from_card(access_code=access_code) + + if user_id is None: user_id = -1 - ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["lookup"], 0x0030, 0x0001 if user_id > -1 else 0) - ret += bytes(0x20 - len(ret)) - ret += struct.pack(" access_code {access_code} user_id {user_id}" + ) - return self.append_padding(ret) + resp = ADBFelicaLookup2Response.from_req(req.head, user_id, access_code) - def handle_log(self, data: bytes) -> bytes: - # TODO: Save aimedb logs - self.logger.info(f"log from {self.transport.getPeer().host}") - ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["log"], 0x0020, 0x0001) - return self.append_padding(ret) + 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) + if auth_key is not None: + auth_key_extra_len = 256 - len(auth_key) + auth_key_full = auth_key.encode() + (b"\0" * auth_key_extra_len) + self.logger.debug(f"Generated auth token {auth_key}") + resp.auth_key = auth_key_full - def handle_log2(self, data: bytes) -> bytes: - self.logger.info(f"log2 from {self.transport.getPeer().host}") - ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["log2"], 0x0040, 0x0001) - ret += bytes(22) - ret += struct.pack("H", 1) + return resp - return self.append_padding(ret) + def handle_campaign_clear(self, data: bytes, resp_code: int) -> ADBBaseResponse: + req = ADBCampaignClearRequest(data) + + resp = ADBCampaignClearResponse.from_req(req.head) + + # We don't support campaign stuff + return resp + + def handle_register(self, data: bytes, resp_code: int) -> bytes: + req = ADBLookupRequest(data) + user_id = -1 + + if self.config.server.allow_user_registration: + user_id = self.data.user.create_user() + + if user_id is None: + self.logger.error("Failed to register user!") + user_id = -1 + + else: + card_id = self.data.card.create_card(user_id, req.access_code) + + if card_id is None: + self.logger.error("Failed to register card!") + user_id = -1 + + self.logger.info( + f"Register access code {req.access_code} -> user_id {user_id}" + ) + + else: + self.logger.info( + f"Registration blocked!: access code {req.access_code}" + ) + + resp = ADBLookupResponse.from_req(req.head, user_id) + if resp.user_id <= 0: + resp.head.status = ADBStatus.BAN_SYS # Closest we can get to a "You cannot register" + + return resp + + # TODO: Save these in some capacity, as deemed relevant + def handle_status_log(self, data: bytes, resp_code: int) -> bytes: + req = ADBStatusLogRequest(data) + self.logger.info(f"User {req.aime_id} logged {req.status.name} event") + return ADBBaseResponse(resp_code, 0x20, 1, req.head.game_id, req.head.store_id, req.head.keychip_id, req.head.protocol_ver) + + def handle_log(self, data: bytes, resp_code: int) -> bytes: + req = ADBLogRequest(data) + self.logger.info(f"User {req.aime_id} logged {req.status.name} event, credit_ct: {req.credit_ct} bet_ct: {req.bet_ct} won_ct: {req.won_ct}") + return ADBBaseResponse(resp_code, 0x20, 1, req.head.game_id, req.head.store_id, req.head.keychip_id, req.head.protocol_ver) + + def handle_log_ex(self, data: bytes, resp_code: int) -> bytes: + req = ADBLogExRequest(data) + strs = [] + self.logger.info(f"Recieved {req.num_logs} or {len(req.logs)} logs") + + for x in range(req.num_logs): + self.logger.debug(f"User {req.logs[x].aime_id} logged {req.logs[x].status.name} event, credit_ct: {req.logs[x].credit_ct} bet_ct: {req.logs[x].bet_ct} won_ct: {req.logs[x].won_ct}") + return ADBLogExResponse.from_req(req.head) + + def handle_goodbye(self, data: bytes, resp_code: int) -> None: + self.logger.info(f"goodbye from {self.transport.getPeer().host}") + self.transport.loseConnection() + return class AimedbFactory(Factory): protocol = AimedbProtocol + def __init__(self, cfg: CoreConfig) -> None: self.config = cfg log_fmt_str = "[%(asctime)s] Aimedb | %(levelname)s | %(message)s" log_fmt = logging.Formatter(log_fmt_str) self.logger = logging.getLogger("aimedb") - fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.config.server.log_dir, "aimedb"), when="d", backupCount=10) + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.config.server.log_dir, "aimedb"), + when="d", + backupCount=10, + ) fileHandler.setFormatter(log_fmt) - + consoleHandler = logging.StreamHandler() consoleHandler.setFormatter(log_fmt) self.logger.addHandler(fileHandler) self.logger.addHandler(consoleHandler) - + self.logger.setLevel(self.config.aimedb.loglevel) - coloredlogs.install(level=cfg.aimedb.loglevel, logger=self.logger, fmt=log_fmt_str) - + coloredlogs.install( + level=cfg.aimedb.loglevel, logger=self.logger, fmt=log_fmt_str + ) + if self.config.aimedb.key == "": self.logger.error("Please set 'key' field in your config file.") exit(1) self.logger.info(f"Ready on port {self.config.aimedb.port}") - + def buildProtocol(self, addr): return AimedbProtocol(self.config) diff --git a/core/allnet.py b/core/allnet.py index 3b371ce..e83aae0 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Any, Optional, Tuple +from typing import Dict, List, Any, Optional, Tuple, Union, Final import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler from twisted.web.http import Request @@ -6,397 +6,846 @@ from datetime import datetime import pytz import base64 import zlib +import json +from enum import Enum from Crypto.PublicKey import RSA from Crypto.Hash import SHA from Crypto.Signature import PKCS1_v1_5 from time import strptime +from os import path +import urllib.parse +import math -from core.config import CoreConfig -from core.data import Data -from core.utils import Utils +from .config import CoreConfig +from .utils import Utils +from .data import Data +from .const import * +from .title import TitleServlet + +BILLING_DT_FORMAT: Final[str] = "%Y%m%d%H%M%S" + +class DLIMG_TYPE(Enum): + app = 0 + opt = 1 + +class ALLNET_STAT(Enum): + ok = 0 + bad_game = -1 + bad_machine = -2 + bad_shop = -3 + +class DLI_STATUS(Enum): + START = 0 + GET_DOWNLOAD_CONFIGURATION = 1 + WAIT_DOWNLOAD = 2 + DOWNLOADING = 3 + + NOT_SPECIFY_DLI = 100 + ONLY_POST_REPORT = 101 + STOPPED_BY_APP_RELEASE = 102 + STOPPED_BY_OPT_RELEASE = 103 + + DOWNLOAD_COMPLETE_RECENTLY = 110 + + DOWNLOAD_COMPLETE_WAIT_RELEASE_TIME = 120 + DOWNLOAD_COMPLETE_BUT_NOT_SYNC_SERVER = 121 + DOWNLOAD_COMPLETE_BUT_NOT_FIRST_RESUME = 122 + DOWNLOAD_COMPLETE_BUT_NOT_FIRST_LAUNCH = 123 + DOWNLOAD_COMPLETE_WAIT_UPDATE = 124 + + DOWNLOAD_COMPLETE_AND_ALREADY_UPDATE = 130 + + ERROR_AUTH_FAILURE = 200 + + ERROR_GET_DLI_HTTP = 300 + ERROR_GET_DLI = 301 + ERROR_PARSE_DLI = 302 + ERROR_INVALID_GAME_ID = 303 + ERROR_INVALID_IMAGE_LIST = 304 + ERROR_GET_DLI_APP = 305 + + ERROR_GET_BOOT_ID = 400 + ERROR_ACCESS_SERVER = 401 + ERROR_NO_IMAGE = 402 + ERROR_ACCESS_IMAGE = 403 + + ERROR_DOWNLOAD_APP = 500 + ERROR_DOWNLOAD_OPT = 501 + + ERROR_DISK_FULL = 600 + ERROR_UNINSTALL = 601 + ERROR_INSTALL_APP = 602 + ERROR_INSTALL_OPT = 603 + + ERROR_GET_DLI_INTERNAL = 900 + ERROR_ICF = 901 + ERROR_CHECK_RELEASE_INTERNAL = 902 + UNKNOWN = 999 # Not the actual enum val but it needs to be here as a catch-all + + @classmethod + def from_int(cls, num: int) -> "DLI_STATUS": + try: + return cls(num) + except ValueError: + return cls.UNKNOWN class AllnetServlet: - def __init__(self, core_cfg: CoreConfig, cfg_folder: str): + def __init__(self, core_cfg: CoreConfig, cfg_folder: str): super().__init__() self.config = core_cfg self.config_folder = cfg_folder self.data = Data(core_cfg) - self.uri_registry: Dict[str, Tuple[str, str]] = {} self.logger = logging.getLogger("allnet") if not hasattr(self.logger, "initialized"): log_fmt_str = "[%(asctime)s] Allnet | %(levelname)s | %(message)s" - log_fmt = logging.Formatter(log_fmt_str) + log_fmt = logging.Formatter(log_fmt_str) - fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.config.server.log_dir, "allnet"), when="d", backupCount=10) + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.config.server.log_dir, "allnet"), + when="d", + backupCount=10, + ) fileHandler.setFormatter(log_fmt) - + consoleHandler = logging.StreamHandler() consoleHandler.setFormatter(log_fmt) self.logger.addHandler(fileHandler) self.logger.addHandler(consoleHandler) - + self.logger.setLevel(core_cfg.allnet.loglevel) - coloredlogs.install(level=core_cfg.allnet.loglevel, logger=self.logger, fmt=log_fmt_str) + coloredlogs.install( + level=core_cfg.allnet.loglevel, logger=self.logger, fmt=log_fmt_str + ) self.logger.initialized = True plugins = Utils.get_all_titles() if len(plugins) == 0: self.logger.error("No games detected!") + + self.logger.info( + f"Serving {len(TitleServlet.title_registry)} game codes port {core_cfg.allnet.port}" + ) + + def handle_poweron(self, request: Request, _: Dict): + request_ip = Utils.get_ip_addr(request) + pragma_header = request.getHeader('Pragma') + is_dfi = pragma_header is not None and pragma_header == "DFI" - for _, mod in plugins.items(): - for code in mod.game_codes: - if hasattr(mod, "use_default_title") and mod.use_default_title: - if hasattr(mod, "include_protocol") and mod.include_protocol: - if hasattr(mod, "title_secure") and mod.title_secure: - uri = "https://" - - else: - uri = "http://" - - else: - uri = "" - - if core_cfg.server.is_develop: - uri += f"{core_cfg.title.hostname}:{core_cfg.title.port}" - - else: - uri += f"{core_cfg.title.hostname}" - - uri += f"/{code}/$v" - - if hasattr(mod, "trailing_slash") and mod.trailing_slash: - uri += "/" - - else: - if hasattr(mod, "uri"): - uri = mod.uri - else: - uri = "" - - if hasattr(mod, "host"): - host = mod.host - - elif hasattr(mod, "use_default_host") and mod.use_default_host: - if core_cfg.server.is_develop: - host = f"{core_cfg.title.hostname}:{core_cfg.title.port}" - - else: - host = f"{core_cfg.title.hostname}" - - else: - host = "" - - self.uri_registry[code] = (uri, host) - self.logger.info(f"Allnet serving {len(self.uri_registry)} games on port {core_cfg.allnet.port}") - - def handle_poweron(self, request: Request): - request_ip = request.getClientAddress().host try: - req = AllnetPowerOnRequest(self.allnet_req_to_dict(request.content.getvalue())) + if is_dfi: + req_urlencode = self.from_dfi(request.content.getvalue()) + else: + req_urlencode = request.content.getvalue().decode() + + req_dict = self.allnet_req_to_dict(req_urlencode) + if req_dict is None: + raise AllnetRequestException() + + req = AllnetPowerOnRequest(req_dict[0]) # Validate the request. Currently we only validate the fields we plan on using - if not req.game_id or not req.ver or not req.token or not req.serial or not req.ip: - raise AllnetRequestException(f"Bad auth request params from {request_ip} - {vars(req)}") - + if not req.game_id or not req.ver or not req.serial or not req.ip or not req.firm_ver or not req.boot_ver: + raise AllnetRequestException( + f"Bad auth request params from {request_ip} - {vars(req)}" + ) + except AllnetRequestException as e: - self.logger.error(e) + if e.message != "": + self.logger.error(e) return b"" - + if req.format_ver == 3: resp = AllnetPowerOnResponse3(req.token) - else: + elif req.format_ver == 2: resp = AllnetPowerOnResponse2() + else: + resp = AllnetPowerOnResponse() - if req.game_id not in self.uri_registry: - msg = f"Unrecognised game {req.game_id} attempted allnet auth from {request_ip}." - self.data.base.log_event("allnet", "ALLNET_AUTH_UNKNOWN_GAME", logging.WARN, msg) - self.logger.warn(msg) + self.logger.debug(f"Allnet request: {vars(req)}") - resp.stat = 0 - return self.dict_to_http_form_string([vars(resp)]) - - resp.uri, resp.host = self.uri_registry[req.game_id] - - machine = self.data.arcade.get_machine(req.serial) + machine = self.data.arcade.get_machine(req.serial) if machine is None and not self.config.server.allow_unregistered_serials: msg = f"Unrecognised serial {req.serial} attempted allnet auth from {request_ip}." - self.data.base.log_event("allnet", "ALLNET_AUTH_UNKNOWN_SERIAL", logging.WARN, msg) - self.logger.warn(msg) + self.data.base.log_event( + "allnet", "ALLNET_AUTH_UNKNOWN_SERIAL", logging.WARN, msg + ) + self.logger.warning(msg) + + resp.stat = ALLNET_STAT.bad_machine.value + resp_dict = {k: v for k, v in vars(resp).items() if v is not None} + return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8") - resp.stat = 0 - return self.dict_to_http_form_string([vars(resp)]) - if machine is not None: arcade = self.data.arcade.get_arcade(machine["arcade"]) - req.country = arcade["country"] if machine["country"] is None else machine["country"] - req.place_id = arcade["id"] - req.allnet_id = machine["id"] - req.name = arcade["name"] - req.nickname = arcade["nickname"] - req.region0 = arcade["region_id"] - req.region_name0 = arcade["country"] - req.region_name1 = arcade["state"] - req.region_name2 = arcade["city"] - req.client_timezone = arcade["timezone"] if arcade["timezone"] is not None else "+0900" + 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']})." + self.data.base.log_event( + "allnet", "ALLNET_AUTH_BAD_IP", logging.ERROR, msg + ) + self.logger.warning(msg) + + resp.stat = ALLNET_STAT.bad_shop.value + resp_dict = {k: v for k, v in vars(resp).items() if v is not None} + return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8") + + 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)." + self.data.base.log_event( + "allnet", "ALLNET_AUTH_NO_SHOP_IP", logging.ERROR, msg + ) + self.logger.warning(msg) + + resp.stat = ALLNET_STAT.bad_shop.value + resp_dict = {k: v for k, v in vars(resp).items() if v is not None} + return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8") + + + country = ( + arcade["country"] if machine["country"] is None else machine["country"] + ) + if country is None: + country = AllnetCountryCode.JAPAN.value + + resp.country = country + resp.place_id = arcade["id"] + resp.allnet_id = machine["id"] + resp.name = arcade["name"] if arcade["name"] is not None else "" + resp.nickname = arcade["nickname"] if arcade["nickname"] is not None else "" + resp.region0 = ( + arcade["region_id"] + if arcade["region_id"] is not None + else AllnetJapanRegionId.AICHI.value + ) + resp.region_name0 = ( + arcade["state"] + if arcade["state"] is not None + else AllnetJapanRegionId.AICHI.name + ) + resp.region_name1 = ( + arcade["country"] + if arcade["country"] is not None + else AllnetCountryCode.JAPAN.value + ) + resp.region_name2 = arcade["city"] if arcade["city"] is not None else "" + resp.client_timezone = ( # lmao + arcade["timezone"] if arcade["timezone"] is not None else "+0900" if req.format_ver == 3 else "+09:00" + ) + 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}." + self.data.base.log_event( + "allnet", "ALLNET_AUTH_UNKNOWN_GAME", logging.WARN, msg + ) + 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 (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8") + + else: + self.logger.info( + f"Allowed unknown game {req.game_id} v{req.ver} to authenticate from {request_ip} due to 'is_develop' being enabled. S/N: {req.serial}" + ) + resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/{req.game_id}/{req.ver.replace('.', '')}/" + resp.host = f"{self.config.title.hostname}:{self.config.title.port}" + + resp_dict = {k: v for k, v in vars(resp).items() if v is not None} + resp_str = urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + + self.logger.debug(f"Allnet response: {resp_str}") + return (resp_str + "\n").encode("utf-8") + + + int_ver = req.ver.replace(".", "") + resp.uri, resp.host = TitleServlet.title_registry[req.game_id].get_allnet_info(req.game_id, int(int_ver), req.serial) + msg = f"{req.serial} authenticated from {request_ip}: {req.game_id} v{req.ver}" self.data.base.log_event("allnet", "ALLNET_AUTH_SUCCESS", logging.INFO, msg) self.logger.info(msg) - return self.dict_to_http_form_string([vars(resp)]) + resp_dict = {k: v for k, v in vars(resp).items() if v is not None} + resp_str = urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + self.logger.debug(f"Allnet response: {resp_dict}") + resp_str += "\n" - def handle_dlorder(self, request: Request): - request_ip = request.getClientAddress().host + """if is_dfi: + request.responseHeaders.addRawHeader('Pragma', 'DFI') + return self.to_dfi(resp_str)""" + + return resp_str.encode("utf-8") + + def handle_dlorder(self, request: Request, _: Dict): + request_ip = Utils.get_ip_addr(request) + pragma_header = request.getHeader('Pragma') + is_dfi = pragma_header is not None and pragma_header == "DFI" + try: - req = AllnetDownloadOrderRequest(self.allnet_req_to_dict(request.content.getvalue())) + if is_dfi: + req_urlencode = self.from_dfi(request.content.getvalue()) + else: + req_urlencode = request.content.getvalue().decode() + + req_dict = self.allnet_req_to_dict(req_urlencode) + if req_dict is None: + raise AllnetRequestException() + + req = AllnetDownloadOrderRequest(req_dict[0]) # Validate the request. Currently we only validate the fields we plan on using - if not req.game_id or not req.ver or not req.token or not req.serial or not req.ip: - raise AllnetRequestException(f"Bad auth request params from {request_ip} - {vars(req)}") - + if not req.game_id or not req.ver or not req.serial: + raise AllnetRequestException( + f"Bad download request params from {request_ip} - {vars(req)}" + ) + except AllnetRequestException as e: - self.logger.error(e) + if e.message != "": + self.logger.error(e) return b"" - resp = AllnetDownloadOrderResponse() - if not self.config.allnet.allow_online_updates: - return self.dict_to_http_form_string(vars(resp)) - - else: # TODO: Actual dlorder response - return self.dict_to_http_form_string(vars(resp)) + self.logger.info( + f"DownloadOrder from {request_ip} -> {req.game_id} v{req.ver} serial {req.serial}" + ) + resp = AllnetDownloadOrderResponse(serial=req.serial) - def handle_billing_request(self, request: Request): - req_dict = self.billing_req_to_dict(request.content.getvalue()) - request_ip = request.getClientAddress() + if ( + not self.config.allnet.allow_online_updates + or not self.config.allnet.update_cfg_folder + ): + return urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n" + + else: # TODO: Keychip check + if path.exists( + f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver.replace('.', '')}-app.ini" + ): + resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-app.ini" + + if path.exists( + f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver.replace('.', '')}-opt.ini" + ): + resp.uri += f"|http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-opt.ini" + + self.logger.debug(f"Sending download uri {resp.uri}") + 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}") + + res_str = urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n" + """if is_dfi: + request.responseHeaders.addRawHeader('Pragma', 'DFI') + return self.to_dfi(res_str)""" + + return res_str + + def handle_dlorder_ini(self, request: Request, match: Dict) -> bytes: + if "file" not in match: + return b"" + + req_file = match["file"].replace("%0A", "") + + 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") + self.data.base.log_event("allnet", "DLORDER_INI_SENT", logging.INFO, f"{Utils.get_ip_addr(request)} successfully recieved {req_file}") + + return open( + f"{self.config.allnet.update_cfg_folder}/{req_file}", "rb" + ).read() + + self.logger.info(f"DL INI File {req_file} not found") + return b"" + + def handle_dlorder_report(self, request: Request, match: Dict) -> bytes: + req_raw = request.content.getvalue() + client_ip = Utils.get_ip_addr(request) + try: + req_dict: Dict = json.loads(req_raw) + except Exception as e: + self.logger.warning(f"Failed to parse DL Report: {e}") + return "NG" + + dl_data_type = DLIMG_TYPE.app + dl_data = req_dict.get("appimage", {}) + + if dl_data is None or not dl_data: + dl_data_type = DLIMG_TYPE.opt + dl_data = req_dict.get("optimage", {}) + + if dl_data is None or not dl_data: + self.logger.warning(f"Failed to parse DL Report: Invalid format - contains neither appimage nor optimage") + return "NG" + + rep = DLReport(dl_data, dl_data_type) + + if not rep.validate(): + self.logger.warning(f"Failed to parse DL Report: Invalid format - {rep.err}") + return "NG" + + 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." + + self.data.base.log_event("allnet", "DL_REPORT", logging.INFO, msg, dl_data) + self.logger.info(msg) + + return "OK" + + def handle_loaderstaterecorder(self, request: Request, match: Dict) -> bytes: + req_data = request.content.getvalue() + sections = req_data.decode("utf-8").split("\r\n") + + req_dict = dict(urllib.parse.parse_qsl(sections[0])) + + serial: Union[str, None] = req_dict.get("serial", None) + num_files_to_dl: Union[str, None] = req_dict.get("nb_ftd", None) + num_files_dld: Union[str, None] = req_dict.get("nb_dld", None) + dl_state: Union[str, None] = req_dict.get("dld_st", None) + ip = Utils.get_ip_addr(request) + + if serial is None or num_files_dld is None or num_files_to_dl is None or dl_state is None: + return "NG".encode() + + self.logger.info(f"LoaderStateRecorder Request from {ip} {serial}: {num_files_dld}/{num_files_to_dl} Files download (State: {dl_state})") + return "OK".encode() + + def handle_alive(self, request: Request, match: Dict) -> bytes: + return "OK".encode() + + def handle_billing_request(self, request: Request, _: Dict): + req_raw = request.content.getvalue() + + if request.getHeader('Content-Type') == "application/octet-stream": + req_unzip = zlib.decompressobj(-zlib.MAX_WBITS).decompress(req_raw) + else: + req_unzip = req_raw + + req_dict = self.billing_req_to_dict(req_unzip) + request_ip = Utils.get_ip_addr(request) + if req_dict is None: self.logger.error(f"Failed to parse request {request.content.getvalue()}") return b"" - + self.logger.debug(f"request {req_dict}") - rsa = RSA.import_key(open(self.config.billing.sign_key, 'rb').read()) + rsa = RSA.import_key(open(self.config.billing.signing_key, "rb").read()) signer = PKCS1_v1_5.new(rsa) digest = SHA.new() + traces: List[TraceData] = [] + try: + req = BillingInfo(req_dict[0]) + except KeyError as e: + self.logger.error(f"Billing request failed to parse: {e}") + return f"result=5&linelimit=&message=field is missing or formatting is incorrect\r\n".encode() - kc_playlimit = int(req_dict[0]["playlimit"]) - kc_nearfull = int(req_dict[0]["nearfull"]) - kc_billigtype = int(req_dict[0]["billingtype"]) - kc_playcount = int(req_dict[0]["playcnt"]) - kc_serial: str = req_dict[0]["keychipid"] - kc_game: str = req_dict[0]["gameid"] - kc_date = strptime(req_dict[0]["date"], "%Y%m%d%H%M%S") - kc_serial_bytes = kc_serial.encode() + for x in range(1, len(req_dict)): + if not req_dict[x]: + continue + + try: + tmp = TraceData(req_dict[x]) + if tmp.trace_type == TraceDataType.CHARGE: + tmp = TraceDataCharge(req_dict[x]) + elif tmp.trace_type == TraceDataType.EVENT: + tmp = TraceDataEvent(req_dict[x]) + elif tmp.trace_type == TraceDataType.CREDIT: + tmp = TraceDataCredit(req_dict[x]) + + traces.append(tmp) + + except KeyError as e: + self.logger.warn(f"Tracelog failed to parse: {e}") - machine = self.data.arcade.get_machine(kc_serial) + kc_serial_bytes = req.keychipid.encode() + + + machine = self.data.arcade.get_machine(req.keychipid) if machine is None and not self.config.server.allow_unregistered_serials: - msg = f"Unrecognised serial {kc_serial} attempted billing checkin from {request_ip} for game {kc_game}." - self.data.base.log_event("allnet", "BILLING_CHECKIN_NG_SERIAL", logging.WARN, msg) - self.logger.warn(msg) + msg = f"Unrecognised serial {req.keychipid} attempted billing checkin from {request_ip} for {req.gameid} v{req.gamever}." + self.data.base.log_event( + "allnet", "BILLING_CHECKIN_NG_SERIAL", logging.WARN, msg + ) + self.logger.warning(msg) - resp = BillingResponse("", "", "", "") - resp.result = "1" - return self.dict_to_http_form_string([vars(resp)]) + return f"result=1&requestno={req.requestno}&message=Keychip Serial bad\r\n".encode() - msg = f"Billing checkin from {request.getClientIP()}: game {kc_game} keychip {kc_serial} playcount " \ - f"{kc_playcount} billing_type {kc_billigtype} nearfull {kc_nearfull} playlimit {kc_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) - self.data.base.log_event('billing', 'BILLING_CHECKIN_OK', logging.INFO, msg) + 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 + kc_nearfull = req.nearfull - while kc_playcount > kc_playlimit: + while req.playcnt > req.playlimit: kc_playlimit += 1024 kc_nearfull += 1024 - - playlimit = kc_playlimit - nearfull = kc_nearfull + (kc_billigtype * 0x00010000) - digest.update(playlimit.to_bytes(4, 'little') + kc_serial_bytes) + playlimit = kc_playlimit + nearfull = kc_nearfull + (req.billingtype.value * 0x00010000) + + digest.update(playlimit.to_bytes(4, "little") + kc_serial_bytes) playlimit_sig = signer.sign(digest).hex() digest = SHA.new() - digest.update(nearfull.to_bytes(4, 'little') + kc_serial_bytes) + digest.update(nearfull.to_bytes(4, "little") + kc_serial_bytes) nearfull_sig = signer.sign(digest).hex() # TODO: playhistory - resp = BillingResponse(playlimit, playlimit_sig, nearfull, nearfull_sig) + #resp = BillingResponse(playlimit, playlimit_sig, nearfull, nearfull_sig) + resp = BillingResponse(playlimit, playlimit_sig, nearfull, nearfull_sig, req.requestno, req.protocolver) - resp_str = self.dict_to_http_form_string([vars(resp)]) - if resp_str is None: - self.logger.error(f"Failed to parse response {vars(resp)}") + resp_str = urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\r\n" self.logger.debug(f"response {vars(resp)}") + if req.traceleft > 0: + self.logger.info(f"Requesting 20 more of {req.traceleft} unsent tracelogs") + return f"result=6&waittime=0&linelimit=20\r\n".encode() + return resp_str.encode("utf-8") - def kvp_to_dict(self, *kvp: str) -> List[Dict[str, Any]]: - ret: List[Dict[str, Any]] = [] - for x in kvp: - items = x.split('&') - tmp = {} + def handle_naomitest(self, request: Request, _: Dict) -> bytes: + self.logger.info(f"Ping from {Utils.get_ip_addr(request)}") + return b"naomi ok" - for item in items: - kvp = item.split('=') - if len(kvp) == 2: - tmp[kvp[0]] = kvp[1] - - ret.append(tmp) - - def allnet_req_to_dict(self, data: bytes): + def billing_req_to_dict(self, data: bytes): """ Parses an billing request string into a python dictionary """ try: - decomp = zlib.decompressobj(-zlib.MAX_WBITS) - unzipped = decomp.decompress(data) - sections = unzipped.decode('ascii').split('\r\n') - - return Utils.kvp_to_dict(sections) + sections = data.decode("ascii").split("\r\n") + + ret = [] + for x in sections: + ret.append(dict(urllib.parse.parse_qsl(x))) + return ret except Exception as e: - print(e) + self.logger.error(f"billing_req_to_dict: {e} while parsing {data}") return None - def billing_req_to_dict(self, data: str) -> Optional[List[Dict[str, Any]]]: + def allnet_req_to_dict(self, data: str) -> Optional[List[Dict[str, Any]]]: """ Parses an allnet request string into a python dictionary - """ - try: - zipped = base64.b64decode(data) - unzipped = zlib.decompress(zipped) - sections = unzipped.decode('utf-8').split('\r\n') - - return Utils.kvp_to_dict(sections) - - except Exception as e: - print(e) - return None - - def dict_to_http_form_string(self, data:List[Dict[str, Any]], crlf: bool = False, trailing_newline: bool = True) -> Optional[str]: - """ - Takes a python dictionary and parses it into an allnet response string """ try: - urlencode = "" - for item in data: - for k,v in item.items(): - urlencode += f"{k}={v}&" + sections = data.split("\r\n") - if crlf: - urlencode = urlencode[:-1] + "\r\n" - else: - urlencode = urlencode[:-1] + "\n" - - if not trailing_newline: - if crlf: - urlencode = urlencode[:-2] - else: - urlencode = urlencode[:-1] + ret = [] + for x in sections: + ret.append(dict(urllib.parse.parse_qsl(x))) + return ret - return urlencode - except Exception as e: - print(e) + self.logger.error(f"allnet_req_to_dict: {e} while parsing {data}") return None -class AllnetPowerOnRequest(): + def from_dfi(self, data: bytes) -> str: + zipped = base64.b64decode(data) + unzipped = zlib.decompress(zipped) + return unzipped.decode("utf-8") + + def to_dfi(self, data: str) -> bytes: + unzipped = data.encode('utf-8') + zipped = zlib.compress(unzipped) + return base64.b64encode(zipped) + + +class AllnetPowerOnRequest: def __init__(self, req: Dict) -> None: if req is None: raise AllnetRequestException("Request processing failed") - self.game_id: str = req["game_id"] if "game_id" in req else "" - self.ver: str = req["ver"] if "ver" in req else "" - self.serial: str = req["serial"] if "serial" in req else "" - self.ip: str = req["ip"] if "ip" in req else "" - self.firm_ver: str = req["firm_ver"] if "firm_ver" in req else "" - self.boot_ver: str = req["boot_ver"] if "boot_ver" in req else "" - self.encode: str = req["encode"] if "encode" in req else "" - - try: - self.hops = int(req["hops"]) if "hops" in req else 0 - self.format_ver = int(req["format_ver"]) if "format_ver" in req else 2 - self.token = int(req["token"]) if "token" in req else 0 - except ValueError as e: - raise AllnetRequestException(f"Failed to parse int: {e}") + self.game_id: str = req.get("game_id", None) + self.ver: str = req.get("ver", None) + self.serial: str = req.get("serial", None) + self.ip: str = req.get("ip", None) + self.firm_ver: str = req.get("firm_ver", None) + self.boot_ver: str = req.get("boot_ver", None) + self.encode: str = req.get("encode", "EUC-JP") + self.hops = int(req.get("hops", "-1")) + self.format_ver = float(req.get("format_ver", "1.00")) + self.token: str = req.get("token", "0") -class AllnetPowerOnResponse3(): - def __init__(self, token) -> None: - self.stat = 1 - self.uri = "" - self.host = "" - self.place_id = "123" - self.name = "" - self.nickname = "" - self.region0 = "1" - self.region_name0 = "W" - self.region_name1 = "" - self.region_name2 = "" - self.region_name3 = "" - self.country = "JPN" - self.allnet_id = "123" - self.client_timezone = "+0900" - self.utc_time = datetime.now(tz=pytz.timezone('UTC')).strftime("%Y-%m-%dT%H:%M:%SZ") - self.setting = "" - self.res_ver = "3" - self.token = str(token) - -class AllnetPowerOnResponse2(): +class AllnetPowerOnResponse: def __init__(self) -> None: self.stat = 1 self.uri = "" self.host = "" - self.place_id = "123" - self.name = "Test" - self.nickname = "Test123" + self.place_id = "0123" + self.name = "ARTEMiS" + self.nickname = "ARTEMiS" self.region0 = "1" self.region_name0 = "W" - self.region_name1 = "X" - self.region_name2 = "Y" - self.region_name3 = "Z" - self.country = "JPN" + self.region_name1 = "" + self.region_name2 = "" + self.region_name3 = "" + self.setting = "1" self.year = datetime.now().year self.month = datetime.now().month self.day = datetime.now().day self.hour = datetime.now().hour self.minute = datetime.now().minute self.second = datetime.now().second - self.setting = "1" - self.timezone = "+0900" + +class AllnetPowerOnResponse3(AllnetPowerOnResponse): + def __init__(self, token) -> None: + super().__init__() + + # Added in v3 + self.country = "JPN" + self.allnet_id = "123" + self.client_timezone = "+0900" + self.utc_time = datetime.now(tz=pytz.timezone("UTC")).strftime( + "%Y-%m-%dT%H:%M:%SZ" + ) + self.res_ver = "3" + self.token = token + + # Removed in v3 + self.year = None + self.month = None + self.day = None + self.hour = None + self.minute = None + self.second = None + + +class AllnetPowerOnResponse2(AllnetPowerOnResponse): + def __init__(self) -> None: + super().__init__() + + # Added in v2 + self.country = "JPN" + self.timezone = "+09:00" self.res_class = "PowerOnResponseV2" -class AllnetDownloadOrderRequest(): - def __init__(self, req: Dict) -> None: - self.game_id = req["game_id"] if "game_id" in req else "" - self.ver = req["ver"] if "ver" in req else "" - self.serial = req["serial"] if "serial" in req else "" - self.encode = req["encode"] if "encode" in req else "" -class AllnetDownloadOrderResponse(): - def __init__(self, stat: int = 1, serial: str = "", uri: str = "null") -> None: +class AllnetDownloadOrderRequest: + def __init__(self, req: Dict) -> None: + self.game_id = req.get("game_id", "") + self.ver = req.get("ver", "") + self.serial = req.get("serial", "") + self.encode = req.get("encode", "") + + +class AllnetDownloadOrderResponse: + def __init__(self, stat: int = 1, serial: str = "", uri: str = "") -> None: self.stat = stat self.serial = serial self.uri = uri -class BillingResponse(): - def __init__(self, playlimit: str = "", playlimit_sig: str = "", nearfull: str = "", nearfull_sig: str = "", - playhistory: str = "000000/0:000000/0:000000/0") -> None: +class TraceDataType(Enum): + CHARGE = 0 + EVENT = 1 + CREDIT = 2 - self.result = "0" - self.waitime = "100" - self.linelimit = "1" - self.message = "" +class BillingType(Enum): + A = 1 + B = 0 + +class float5: + def __init__(self, n: str = "0") -> None: + nf = float(n) + if nf > 999.9 or nf < 0: + raise ValueError('float5 must be between 0.000 and 999.9 inclusive') + + return nf + + @classmethod + def to_str(cls, f: float): + return f"%.{2 - int(math.log10(f))+1}f" % f + +class BillingInfo: + def __init__(self, data: Dict) -> None: + try: + self.keychipid = str(data.get("keychipid", None)) + self.functype = int(data.get("functype", None)) + self.gameid = str(data.get("gameid", None)) + self.gamever = float(data.get("gamever", None)) + self.boardid = str(data.get("boardid", None)) + self.tenpoip = str(data.get("tenpoip", None)) + self.libalibver = float(data.get("libalibver", None)) + self.datamax = int(data.get("datamax", None)) + self.billingtype = BillingType(int(data.get("billingtype", None))) + self.protocolver = float(data.get("protocolver", None)) + self.operatingfix = bool(data.get("operatingfix", None)) + self.traceleft = int(data.get("traceleft", None)) + self.requestno = int(data.get("requestno", None)) + self.datesync = bool(data.get("datesync", None)) + self.timezone = str(data.get("timezone", None)) + self.date = datetime.strptime(data.get("date", None), BILLING_DT_FORMAT) + self.crcerrcnt = int(data.get("crcerrcnt", None)) + self.memrepair = bool(data.get("memrepair", None)) + self.playcnt = int(data.get("playcnt", None)) + self.playlimit = int(data.get("playlimit", None)) + self.nearfull = int(data.get("nearfull", None)) + except Exception as e: + raise KeyError(e) + +class TraceData: + def __init__(self, data: Dict) -> None: + try: + self.crc_err_flg = bool(data.get("cs", None)) + self.record_number = int(data.get("rn", None)) + self.seq_number = int(data.get("sn", None)) + self.trace_type = TraceDataType(int(data.get("tt", None))) + self.date_sync_flg = bool(data.get("ds", None)) + + dt = data.get("dt", None) + if dt is None: + raise KeyError("dt not present") + if dt == "20000000000000": # Not sure what causes it to send like this... + self.date = datetime(2000, 1, 1, 0, 0, 0, 0) + else: + self.date = datetime.strptime(data.get("dt", None), BILLING_DT_FORMAT) + + self.keychip = str(data.get("kn", None)) + self.lib_ver = float(data.get("alib", 0)) + except Exception as e: + raise KeyError(e) + +class TraceDataCharge(TraceData): + def __init__(self, data: Dict) -> None: + super().__init__(data) + try: + self.game_id = str(data.get("gi", None)) # these seem optional...? + self.game_version = float(data.get("gv", 0)) + self.board_serial = str(data.get("bn", None)) + self.shop_ip = str(data.get("ti", None)) + self.play_count = int(data.get("pc", None)) + self.play_limit = int(data.get("pl", None)) + self.product_code = int(data.get("ic", None)) + self.product_count = int(data.get("in", None)) + self.func_type = int(data.get("kk", None)) + self.player_number = int(data.get("playerno", None)) + except Exception as e: + raise KeyError(e) + +class TraceDataEvent(TraceData): + def __init__(self, data: Dict) -> None: + super().__init__(data) + try: + self.message = str(data.get("me", None)) + except Exception as e: + raise KeyError(e) + +class TraceDataCredit(TraceData): + def __init__(self, data: Dict) -> None: + super().__init__(data) + try: + self.chute_type = int(data.get("cct", None)) + self.service_type = int(data.get("cst", None)) + self.operation_type = int(data.get("cop", None)) + self.coin_rate0 = int(data.get("cr0", None)) + self.coin_rate1 = int(data.get("cr1", None)) + self.bonus_addition = int(data.get("cba", None)) + self.credit_rate = int(data.get("ccr", None)) + self.credit0 = int(data.get("cc0", None)) + self.credit1 = int(data.get("cc1", None)) + self.credit2 = int(data.get("cc2", None)) + self.credit3 = int(data.get("cc3", None)) + self.credit4 = int(data.get("cc4", None)) + self.credit5 = int(data.get("cc5", None)) + self.credit6 = int(data.get("cc6", None)) + self.credit7 = int(data.get("cc7", None)) + except Exception as e: + raise KeyError(e) + +class BillingResponse: + def __init__( + self, + playlimit: str = "", + playlimit_sig: str = "", + nearfull: str = "", + nearfull_sig: str = "", + request_num: int = 1, + protocol_ver: float = 1.000, + playhistory: str = "000000/0:000000/0:000000/0", + ) -> None: + self.result = 0 + self.requestno = request_num + self.traceerase = 1 + self.fixinterval = 120 + self.fixlogcnt = 100 self.playlimit = playlimit self.playlimitsig = playlimit_sig - self.protocolver = "1.000" + self.playhistory = playhistory self.nearfull = nearfull self.nearfullsig = nearfull_sig - self.fixlogincnt = "0" - self.fixinterval = "5" - self.playhistory = playhistory + self.linelimit = 100 + self.protocolver = float5.to_str(protocol_ver) # playhistory -> YYYYMM/C:... # YYYY -> 4 digit year, MM -> 2 digit month, C -> Playcount during that period + class AllnetRequestException(Exception): - def __init__(self, message="Allnet Request Error") -> None: + def __init__(self, message="") -> None: self.message = message super().__init__(self.message) + +class DLReport: + def __init__(self, data: Dict, report_type: DLIMG_TYPE) -> None: + self.serial = data.get("serial") + self.dfl = data.get("dfl") + self.wfl = data.get("wfl") + self.tsc = data.get("tsc") + self.tdsc = data.get("tdsc") + self.at = data.get("at") + self.ot = data.get("ot") + self.rt = data.get("rt") + self.as_ = data.get("as") + self.rf_state = DLI_STATUS.from_int(data.get("rf_state")) + self.gd = data.get("gd") + self.dav = data.get("dav") + self.wdav = data.get("wdav") # app only + self.dov = data.get("dov") + self.wdov = data.get("wdov") # app only + self.rep_type = report_type + self.err = "" + + def validate(self) -> bool: + if self.serial is None: + self.err = "serial not provided" + return False + + if self.tsc is None: + self.err = "tsc not provided" + return False + + if self.tdsc is None: + self.err = "tdsc not provided" + return False + + if self.as_ is None: + self.err = "as not provided" + return False + + if self.rf_state is None: + self.err = "rf_state not provided" + return False + + if self.gd is None: + self.err = "gd not provided" + return False + + if self.dav is None: + self.err = "dav not provided" + return False + + if self.dov is None: + self.err = "dov not provided" + return False + + if (self.wdav is None or self.wdov is None) and self.rep_type == DLIMG_TYPE.app: + self.err = "wdav or wdov not provided in app image" + return False + + if (self.wdav is not None or self.wdov is not None) and self.rep_type == DLIMG_TYPE.opt: + self.err = "wdav or wdov provided in opt image" + return False + + return True diff --git a/core/config.py b/core/config.py index f8f57e4..68db052 100644 --- a/core/config.py +++ b/core/config.py @@ -1,33 +1,71 @@ import logging, os from typing import Any + class ServerConfig: def __init__(self, parent_config: "CoreConfig") -> None: self.__config = parent_config @property def listen_address(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'server', 'listen_address', default='127.0.0.1') + return CoreConfig.get_config_field( + self.__config, "core", "server", "listen_address", default="127.0.0.1" + ) @property def allow_user_registration(self) -> bool: - return CoreConfig.get_config_field(self.__config, 'core', 'server', 'allow_user_registration', default=True) + return CoreConfig.get_config_field( + self.__config, "core", "server", "allow_user_registration", default=True + ) @property def allow_unregistered_serials(self) -> bool: - return CoreConfig.get_config_field(self.__config, 'core', 'server', 'allow_unregistered_serials', default=True) + return CoreConfig.get_config_field( + self.__config, "core", "server", "allow_unregistered_serials", default=True + ) @property def name(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'server', 'name', default="ARTEMiS") + return CoreConfig.get_config_field( + self.__config, "core", "server", "name", default="ARTEMiS" + ) @property def is_develop(self) -> bool: - return CoreConfig.get_config_field(self.__config, 'core', 'server', 'is_develop', default=True) + return CoreConfig.get_config_field( + self.__config, "core", "server", "is_develop", default=True + ) + + @property + def is_using_proxy(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "core", "server", "is_using_proxy", default=False + ) + + @property + def threading(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "core", "server", "threading", default=False + ) @property def log_dir(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'server', 'log_dir', default='logs') + return CoreConfig.get_config_field( + self.__config, "core", "server", "log_dir", default="logs" + ) + + @property + def check_arcade_ip(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "core", "server", "check_arcade_ip", default=False + ) + + @property + def strict_ip_checking(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "core", "server", "strict_ip_checking", default=False + ) + class TitleConfig: def __init__(self, parent_config: "CoreConfig") -> None: @@ -35,15 +73,54 @@ class TitleConfig: @property def loglevel(self) -> int: - return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'core', 'title', 'loglevel', default="info")) + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "core", "title", "loglevel", default="info" + ) + ) @property def hostname(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'title', 'hostname', default="localhost") + return CoreConfig.get_config_field( + self.__config, "core", "title", "hostname", default="localhost" + ) @property def port(self) -> int: - return CoreConfig.get_config_field(self.__config, 'core', 'title', 'port', default=8080) + return CoreConfig.get_config_field( + self.__config, "core", "title", "port", default=8080 + ) + + @property + def port_ssl(self) -> int: + return CoreConfig.get_config_field( + self.__config, "core", "title", "port_ssl", default=0 + ) + + @property + def ssl_key(self) -> str: + return CoreConfig.get_config_field( + self.__config, "core", "title", "ssl_key", default="cert/title.key" + ) + + @property + def ssl_cert(self) -> str: + return CoreConfig.get_config_field( + self.__config, "core", "title", "ssl_cert", default="cert/title.pem" + ) + + @property + def reboot_start_time(self) -> str: + return CoreConfig.get_config_field( + self.__config, "core", "title", "reboot_start_time", default="" + ) + + @property + def reboot_end_time(self) -> str: + return CoreConfig.get_config_field( + self.__config, "core", "title", "reboot_end_time", default="" + ) + class DatabaseConfig: def __init__(self, parent_config: "CoreConfig") -> None: @@ -51,43 +128,76 @@ class DatabaseConfig: @property def host(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'database', 'host', default="localhost") + return CoreConfig.get_config_field( + self.__config, "core", "database", "host", default="localhost" + ) @property def username(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'database', 'username', default='aime') + return CoreConfig.get_config_field( + self.__config, "core", "database", "username", default="aime" + ) @property def password(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'database', 'password', default='aime') + return CoreConfig.get_config_field( + self.__config, "core", "database", "password", default="aime" + ) @property def name(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'database', 'name', default='aime') + return CoreConfig.get_config_field( + self.__config, "core", "database", "name", default="aime" + ) @property def port(self) -> int: - return CoreConfig.get_config_field(self.__config, 'core', 'database', 'port', default=3306) + return CoreConfig.get_config_field( + self.__config, "core", "database", "port", default=3306 + ) @property def protocol(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'database', 'type', default="mysql") - + return CoreConfig.get_config_field( + self.__config, "core", "database", "type", default="mysql" + ) + @property def sha2_password(self) -> bool: - return CoreConfig.get_config_field(self.__config, 'core', 'database', 'sha2_password', default=False) - + return CoreConfig.get_config_field( + self.__config, "core", "database", "sha2_password", default=False + ) + @property def loglevel(self) -> int: - return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'core', 'database', 'loglevel', default="info")) + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "core", "database", "loglevel", default="info" + ) + ) @property def user_table_autoincrement_start(self) -> int: - return CoreConfig.get_config_field(self.__config, 'core', 'database', 'user_table_autoincrement_start', default=10000) - + return CoreConfig.get_config_field( + self.__config, + "core", + "database", + "user_table_autoincrement_start", + default=10000, + ) + + @property + def enable_memcached(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "core", "database", "enable_memcached", default=True + ) + @property def memcached_host(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'database', 'memcached_host', default="localhost") + return CoreConfig.get_config_field( + self.__config, "core", "database", "memcached_host", default="localhost" + ) + class FrontendConfig: def __init__(self, parent_config: "CoreConfig") -> None: @@ -95,15 +205,24 @@ class FrontendConfig: @property def enable(self) -> int: - return CoreConfig.get_config_field(self.__config, 'core', 'frontend', 'enable', default=False) + return CoreConfig.get_config_field( + self.__config, "core", "frontend", "enable", default=False + ) @property def port(self) -> int: - return CoreConfig.get_config_field(self.__config, 'core', 'frontend', 'port', default=8090) - + return CoreConfig.get_config_field( + self.__config, "core", "frontend", "port", default=8090 + ) + @property def loglevel(self) -> int: - return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'core', 'frontend', 'loglevel', default="info")) + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "core", "frontend", "loglevel", default="info" + ) + ) + class AllnetConfig: def __init__(self, parent_config: "CoreConfig") -> None: @@ -111,15 +230,36 @@ class AllnetConfig: @property def loglevel(self) -> int: - return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'core', 'allnet', 'loglevel', default="info")) + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "core", "allnet", "loglevel", default="info" + ) + ) @property def port(self) -> int: - return CoreConfig.get_config_field(self.__config, 'core', 'allnet', 'port', default=80) - + return CoreConfig.get_config_field( + self.__config, "core", "allnet", "port", default=80 + ) + + @property + def ip_check(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "core", "allnet", "ip_check", default=False + ) + @property def allow_online_updates(self) -> int: - return CoreConfig.get_config_field(self.__config, 'core', 'allnet', 'allow_online_updates', default=False) + return CoreConfig.get_config_field( + self.__config, "core", "allnet", "allow_online_updates", default=False + ) + + @property + def update_cfg_folder(self) -> str: + return CoreConfig.get_config_field( + self.__config, "core", "allnet", "update_cfg_folder", default="" + ) + class BillingConfig: def __init__(self, parent_config: "CoreConfig") -> None: @@ -127,35 +267,65 @@ class BillingConfig: @property def port(self) -> int: - return CoreConfig.get_config_field(self.__config, 'core', 'billing', 'port', default=8443) + return CoreConfig.get_config_field( + self.__config, "core", "billing", "port", default=8443 + ) @property def ssl_key(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'billing', 'ssl_key', default="cert/server.key") + return CoreConfig.get_config_field( + self.__config, "core", "billing", "ssl_key", default="cert/server.key" + ) @property def ssl_cert(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'billing', 'ssl_cert', default="cert/server.pem") - + return CoreConfig.get_config_field( + self.__config, "core", "billing", "ssl_cert", default="cert/server.pem" + ) + @property def signing_key(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'billing', 'signing_key', default="cert/billing.key") + return CoreConfig.get_config_field( + self.__config, "core", "billing", "signing_key", default="cert/billing.key" + ) + class AimedbConfig: def __init__(self, parent_config: "CoreConfig") -> None: self.__config = parent_config - + @property def loglevel(self) -> int: - return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'core', 'aimedb', 'loglevel', default="info")) + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "core", "aimedb", "loglevel", default="info" + ) + ) @property def port(self) -> int: - return CoreConfig.get_config_field(self.__config, 'core', 'aimedb', 'port', default=22345) + return CoreConfig.get_config_field( + self.__config, "core", "aimedb", "port", default=22345 + ) @property def key(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'aimedb', 'key', default="") + return CoreConfig.get_config_field( + self.__config, "core", "aimedb", "key", default="" + ) + + @property + def id_secret(self) -> str: + return CoreConfig.get_config_field( + self.__config, "core", "aimedb", "id_secret", default="" + ) + + @property + def id_lifetime_seconds(self) -> int: + return CoreConfig.get_config_field( + self.__config, "core", "aimedb", "id_lifetime_seconds", default=86400 + ) + class MuchaConfig: def __init__(self, parent_config: "CoreConfig") -> None: @@ -163,27 +333,24 @@ class MuchaConfig: @property def enable(self) -> int: - return CoreConfig.get_config_field(self.__config, 'core', 'mucha', 'enable', default=False) - - @property - def loglevel(self) -> int: - return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'core', 'mucha', 'loglevel', default="info")) - - @property - def hostname(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'mucha', 'hostname', default="localhost") + return CoreConfig.get_config_field( + self.__config, "core", "mucha", "enable", default=False + ) @property - def port(self) -> int: - return CoreConfig.get_config_field(self.__config, 'core', 'mucha', 'port', default=8444) - + def loglevel(self) -> int: + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "core", "mucha", "loglevel", default="info" + ) + ) + @property - def ssl_cert(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'mucha', 'ssl_cert', default="cert/server.pem") - - @property - def signing_key(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'mucha', 'signing_key', default="cert/billing.key") + def hostname(self) -> str: + return CoreConfig.get_config_field( + self.__config, "core", "mucha", "hostname", default="localhost" + ) + class CoreConfig(dict): def __init__(self) -> None: @@ -194,25 +361,28 @@ class CoreConfig(dict): self.allnet = AllnetConfig(self) self.billing = BillingConfig(self) self.aimedb = AimedbConfig(self) + self.mucha = MuchaConfig(self) @classmethod def str_to_loglevel(cls, level_str: str): if level_str.lower() == "error": return logging.ERROR - elif level_str.lower().startswith("warn"): # Fits warn or warning + elif level_str.lower().startswith("warn"): # Fits warn or warning return logging.WARN elif level_str.lower() == "debug": return logging.DEBUG else: - return logging.INFO + return logging.INFO @classmethod - def get_config_field(cls, __config: dict, module, *path: str, default: Any = "") -> Any: - envKey = f'CFG_{module}_' + def get_config_field( + cls, __config: dict, module, *path: str, default: Any = "" + ) -> Any: + envKey = f"CFG_{module}_" for arg in path: - envKey += arg + '_' - - if envKey.endswith('_'): + envKey += arg + "_" + + if envKey.endswith("_"): envKey = envKey[:-1] if envKey in os.environ: diff --git a/core/const.py b/core/const.py index f5979e8..98effb6 100644 --- a/core/const.py +++ b/core/const.py @@ -1,6 +1,7 @@ from enum import Enum -class MainboardPlatformCodes(): + +class MainboardPlatformCodes: RINGEDGE = "AALE" RINGWIDE = "AAML" NU = "AAVE" @@ -8,7 +9,8 @@ class MainboardPlatformCodes(): ALLS_UX = "ACAE" ALLS_HX = "ACAX" -class MainboardRevisions(): + +class MainboardRevisions: RINGEDGE = 1 RINGEDGE2 = 2 @@ -26,11 +28,70 @@ class MainboardRevisions(): ALLS_UX2 = 2 ALLS_HX2 = 12 -class KeychipPlatformsCodes(): + +class KeychipPlatformsCodes: RING = "A72E" NU = ("A60E", "A60E", "A60E") NUSX = ("A61X", "A69X") ALLS = "A63E" - -class RegionIDs(Enum): - pass \ No newline at end of file + + +class AllnetCountryCode(Enum): + JAPAN = "JPN" + UNITED_STATES = "USA" + HONG_KONG = "HKG" + SINGAPORE = "SGP" + SOUTH_KOREA = "KOR" + TAIWAN = "TWN" + CHINA = "CHN" + + +class AllnetJapanRegionId(Enum): + NONE = 0 + AICHI = 1 + AOMORI = 2 + AKITA = 3 + ISHIKAWA = 4 + IBARAKI = 5 + IWATE = 6 + EHIME = 7 + OITA = 8 + OSAKA = 9 + OKAYAMA = 10 + OKINAWA = 11 + KAGAWA = 12 + KAGOSHIMA = 13 + KANAGAWA = 14 + GIFU = 15 + KYOTO = 16 + KUMAMOTO = 17 + GUNMA = 18 + KOCHI = 19 + SAITAMA = 20 + SAGA = 21 + SHIGA = 22 + SHIZUOKA = 23 + SHIMANE = 24 + CHIBA = 25 + TOKYO = 26 + TOKUSHIMA = 27 + TOCHIGI = 28 + TOTTORI = 29 + TOYAMA = 30 + NAGASAKI = 31 + NAGANO = 32 + NARA = 33 + NIIGATA = 34 + HYOGO = 35 + HIROSHIMA = 36 + FUKUI = 37 + FUKUOKA = 38 + FUKUSHIMA = 39 + HOKKAIDO = 40 + MIE = 41 + MIYAGI = 42 + MIYAZAKI = 43 + YAMAGATA = 44 + YAMAGUCHI = 45 + YAMANASHI = 46 + WAKAYAMA = 47 diff --git a/core/data/__init__.py b/core/data/__init__.py index 4eee928..eb30d05 100644 --- a/core/data/__init__.py +++ b/core/data/__init__.py @@ -1,2 +1,2 @@ from core.data.database import Data -from core.data.cache import cached \ No newline at end of file +from core.data.cache import cached diff --git a/core/data/cache.py b/core/data/cache.py index cdea825..1490826 100644 --- a/core/data/cache.py +++ b/core/data/cache.py @@ -1,4 +1,3 @@ - from typing import Any, Callable from functools import wraps import hashlib @@ -6,27 +5,28 @@ import pickle import logging from core.config import CoreConfig -cfg:CoreConfig = None # type: ignore +cfg: CoreConfig = None # type: ignore # Make memcache optional try: import pylibmc # type: ignore + has_mc = True except ModuleNotFoundError: has_mc = False -def cached(lifetime: int=10, extra_key: Any=None) -> Callable: + +def cached(lifetime: int = 10, extra_key: Any = None) -> Callable: def _cached(func: Callable) -> Callable: - if has_mc: + if has_mc and (cfg and cfg.database.enable_memcached): hostname = "127.0.0.1" if cfg: hostname = cfg.database.memcached_host memcache = pylibmc.Client([hostname], binary=True) memcache.behaviors = {"tcp_nodelay": True, "ketama": True} - + @wraps(func) def wrapper(*args: Any, **kwargs: Any) -> Any: if lifetime is not None: - # Hash function args items = kwargs.items() hashable_args = (args[1:], sorted(list(items))) @@ -41,7 +41,7 @@ def cached(lifetime: int=10, extra_key: Any=None) -> Callable: except pylibmc.Error as e: logging.getLogger("database").error(f"Memcache failed: {e}") result = None - + if result is not None: logging.getLogger("database").debug(f"Cache hit: {result}") return result @@ -55,7 +55,9 @@ def cached(lifetime: int=10, extra_key: Any=None) -> Callable: memcache.set(cache_key, result, lifetime) return result + else: + @wraps(func) def wrapper(*args: Any, **kwargs: Any) -> Any: return func(*args, **kwargs) diff --git a/core/data/database.py b/core/data/database.py index 800b5d0..e39d864 100644 --- a/core/data/database.py +++ b/core/data/database.py @@ -1,45 +1,70 @@ import logging, coloredlogs -from typing import Any, Dict, List +from typing import Optional, Dict, List from sqlalchemy.orm import scoped_session, sessionmaker from sqlalchemy.exc import SQLAlchemyError from sqlalchemy import create_engine from logging.handlers import TimedRotatingFileHandler - +import importlib, os +import secrets, string +import bcrypt from hashlib import sha256 from core.config import CoreConfig from core.data.schema import * +from core.utils import Utils + class Data: + current_schema_version = 6 + engine = None + session = None + user = None + arcade = None + card = None + base = None def __init__(self, cfg: CoreConfig) -> None: self.config = cfg if self.config.database.sha2_password: passwd = sha256(self.config.database.password.encode()).digest() - self.__url = f"{self.config.database.protocol}://{self.config.database.username}:{passwd.hex()}@{self.config.database.host}/{self.config.database.name}?charset=utf8mb4" + self.__url = f"{self.config.database.protocol}://{self.config.database.username}:{passwd.hex()}@{self.config.database.host}:{self.config.database.port}/{self.config.database.name}?charset=utf8mb4" else: - self.__url = f"{self.config.database.protocol}://{self.config.database.username}:{self.config.database.password}@{self.config.database.host}/{self.config.database.name}?charset=utf8mb4" + self.__url = f"{self.config.database.protocol}://{self.config.database.username}:{self.config.database.password}@{self.config.database.host}:{self.config.database.port}/{self.config.database.name}?charset=utf8mb4" + + if Data.engine is None: + Data.engine = create_engine(self.__url, pool_recycle=3600) + self.__engine = Data.engine + + if Data.session is None: + s = sessionmaker(bind=Data.engine, autoflush=True, autocommit=True) + Data.session = scoped_session(s) + + if Data.user is None: + Data.user = UserData(self.config, self.session) - self.__engine = create_engine(self.__url, pool_recycle=3600) - session = sessionmaker(bind=self.__engine, autoflush=True, autocommit=True) - self.session = scoped_session(session) + if Data.arcade is None: + Data.arcade = ArcadeData(self.config, self.session) + + if Data.card is None: + Data.card = CardData(self.config, self.session) + + if Data.base is None: + Data.base = BaseData(self.config, self.session) - self.user = UserData(self.config, self.session) - self.arcade = ArcadeData(self.config, self.session) - self.card = CardData(self.config, self.session) - self.base = BaseData(self.config, self.session) - self.schema_ver_latest = 1 - - log_fmt_str = "[%(asctime)s] %(levelname)s | Database | %(message)s" - log_fmt = logging.Formatter(log_fmt_str) self.logger = logging.getLogger("database") # Prevent the logger from adding handlers multiple times - if not getattr(self.logger, 'handler_set', None): - fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.config.server.log_dir, "db"), encoding="utf-8", - when="d", backupCount=10) + if not getattr(self.logger, "handler_set", None): + log_fmt_str = "[%(asctime)s] %(levelname)s | Database | %(message)s" + log_fmt = logging.Formatter(log_fmt_str) + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.config.server.log_dir, "db"), + encoding="utf-8", + when="d", + backupCount=10, + ) fileHandler.setFormatter(log_fmt) - + consoleHandler = logging.StreamHandler() consoleHandler.setFormatter(log_fmt) @@ -47,7 +72,286 @@ class Data: self.logger.addHandler(consoleHandler) self.logger.setLevel(self.config.database.loglevel) - coloredlogs.install(cfg.database.loglevel, logger=self.logger, fmt=log_fmt_str) - self.logger.handler_set = True # type: ignore + coloredlogs.install( + cfg.database.loglevel, logger=self.logger, fmt=log_fmt_str + ) + self.logger.handler_set = True # type: ignore - \ No newline at end of file + def create_database(self): + self.logger.info("Creating databases...") + try: + metadata.create_all(self.__engine.connect()) + except SQLAlchemyError as e: + self.logger.error(f"Failed to create databases! {e}") + return + + games = Utils.get_all_titles() + for game_dir, game_mod in games.items(): + try: + if hasattr(game_mod, "database") and hasattr( + game_mod, "current_schema_version" + ): + game_mod.database(self.config) + metadata.create_all(self.__engine.connect()) + + self.base.touch_schema_ver( + game_mod.current_schema_version, game_mod.game_codes[0] + ) + + except Exception as e: + self.logger.warning( + f"Could not load database schema from {game_dir} - {e}" + ) + + self.logger.info(f"Setting base_schema_ver to {self.current_schema_version}") + self.base.set_schema_ver(self.current_schema_version) + + self.logger.info( + f"Setting user auto_incrememnt to {self.config.database.user_table_autoincrement_start}" + ) + self.user.reset_autoincrement( + self.config.database.user_table_autoincrement_start + ) + + def recreate_database(self): + self.logger.info("Dropping all databases...") + self.base.execute("SET FOREIGN_KEY_CHECKS=0") + try: + metadata.drop_all(self.__engine.connect()) + except SQLAlchemyError as e: + self.logger.error(f"Failed to drop databases! {e}") + return + + for root, dirs, files in os.walk("./titles"): + for dir in dirs: + if not dir.startswith("__"): + try: + mod = importlib.import_module(f"titles.{dir}") + + try: + if hasattr(mod, "database"): + mod.database(self.config) + metadata.drop_all(self.__engine.connect()) + + except Exception as e: + self.logger.warning( + f"Could not load database schema from {dir} - {e}" + ) + + except ImportError as e: + self.logger.warning( + f"Failed to load database schema dir {dir} - {e}" + ) + break + + self.base.execute("SET FOREIGN_KEY_CHECKS=1") + + self.create_database() + + def migrate_database(self, game: str, version: Optional[int], action: str) -> None: + old_ver = self.base.get_schema_ver(game) + sql = "" + if version is None: + if not game == "CORE": + titles = Utils.get_all_titles() + + for folder, mod in titles.items(): + if not mod.game_codes[0] == game: + continue + + if hasattr(mod, "current_schema_version"): + version = mod.current_schema_version + + else: + self.logger.warning( + f"current_schema_version not found for {folder}" + ) + + else: + version = self.current_schema_version + + if version is None: + self.logger.warning( + f"Could not determine latest version for {game}, please specify --version" + ) + + if old_ver is None: + self.logger.error( + f"Schema for game {game} does not exist, did you run the creation script?" + ) + return + + if old_ver == version: + self.logger.info( + f"Schema for game {game} is already version {old_ver}, nothing to do" + ) + return + + if action == "upgrade": + for x in range(old_ver, version): + if not os.path.exists( + f"core/data/schema/versions/{game.upper()}_{x + 1}_{action}.sql" + ): + self.logger.error( + f"Could not find {action} script {game.upper()}_{x + 1}_{action}.sql in core/data/schema/versions folder" + ) + return + + with open( + f"core/data/schema/versions/{game.upper()}_{x + 1}_{action}.sql", + "r", + encoding="utf-8", + ) as f: + sql = f.read() + + result = self.base.execute(sql) + if result is None: + self.logger.error("Error execuing sql script!") + return None + + else: + for x in range(old_ver, version, -1): + if not os.path.exists( + f"core/data/schema/versions/{game.upper()}_{x - 1}_{action}.sql" + ): + self.logger.error( + f"Could not find {action} script {game.upper()}_{x - 1}_{action}.sql in core/data/schema/versions folder" + ) + return + + with open( + f"core/data/schema/versions/{game.upper()}_{x - 1}_{action}.sql", + "r", + encoding="utf-8", + ) as f: + sql = f.read() + + result = self.base.execute(sql) + if result is None: + self.logger.error("Error execuing sql script!") + return None + + result = self.base.set_schema_ver(version, game) + if result is None: + self.logger.error("Error setting version in schema_version table!") + return None + + self.logger.info(f"Successfully migrated {game} to schema version {version}") + + def create_owner(self, email: Optional[str] = None) -> None: + pw = "".join( + secrets.choice(string.ascii_letters + string.digits) for i in range(20) + ) + hash = bcrypt.hashpw(pw.encode(), bcrypt.gensalt()) + + user_id = self.user.create_user(email=email, permission=255, password=hash) + if user_id is None: + self.logger.error(f"Failed to create owner with email {email}") + return + + card_id = self.card.create_card(user_id, "00000000000000000000") + if card_id is None: + self.logger.error(f"Failed to create card for owner with id {user_id}") + return + + self.logger.warning( + f"Successfully created owner with email {email}, access code 00000000000000000000, and password {pw} Make sure to change this password and assign a real card ASAP!" + ) + + def migrate_card(self, old_ac: str, new_ac: str, should_force: bool) -> None: + if old_ac == new_ac: + self.logger.error("Both access codes are the same!") + return + + new_card = self.card.get_card_by_access_code(new_ac) + if new_card is None: + self.card.update_access_code(old_ac, new_ac) + return + + if not should_force: + self.logger.warning( + f"Card already exists for access code {new_ac} (id {new_card['id']}). If you wish to continue, rerun with the '--force' flag." + f" All exiting data on the target card {new_ac} will be perminently erased and replaced with data from card {old_ac}." + ) + return + + self.logger.info( + f"All exiting data on the target card {new_ac} will be perminently erased and replaced with data from card {old_ac}." + ) + self.card.delete_card(new_card["id"]) + self.card.update_access_code(old_ac, new_ac) + + hanging_user = self.user.get_user(new_card["user"]) + if hanging_user["password"] is None: + self.logger.info(f"Delete hanging user {hanging_user['id']}") + self.user.delete_user(hanging_user["id"]) + + def delete_hanging_users(self) -> None: + """ + Finds and deletes users that have not registered for the webui that have no cards assocated with them. + """ + unreg_users = self.user.get_unregistered_users() + if unreg_users is None: + self.logger.error("Error occoured finding unregistered users") + + for user in unreg_users: + cards = self.card.get_user_cards(user["id"]) + if cards is None: + self.logger.error(f"Error getting cards for user {user['id']}") + continue + + if not cards: + self.logger.info(f"Delete hanging user {user['id']}") + self.user.delete_user(user["id"]) + + def autoupgrade(self) -> None: + all_game_versions = self.base.get_all_schema_vers() + if all_game_versions is None: + self.logger.warning("Failed to get schema versions") + return + + all_games = Utils.get_all_titles() + all_games_list: Dict[str, int] = {} + for _, mod in all_games.items(): + if hasattr(mod, "current_schema_version"): + all_games_list[mod.game_codes[0]] = mod.current_schema_version + + for x in all_game_versions: + failed = False + game = x["game"].upper() + update_ver = int(x["version"]) + latest_ver = all_games_list.get(game, 1) + if game == "CORE": + latest_ver = self.current_schema_version + + if update_ver == latest_ver: + self.logger.info(f"{game} is already latest version") + continue + + for y in range(update_ver + 1, latest_ver + 1): + if os.path.exists(f"core/data/schema/versions/{game}_{y}_upgrade.sql"): + with open( + f"core/data/schema/versions/{game}_{y}_upgrade.sql", + "r", + encoding="utf-8", + ) as f: + sql = f.read() + + result = self.base.execute(sql) + if result is None: + self.logger.error( + f"Error execuing sql script for game {game} v{y}!" + ) + failed = True + break + else: + self.logger.warning(f"Could not find script {game}_{y}_upgrade.sql") + failed = True + + if not failed: + self.base.set_schema_ver(latest_ver, game) + + def show_versions(self) -> None: + all_game_versions = self.base.get_all_schema_vers() + for ver in all_game_versions: + self.logger.info(f"{ver['game']} -> v{ver['version']}") diff --git a/core/data/schema/__init__.py b/core/data/schema/__init__.py index 9032a68..45931d7 100644 --- a/core/data/schema/__init__.py +++ b/core/data/schema/__init__.py @@ -3,4 +3,4 @@ from core.data.schema.card import CardData from core.data.schema.base import BaseData, metadata from core.data.schema.arcade import ArcadeData -__all__ = ["UserData", "CardData", "BaseData", "metadata", "ArcadeData"] \ No newline at end of file +__all__ = ["UserData", "CardData", "BaseData", "metadata", "ArcadeData"] diff --git a/core/data/schema/arcade.py b/core/data/schema/arcade.py index 117c3fe..2fb8e43 100644 --- a/core/data/schema/arcade.py +++ b/core/data/schema/arcade.py @@ -1,113 +1,232 @@ -from typing import Optional, Dict -from sqlalchemy import Table, Column +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, and_, or_ from sqlalchemy.sql.schema import ForeignKey, PrimaryKeyConstraint -from sqlalchemy.types import Integer, String, Boolean +from sqlalchemy.types import Integer, String, Boolean, JSON from sqlalchemy.sql import func, select from sqlalchemy.dialects.mysql import insert +from sqlalchemy.engine import Row +import re from core.data.schema.base import BaseData, metadata +from core.const import * arcade = Table( "arcade", metadata, Column("id", Integer, primary_key=True, nullable=False), Column("name", String(255)), - Column("nickname", String(255)), + Column("nickname", String(255)), Column("country", String(3)), Column("country_id", Integer), Column("state", String(255)), Column("city", String(255)), Column("region_id", Integer), Column("timezone", String(255)), - mysql_charset='utf8mb4' + Column("ip", String(39)), + mysql_charset="utf8mb4", ) machine = Table( "machine", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("arcade", ForeignKey("arcade.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "arcade", + ForeignKey("arcade.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("serial", String(15), nullable=False), Column("board", String(15)), Column("game", String(4)), - Column("country", String(3)), # overwrites if not null + Column("country", String(3)), # overwrites if not null Column("timezone", String(255)), Column("ota_enable", Boolean), + Column("memo", String(255)), Column("is_cab", Boolean), - mysql_charset='utf8mb4' + Column("data", JSON), + mysql_charset="utf8mb4", ) arcade_owner = Table( - 'arcade_owner', + "arcade_owner", metadata, - Column('user', Integer, ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column('arcade', Integer, ForeignKey("arcade.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column('permissions', Integer, nullable=False), - PrimaryKeyConstraint('user', 'arcade', name='arcade_owner_pk'), - mysql_charset='utf8mb4' + Column( + "user", + Integer, + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column( + "arcade", + Integer, + ForeignKey("arcade.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("permissions", Integer, nullable=False), + PrimaryKeyConstraint("user", "arcade", name="arcade_owner_pk"), + mysql_charset="utf8mb4", ) + class ArcadeData(BaseData): - def get_machine(self, serial: str = None, id: int = None) -> Optional[Dict]: + def get_machine(self, serial: str = None, id: int = None) -> Optional[Row]: if serial is not None: - sql = machine.select(machine.c.serial == serial) + serial = serial.replace("-", "") + if len(serial) == 11: + sql = machine.select(machine.c.serial.like(f"{serial}%")) + + elif len(serial) == 15: + sql = machine.select(machine.c.serial == serial) + + else: + self.logger.error(f"{__name__ }: Malformed serial {serial}") + return None + elif id is not None: sql = machine.select(machine.c.id == id) - else: + + else: self.logger.error(f"{__name__ }: Need either serial or ID to look up!") return None - + result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() - - def put_machine(self, arcade_id: int, serial: str = None, board: str = None, game: str = None, is_cab: bool = False) -> Optional[int]: + + def put_machine( + self, + arcade_id: int, + serial: str = "", + board: str = None, + game: str = None, + is_cab: bool = False, + ) -> Optional[int]: if arcade_id: self.logger.error(f"{__name__ }: Need arcade id!") return None - if serial is None: - pass - - sql = machine.insert().values(arcade = arcade_id, keychip = serial, board = board, game = game, is_cab = is_cab) - - result = self.execute(sql) - if result is None: return None - return result.lastrowid - - def get_arcade(self, id: int) -> Optional[Dict]: - sql = arcade.select(arcade.c.id == id) - result = self.execute(sql) - if result is None: return None - return result.fetchone() - - def put_arcade(self, name: str, nickname: str = None, country: str = "JPN", country_id: int = 1, - state: str = "", city: str = "", regional_id: int = 1) -> Optional[int]: - if nickname is None: nickname = name - - sql = arcade.insert().values(name = name, nickname = nickname, country = country, country_id = country_id, - state = state, city = city, regional_id = regional_id) - - result = self.execute(sql) - if result is None: return None - return result.lastrowid - - def get_arcade_owners(self, arcade_id: int) -> Optional[Dict]: - sql = select(arcade_owner).where(arcade_owner.c.arcade==arcade_id) - - result = self.execute(sql) - if result is None: return None - return result.fetchall() - - def add_arcade_owner(self, arcade_id: int, user_id: int) -> None: - sql = insert(arcade_owner).values( - arcade=arcade_id, - user=user_id + sql = machine.insert().values( + arcade=arcade_id, keychip=serial, board=board, game=game, is_cab=is_cab ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.lastrowid - def generate_keychip_serial(self, platform_id: int) -> str: - pass \ No newline at end of file + def set_machine_serial(self, machine_id: int, serial: str) -> None: + result = self.execute( + machine.update(machine.c.id == machine_id).values(keychip=serial) + ) + if result is None: + self.logger.error( + f"Failed to update serial for machine {machine_id} -> {serial}" + ) + return result.lastrowid + + def set_machine_boardid(self, machine_id: int, boardid: str) -> None: + result = self.execute( + machine.update(machine.c.id == machine_id).values(board=boardid) + ) + if result is None: + self.logger.error( + f"Failed to update board id for machine {machine_id} -> {boardid}" + ) + + def get_arcade(self, id: int) -> Optional[Row]: + sql = arcade.select(arcade.c.id == id) + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_arcade_machines(self, id: int) -> Optional[List[Row]]: + sql = machine.select(machine.c.arcade == id) + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def put_arcade( + self, + name: str, + nickname: str = None, + country: str = "JPN", + country_id: int = 1, + state: str = "", + city: str = "", + regional_id: int = 1, + ) -> Optional[int]: + if nickname is None: + nickname = name + + sql = arcade.insert().values( + name=name, + nickname=nickname, + country=country, + country_id=country_id, + state=state, + city=city, + regional_id=regional_id, + ) + + result = self.execute(sql) + if result is None: + return None + return result.lastrowid + + def get_arcades_managed_by_user(self, user_id: int) -> Optional[List[Row]]: + sql = select(arcade).join(arcade_owner, arcade_owner.c.arcade == arcade.c.id).where(arcade_owner.c.user == user_id) + result = self.execute(sql) + if result is None: + return False + return result.fetchall() + + def get_manager_permissions(self, user_id: int, arcade_id: int) -> Optional[int]: + sql = select(arcade_owner.c.permissions).where(and_(arcade_owner.c.user == user_id, arcade_owner.c.arcade == arcade_id)) + result = self.execute(sql) + if result is None: + return False + return result.fetchone() + + def get_arcade_owners(self, arcade_id: int) -> Optional[Row]: + sql = select(arcade_owner).where(arcade_owner.c.arcade == arcade_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def add_arcade_owner(self, arcade_id: int, user_id: int) -> None: + sql = insert(arcade_owner).values(arcade=arcade_id, user=user_id) + + result = self.execute(sql) + if result is None: + return None + return result.lastrowid + + def format_serial( + self, platform_code: str, platform_rev: int, serial_num: int, append: int = 4152 + ) -> str: + return f"{platform_code}{platform_rev:02d}A{serial_num:04d}{append:04d}" # 0x41 = A, 0x52 = R + + def validate_keychip_format(self, serial: str) -> bool: + if re.fullmatch(r"^A[0-9]{2}[E|X][-]?[0-9]{2}[A-HJ-NP-Z][0-9]{4}([0-9]{4})?$", serial) is None: + return False + + return True + + def get_arcade_by_name(self, name: str) -> Optional[List[Row]]: + sql = arcade.select(or_(arcade.c.name.like(f"%{name}%"), arcade.c.nickname.like(f"%{name}%"))) + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_arcades_by_ip(self, ip: str) -> Optional[List[Row]]: + sql = arcade.select().where(arcade.c.ip == ip) + result = self.execute(sql) + if result is None: + return None + return result.fetchall() diff --git a/core/data/schema/base.py b/core/data/schema/base.py index 78f3ab4..ef980e5 100644 --- a/core/data/schema/base.py +++ b/core/data/schema/base.py @@ -2,6 +2,7 @@ import json import logging from random import randrange from typing import Any, Optional, Dict, List +from sqlalchemy.engine import Row from sqlalchemy.engine.cursor import CursorResult from sqlalchemy.engine.base import Connection from sqlalchemy.sql import text, func, select @@ -19,7 +20,7 @@ schema_ver = Table( metadata, Column("game", String(4), primary_key=True, nullable=False), Column("version", Integer, nullable=False, server_default="1"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) event_log = Table( @@ -29,96 +30,138 @@ event_log = Table( Column("system", String(255), nullable=False), Column("type", String(255), nullable=False), Column("severity", Integer, nullable=False), + Column("message", String(1000), nullable=False), Column("details", JSON, nullable=False), Column("when_logged", TIMESTAMP, nullable=False, server_default=func.now()), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) -class BaseData(): + +class BaseData: def __init__(self, cfg: CoreConfig, conn: Connection) -> None: self.config = cfg self.conn = conn self.logger = logging.getLogger("database") - - def execute(self, sql: str, opts: Dict[str, Any]={}) -> Optional[CursorResult]: + + def execute(self, sql: str, opts: Dict[str, Any] = {}) -> Optional[CursorResult]: res = None try: - self.logger.info(f"SQL Execute: {''.join(str(sql).splitlines())} || {opts}") + self.logger.info(f"SQL Execute: {''.join(str(sql).splitlines())}") res = self.conn.execute(text(sql), opts) except SQLAlchemyError as e: self.logger.error(f"SQLAlchemy error {e}") return None - + except UnicodeEncodeError as e: self.logger.error(f"UnicodeEncodeError error {e}") return None - except: + except Exception: try: res = self.conn.execute(sql, opts) except SQLAlchemyError as e: self.logger.error(f"SQLAlchemy error {e}") return None - + except UnicodeEncodeError as e: self.logger.error(f"UnicodeEncodeError error {e}") return None - except: + except Exception: self.logger.error(f"Unknown error") raise return res - + def generate_id(self) -> int: """ Generate a random 5-7 digit id """ return randrange(10000, 9999999) - + + def get_all_schema_vers(self) -> Optional[List[Row]]: + sql = select(schema_ver) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + def get_schema_ver(self, game: str) -> Optional[int]: sql = select(schema_ver).where(schema_ver.c.game == game) result = self.execute(sql) if result is None: return None - return result.fetchone()["version"] + + row = result.fetchone() + if row is None: + return None + + return row["version"] - def set_schema_ver(self, ver: int, game: str = "CORE") -> Optional[int]: - sql = insert(schema_ver).values(game = game, version = ver) - conflict = sql.on_duplicate_key_update(version = ver) - + def touch_schema_ver(self, ver: int, game: str = "CORE") -> Optional[int]: + sql = insert(schema_ver).values(game=game, version=ver) + conflict = sql.on_duplicate_key_update(version=schema_ver.c.version) + result = self.execute(conflict) if result is None: - self.logger.error(f"Failed to update schema version for game {game} (v{ver})") + self.logger.error( + f"Failed to update schema version for game {game} (v{ver})" + ) return None return result.lastrowid - def log_event(self, system: str, type: str, severity: int, details: Dict) -> Optional[int]: - sql = event_log.insert().values(system = system, type = type, severity = severity, details = json.dumps(details)) + def set_schema_ver(self, ver: int, game: str = "CORE") -> Optional[int]: + sql = insert(schema_ver).values(game=game, version=ver) + conflict = sql.on_duplicate_key_update(version=ver) + + result = self.execute(conflict) + if result is None: + self.logger.error( + f"Failed to update schema version for game {game} (v{ver})" + ) + return None + return result.lastrowid + + def log_event( + self, system: str, type: str, severity: int, message: str, details: Dict = {} + ) -> Optional[int]: + sql = event_log.insert().values( + system=system, + type=type, + severity=severity, + message=message, + details=json.dumps(details), + ) result = self.execute(sql) if result is None: - self.logger.error(f"{__name__}: Failed to insert event into event log! system = {system}, type = {type}, severity = {severity}, details = {details}") + self.logger.error( + f"{__name__}: Failed to insert event into event log! system = {system}, type = {type}, severity = {severity}, message = {message}" + ) return None return result.lastrowid - + def get_event_log(self, entries: int = 100) -> Optional[List[Dict]]: sql = event_log.select().limit(entries).all() result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - + def fix_bools(self, data: Dict) -> Dict: - for k,v in data.items(): + for k, v in data.items(): + if k == "userName" or k == "teamName": + continue if type(v) == str and v.lower() == "true": data[k] = True elif type(v) == str and v.lower() == "false": data[k] = False - + return data diff --git a/core/data/schema/card.py b/core/data/schema/card.py index 7c0c945..a95684e 100644 --- a/core/data/schema/card.py +++ b/core/data/schema/card.py @@ -3,55 +3,113 @@ from sqlalchemy import Table, Column, UniqueConstraint from sqlalchemy.types import Integer, String, Boolean, TIMESTAMP from sqlalchemy.sql.schema import ForeignKey from sqlalchemy.sql import func +from sqlalchemy.engine import Row from core.data.schema.base import BaseData, metadata aime_card = Table( - 'aime_card', + "aime_card", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("access_code", String(20)), Column("created_date", TIMESTAMP, server_default=func.now()), Column("last_login_date", TIMESTAMP, onupdate=func.now()), Column("is_locked", Boolean, server_default="0"), Column("is_banned", Boolean, server_default="0"), UniqueConstraint("user", "access_code", name="aime_card_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) + class CardData(BaseData): + def get_card_by_access_code(self, access_code: str) -> Optional[Row]: + sql = aime_card.select(aime_card.c.access_code == access_code) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_card_by_id(self, card_id: int) -> Optional[Row]: + sql = aime_card.select(aime_card.c.id == card_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def update_access_code(self, old_ac: str, new_ac: str) -> None: + sql = aime_card.update(aime_card.c.access_code == old_ac).values( + access_code=new_ac + ) + + result = self.execute(sql) + if result is None: + self.logger.error( + f"Failed to change card access code from {old_ac} to {new_ac}" + ) + def get_user_id_from_card(self, access_code: str) -> Optional[int]: """ Given a 20 digit access code as a string, get the user id associated with that card """ - sql = aime_card.select(aime_card.c.access_code == access_code) - - result = self.execute(sql) - if result is None: return None - - card = result.fetchone() - if card is None: return None + card = self.get_card_by_access_code(access_code) + if card is None: + return None return int(card["user"]) - def get_user_cards(self, aime_id: int) -> Optional[List[Dict]]: + def get_card_banned(self, access_code: str) -> Optional[bool]: + """ + Given a 20 digit access code as a string, check if the card is banned + """ + card = self.get_card_by_access_code(access_code) + if card is None: + return None + if card["is_banned"]: + return True + return False + def get_card_locked(self, access_code: str) -> Optional[bool]: + """ + Given a 20 digit access code as a string, check if the card is locked + """ + card = self.get_card_by_access_code(access_code) + if card is None: + return None + if card["is_locked"]: + return True + return False + + def delete_card(self, card_id: int) -> None: + sql = aime_card.delete(aime_card.c.id == card_id) + + result = self.execute(sql) + if result is None: + self.logger.error(f"Failed to delete card with id {card_id}") + + def get_user_cards(self, aime_id: int) -> Optional[List[Row]]: """ Returns all cards owned by a user """ sql = aime_card.select(aime_card.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - def create_card(self, user_id: int, access_code: str) -> Optional[int]: """ Given a aime_user id and a 20 digit access code as a string, create a card and return the ID if successful """ sql = aime_card.insert().values(user=user_id, access_code=access_code) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.lastrowid def to_access_code(self, luid: str) -> str: @@ -64,4 +122,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}' \ No newline at end of file + return f"{int(access_code):0{16}x}" diff --git a/core/data/schema/user.py b/core/data/schema/user.py index 7d76bbe..221ba81 100644 --- a/core/data/schema/user.py +++ b/core/data/schema/user.py @@ -1,9 +1,12 @@ from enum import Enum -from typing import Dict, Optional +from typing import Optional, List from sqlalchemy import Table, Column from sqlalchemy.types import Integer, String, TIMESTAMP -from sqlalchemy.sql.schema import ForeignKey from sqlalchemy.sql import func +from sqlalchemy.dialects.mysql import insert +from sqlalchemy.sql import func, select +from sqlalchemy.engine import Row +import bcrypt from core.data.schema.base import BaseData, metadata @@ -14,44 +17,107 @@ aime_user = Table( Column("username", String(25), unique=True), Column("email", String(255), unique=True), Column("password", String(255)), - Column("permissions", Integer), + Column("permissions", Integer), Column("created_date", TIMESTAMP, server_default=func.now()), Column("last_login_date", TIMESTAMP, onupdate=func.now()), Column("suspend_expire_time", TIMESTAMP), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) -frontend_session = Table( - "frontend_session", - metadata, - Column("id", Integer, primary_key=True, unique=True), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column('session_cookie', String(32), nullable=False, unique=True), - Column("expires", TIMESTAMP, nullable=False), - mysql_charset='utf8mb4' -) class PermissionBits(Enum): PermUser = 1 PermMod = 2 PermSysAdmin = 4 + class UserData(BaseData): - def create_user(self, username: str = None, email: str = None, password: str = None) -> Optional[int]: - - if email is None: - permission = None + def create_user( + self, + id: int = None, + username: str = None, + email: str = None, + password: str = None, + permission: int = 1, + ) -> Optional[int]: + if id is None: + sql = insert(aime_user).values( + username=username, + email=email, + password=password, + permissions=permission, + ) else: - permission = 0 + sql = insert(aime_user).values( + id=id, + username=username, + email=email, + password=password, + permissions=permission, + ) - sql = aime_user.insert().values(username=username, email=email, password=password, permissions=permission) - - result = self.execute(sql) - if result is None: return None + conflict = sql.on_duplicate_key_update( + username=username, email=email, password=password, permissions=permission + ) + + result = self.execute(conflict) + if result is None: + return None return result.lastrowid - + + def get_user(self, user_id: int) -> Optional[Row]: + sql = select(aime_user).where(aime_user.c.id == user_id) + result = self.execute(sql) + if result is None: + return False + return result.fetchone() + + def check_password(self, user_id: int, passwd: bytes = None) -> bool: + usr = self.get_user(user_id) + if usr is None: + return False + + if usr["password"] is None: + return False + + if passwd is None or not passwd: + return False + + return bcrypt.checkpw(passwd, usr["password"].encode()) + def reset_autoincrement(self, ai_value: int) -> None: - # Didn't feel like learning how to do this the right way - # if somebody wants a free PR go nuts I guess + # ALTER TABLE isn't in sqlalchemy so we do this the ugly way sql = f"ALTER TABLE aime_user AUTO_INCREMENT={ai_value}" - self.execute(sql) \ No newline at end of file + self.execute(sql) + + def delete_user(self, user_id: int) -> None: + sql = aime_user.delete(aime_user.c.id == user_id) + + result = self.execute(sql) + if result is None: + self.logger.error(f"Failed to delete user with id {user_id}") + + def get_unregistered_users(self) -> List[Row]: + """ + Returns a list of users who have not registered with the webui. They may or may not have cards. + """ + sql = select(aime_user).where(aime_user.c.password == None) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def find_user_by_email(self, email: str) -> Row: + sql = select(aime_user).where(aime_user.c.email == email) + result = self.execute(sql) + if result is None: + return False + return result.fetchone() + + def find_user_by_username(self, username: str) -> List[Row]: + sql = aime_user.select(aime_user.c.username.like(f"%{username}%")) + result = self.execute(sql) + if result is None: + return False + return result.fetchall() diff --git a/core/data/schema/versions/CORE_1_rollback.sql b/core/data/schema/versions/CORE_1_rollback.sql new file mode 100644 index 0000000..8a1144b --- /dev/null +++ b/core/data/schema/versions/CORE_1_rollback.sql @@ -0,0 +1,2 @@ +ALTER TABLE `frontend_session` +DROP COLUMN `ip`; \ No newline at end of file diff --git a/core/data/schema/versions/CORE_2_rollback.sql b/core/data/schema/versions/CORE_2_rollback.sql new file mode 100644 index 0000000..8944df0 --- /dev/null +++ b/core/data/schema/versions/CORE_2_rollback.sql @@ -0,0 +1 @@ +ALTER TABLE `event_log` DROP COLUMN `message`; \ No newline at end of file diff --git a/core/data/schema/versions/CORE_2_upgrade.sql b/core/data/schema/versions/CORE_2_upgrade.sql new file mode 100644 index 0000000..44deb6d --- /dev/null +++ b/core/data/schema/versions/CORE_2_upgrade.sql @@ -0,0 +1,2 @@ +ALTER TABLE `frontend_session` +ADD `ip` CHAR(15); \ No newline at end of file diff --git a/core/data/schema/versions/CORE_3_rollback.sql b/core/data/schema/versions/CORE_3_rollback.sql new file mode 100644 index 0000000..9132cc3 --- /dev/null +++ b/core/data/schema/versions/CORE_3_rollback.sql @@ -0,0 +1,12 @@ +CREATE TABLE `frontend_session` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `user` int(11) NOT NULL, + `ip` varchar(15) DEFAULT NULL, + `session_cookie` varchar(32) NOT NULL, + `expires` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), + PRIMARY KEY (`id`), + UNIQUE KEY `id` (`id`), + UNIQUE KEY `session_cookie` (`session_cookie`), + KEY `user` (`user`), + CONSTRAINT `frontend_session_ibfk_1` FOREIGN KEY (`user`) REFERENCES `aime_user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4; \ No newline at end of file diff --git a/core/data/schema/versions/CORE_3_upgrade.sql b/core/data/schema/versions/CORE_3_upgrade.sql new file mode 100644 index 0000000..cc0e8c6 --- /dev/null +++ b/core/data/schema/versions/CORE_3_upgrade.sql @@ -0,0 +1 @@ +ALTER TABLE `event_log` ADD COLUMN `message` VARCHAR(1000) NOT NULL AFTER `severity`; \ No newline at end of file diff --git a/core/data/schema/versions/CORE_4_rollback.sql b/core/data/schema/versions/CORE_4_rollback.sql new file mode 100644 index 0000000..4464915 --- /dev/null +++ b/core/data/schema/versions/CORE_4_rollback.sql @@ -0,0 +1,3 @@ +ALTER TABLE machine DROP COLUMN memo; +ALTER TABLE machine DROP COLUMN is_blacklisted; +ALTER TABLE machine DROP COLUMN `data`; diff --git a/core/data/schema/versions/CORE_4_upgrade.sql b/core/data/schema/versions/CORE_4_upgrade.sql new file mode 100644 index 0000000..6a04c74 --- /dev/null +++ b/core/data/schema/versions/CORE_4_upgrade.sql @@ -0,0 +1 @@ +DROP TABLE `frontend_session`; \ No newline at end of file diff --git a/core/data/schema/versions/CORE_5_rollback.sql b/core/data/schema/versions/CORE_5_rollback.sql new file mode 100644 index 0000000..2462fee --- /dev/null +++ b/core/data/schema/versions/CORE_5_rollback.sql @@ -0,0 +1 @@ +ALTER TABLE arcade DROP COLUMN 'ip'; \ No newline at end of file diff --git a/core/data/schema/versions/CORE_5_upgrade.sql b/core/data/schema/versions/CORE_5_upgrade.sql new file mode 100644 index 0000000..8e88b00 --- /dev/null +++ b/core/data/schema/versions/CORE_5_upgrade.sql @@ -0,0 +1,3 @@ +ALTER TABLE machine ADD memo varchar(255) NULL; +ALTER TABLE machine ADD is_blacklisted tinyint(1) NULL; +ALTER TABLE machine ADD `data` longtext NULL; diff --git a/core/data/schema/versions/CORE_6_upgrade.sql b/core/data/schema/versions/CORE_6_upgrade.sql new file mode 100644 index 0000000..013728c --- /dev/null +++ b/core/data/schema/versions/CORE_6_upgrade.sql @@ -0,0 +1 @@ +ALTER TABLE arcade ADD ip varchar(39) NULL; \ No newline at end of file diff --git a/core/data/schema/versions/SBZV_1_rollback.sql b/core/data/schema/versions/SBZV_1_rollback.sql new file mode 100644 index 0000000..6389157 --- /dev/null +++ b/core/data/schema/versions/SBZV_1_rollback.sql @@ -0,0 +1,9 @@ +SET FOREIGN_KEY_CHECKS=0; +ALTER TABLE diva_score DROP FOREIGN KEY diva_score_ibfk_1; +ALTER TABLE diva_score DROP CONSTRAINT diva_score_uk; +ALTER TABLE diva_score ADD CONSTRAINT diva_score_uk UNIQUE (user, pv_id, difficulty); +ALTER TABLE diva_score ADD CONSTRAINT diva_score_ibfk_1 FOREIGN KEY (user) REFERENCES aime_user(id) ON DELETE CASCADE; + +ALTER TABLE diva_score DROP COLUMN edition; +ALTER TABLE diva_playlog DROP COLUMN edition; +SET FOREIGN_KEY_CHECKS=1; \ No newline at end of file diff --git a/core/data/schema/versions/SBZV_2_rollback.sql b/core/data/schema/versions/SBZV_2_rollback.sql new file mode 100644 index 0000000..5bd46c0 --- /dev/null +++ b/core/data/schema/versions/SBZV_2_rollback.sql @@ -0,0 +1,17 @@ +ALTER TABLE diva_profile_shop DROP COLUMN c_itm_eqp_ary; +ALTER TABLE diva_profile_shop DROP COLUMN ms_itm_flg_ary; + +ALTER TABLE diva_profile DROP COLUMN use_pv_mdl_eqp; +ALTER TABLE diva_profile DROP COLUMN use_mdl_pri; +ALTER TABLE diva_profile DROP COLUMN use_pv_skn_eqp; +ALTER TABLE diva_profile DROP COLUMN use_pv_btn_se_eqp; +ALTER TABLE diva_profile DROP COLUMN use_pv_sld_se_eqp; +ALTER TABLE diva_profile DROP COLUMN use_pv_chn_sld_se_eqp; +ALTER TABLE diva_profile DROP COLUMN use_pv_sldr_tch_se_eqp; +ALTER TABLE diva_profile ADD COLUMN use_pv_mdl_eqp VARCHAR(8) NOT NULL DEFAULT "true" AFTER sort_kind; +ALTER TABLE diva_profile ADD COLUMN use_pv_btn_se_eqp VARCHAR(8) NOT NULL DEFAULT "true" AFTER use_pv_mdl_eqp; +ALTER TABLE diva_profile ADD COLUMN use_pv_sld_se_eqp VARCHAR(8) NOT NULL DEFAULT "false" AFTER use_pv_btn_se_eqp; +ALTER TABLE diva_profile ADD COLUMN use_pv_chn_sld_se_eqp VARCHAR(8) NOT NULL DEFAULT "false" AFTER use_pv_sld_se_eqp; +ALTER TABLE diva_profile ADD COLUMN use_pv_sldr_tch_se_eqp VARCHAR(8) NOT NULL DEFAULT "false" AFTER use_pv_chn_sld_se_eqp; + +DROP TABLE IF EXISTS `diva_profile_pv_customize`; \ No newline at end of file diff --git a/core/data/schema/versions/SBZV_2_upgrade.sql b/core/data/schema/versions/SBZV_2_upgrade.sql new file mode 100644 index 0000000..26cb2ed --- /dev/null +++ b/core/data/schema/versions/SBZV_2_upgrade.sql @@ -0,0 +1,9 @@ +SET FOREIGN_KEY_CHECKS=0; +ALTER TABLE diva_score ADD COLUMN edition int(11) DEFAULT 0 AFTER difficulty; +ALTER TABLE diva_playlog ADD COLUMN edition int(11) DEFAULT 0 AFTER difficulty; + +ALTER TABLE diva_score DROP FOREIGN KEY diva_score_ibfk_1; +ALTER TABLE diva_score DROP CONSTRAINT diva_score_uk; +ALTER TABLE diva_score ADD CONSTRAINT diva_score_uk UNIQUE (user, pv_id, difficulty, edition); +ALTER TABLE diva_score ADD CONSTRAINT diva_score_ibfk_1 FOREIGN KEY (user) REFERENCES aime_user(id) ON DELETE CASCADE; +SET FOREIGN_KEY_CHECKS=1; \ No newline at end of file diff --git a/core/data/schema/versions/SBZV_3_rollback.sql b/core/data/schema/versions/SBZV_3_rollback.sql new file mode 100644 index 0000000..18c2448 --- /dev/null +++ b/core/data/schema/versions/SBZV_3_rollback.sql @@ -0,0 +1,3 @@ +ALTER TABLE diva_profile DROP COLUMN passwd_stat; +ALTER TABLE diva_profile DROP COLUMN passwd; +ALTER TABLE diva_profile MODIFY player_name VARCHAR(8); diff --git a/core/data/schema/versions/SBZV_3_upgrade.sql b/core/data/schema/versions/SBZV_3_upgrade.sql new file mode 100644 index 0000000..a06d7f4 --- /dev/null +++ b/core/data/schema/versions/SBZV_3_upgrade.sql @@ -0,0 +1,33 @@ +ALTER TABLE diva_profile_shop ADD COLUMN c_itm_eqp_ary varchar(59) DEFAULT "-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999"; +ALTER TABLE diva_profile_shop ADD COLUMN ms_itm_flg_ary varchar(59) DEFAULT "-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1"; + +ALTER TABLE diva_profile DROP COLUMN use_pv_mdl_eqp; +ALTER TABLE diva_profile DROP COLUMN use_pv_btn_se_eqp; +ALTER TABLE diva_profile DROP COLUMN use_pv_sld_se_eqp; +ALTER TABLE diva_profile DROP COLUMN use_pv_chn_sld_se_eqp; +ALTER TABLE diva_profile DROP COLUMN use_pv_sldr_tch_se_eqp; +ALTER TABLE diva_profile ADD COLUMN use_pv_mdl_eqp BOOLEAN NOT NULL DEFAULT true AFTER sort_kind; +ALTER TABLE diva_profile ADD COLUMN use_mdl_pri BOOLEAN NOT NULL DEFAULT false AFTER use_pv_mdl_eqp; +ALTER TABLE diva_profile ADD COLUMN use_pv_skn_eqp BOOLEAN NOT NULL DEFAULT false AFTER use_mdl_pri; +ALTER TABLE diva_profile ADD COLUMN use_pv_btn_se_eqp BOOLEAN NOT NULL DEFAULT true AFTER use_pv_skn_eqp; +ALTER TABLE diva_profile ADD COLUMN use_pv_sld_se_eqp BOOLEAN NOT NULL DEFAULT false AFTER use_pv_btn_se_eqp; +ALTER TABLE diva_profile ADD COLUMN use_pv_chn_sld_se_eqp BOOLEAN NOT NULL DEFAULT false AFTER use_pv_sld_se_eqp; +ALTER TABLE diva_profile ADD COLUMN use_pv_sldr_tch_se_eqp BOOLEAN NOT NULL DEFAULT false AFTER use_pv_chn_sld_se_eqp; + + +CREATE TABLE diva_profile_pv_customize ( + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, + user INT NOT NULL, + version INT NOT NULL, + pv_id INT NOT NULL, + mdl_eqp_ary VARCHAR(14) DEFAULT '-999,-999,-999', + c_itm_eqp_ary VARCHAR(59) DEFAULT '-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999', + ms_itm_flg_ary VARCHAR(59) DEFAULT '-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1', + skin INT DEFAULT '-1', + btn_se INT DEFAULT '-1', + sld_se INT DEFAULT '-1', + chsld_se INT DEFAULT '-1', + sldtch_se INT DEFAULT '-1', + UNIQUE KEY diva_profile_pv_customize_uk (user, version, pv_id), + CONSTRAINT diva_profile_pv_customize_ibfk_1 FOREIGN KEY (user) REFERENCES aime_user (id) ON DELETE CASCADE ON UPDATE CASCADE +); diff --git a/core/data/schema/versions/SBZV_4_rollback.sql b/core/data/schema/versions/SBZV_4_rollback.sql new file mode 100644 index 0000000..f56327e --- /dev/null +++ b/core/data/schema/versions/SBZV_4_rollback.sql @@ -0,0 +1,9 @@ +ALTER TABLE diva_profile + DROP cnp_cid, + DROP cnp_val, + DROP cnp_rr, + DROP cnp_sp, + DROP btn_se_eqp, + DROP sld_se_eqp, + DROP chn_sld_se_eqp, + DROP sldr_tch_se_eqp; \ No newline at end of file diff --git a/core/data/schema/versions/SBZV_4_upgrade.sql b/core/data/schema/versions/SBZV_4_upgrade.sql new file mode 100644 index 0000000..c3a802c --- /dev/null +++ b/core/data/schema/versions/SBZV_4_upgrade.sql @@ -0,0 +1,3 @@ +ALTER TABLE diva_profile ADD COLUMN passwd_stat INTEGER NOT NULL DEFAULT 0; +ALTER TABLE diva_profile ADD COLUMN passwd VARCHAR(12) NOT NULL DEFAULT "**********"; +ALTER TABLE diva_profile MODIFY player_name VARCHAR(10); diff --git a/core/data/schema/versions/SBZV_5_rollback.sql b/core/data/schema/versions/SBZV_5_rollback.sql new file mode 100644 index 0000000..851d357 --- /dev/null +++ b/core/data/schema/versions/SBZV_5_rollback.sql @@ -0,0 +1,2 @@ +ALTER TABLE diva_profile + DROP skn_eqp; \ No newline at end of file diff --git a/core/data/schema/versions/SBZV_5_upgrade.sql b/core/data/schema/versions/SBZV_5_upgrade.sql new file mode 100644 index 0000000..7e29f7b --- /dev/null +++ b/core/data/schema/versions/SBZV_5_upgrade.sql @@ -0,0 +1,9 @@ +ALTER TABLE diva_profile + ADD cnp_cid INT NOT NULL DEFAULT -1, + ADD cnp_val INT NOT NULL DEFAULT -1, + ADD cnp_rr INT NOT NULL DEFAULT -1, + ADD cnp_sp VARCHAR(255) NOT NULL DEFAULT "", + ADD btn_se_eqp INT NOT NULL DEFAULT -1, + ADD sld_se_eqp INT NOT NULL DEFAULT -1, + ADD chn_sld_se_eqp INT NOT NULL DEFAULT -1, + ADD sldr_tch_se_eqp INT NOT NULL DEFAULT -1; \ No newline at end of file diff --git a/core/data/schema/versions/SBZV_6_upgrade.sql b/core/data/schema/versions/SBZV_6_upgrade.sql new file mode 100644 index 0000000..d417506 --- /dev/null +++ b/core/data/schema/versions/SBZV_6_upgrade.sql @@ -0,0 +1,2 @@ +ALTER TABLE diva_profile + ADD skn_eqp INT NOT NULL DEFAULT 0; \ No newline at end of file diff --git a/core/data/schema/versions/SDBT_1_rollback.sql b/core/data/schema/versions/SDBT_1_rollback.sql new file mode 100644 index 0000000..e8bca83 --- /dev/null +++ b/core/data/schema/versions/SDBT_1_rollback.sql @@ -0,0 +1 @@ +ALTER TABLE chuni_static_music CHANGE COLUMN worldsEndTag worldsEndTag VARCHAR(20) NULL DEFAULT NULL ; diff --git a/core/data/schema/versions/SDBT_2_rollback.sql b/core/data/schema/versions/SDBT_2_rollback.sql new file mode 100644 index 0000000..de11416 --- /dev/null +++ b/core/data/schema/versions/SDBT_2_rollback.sql @@ -0,0 +1 @@ +ALTER TABLE chuni_score_course DROP COLUMN theoryCount, DROP COLUMN orderId, DROP COLUMN playerRating; diff --git a/core/data/schema/versions/SDBT_2_upgrade.sql b/core/data/schema/versions/SDBT_2_upgrade.sql new file mode 100644 index 0000000..30f0fc4 --- /dev/null +++ b/core/data/schema/versions/SDBT_2_upgrade.sql @@ -0,0 +1 @@ +ALTER TABLE chuni_static_music CHANGE COLUMN worldsEndTag worldsEndTag VARCHAR(7) NULL DEFAULT NULL ; diff --git a/core/data/schema/versions/SDBT_3_rollback.sql b/core/data/schema/versions/SDBT_3_rollback.sql new file mode 100644 index 0000000..ff78c54 --- /dev/null +++ b/core/data/schema/versions/SDBT_3_rollback.sql @@ -0,0 +1,30 @@ +SET FOREIGN_KEY_CHECKS = 0; + +ALTER TABLE chuni_score_playlog + DROP COLUMN regionId, + DROP COLUMN machineType; + +ALTER TABLE chuni_static_events + DROP COLUMN startDate; + +ALTER TABLE chuni_profile_data + DROP COLUMN rankUpChallengeResults; + +ALTER TABLE chuni_static_login_bonus + DROP FOREIGN KEY chuni_static_login_bonus_ibfk_1; + +ALTER TABLE chuni_static_login_bonus_preset + DROP PRIMARY KEY; + +ALTER TABLE chuni_static_login_bonus_preset + CHANGE COLUMN presetId id INT NOT NULL; +ALTER TABLE chuni_static_login_bonus_preset + ADD PRIMARY KEY(id); +ALTER TABLE chuni_static_login_bonus_preset + ADD CONSTRAINT chuni_static_login_bonus_preset_uk UNIQUE(id, version); + +ALTER TABLE chuni_static_login_bonus + ADD CONSTRAINT chuni_static_login_bonus_ibfk_1 FOREIGN KEY(presetId) + REFERENCES chuni_static_login_bonus_preset(id) ON UPDATE CASCADE ON DELETE CASCADE; + +SET FOREIGN_KEY_CHECKS = 1; \ No newline at end of file diff --git a/core/data/schema/versions/SDBT_3_upgrade.sql b/core/data/schema/versions/SDBT_3_upgrade.sql new file mode 100644 index 0000000..57498cc --- /dev/null +++ b/core/data/schema/versions/SDBT_3_upgrade.sql @@ -0,0 +1 @@ +ALTER TABLE chuni_score_course ADD theoryCount int(11), ADD orderId int(11), ADD playerRating int(11); diff --git a/core/data/schema/versions/SDBT_4_rollback.sql b/core/data/schema/versions/SDBT_4_rollback.sql new file mode 100644 index 0000000..81366ea --- /dev/null +++ b/core/data/schema/versions/SDBT_4_rollback.sql @@ -0,0 +1,12 @@ +SET FOREIGN_KEY_CHECKS = 0; + +ALTER TABLE chuni_score_playlog + CHANGE COLUMN isClear isClear TINYINT(1) NULL DEFAULT NULL; + +ALTER TABLE chuni_score_best + CHANGE COLUMN isSuccess isSuccess TINYINT(1) NULL DEFAULT NULL ; + +ALTER TABLE chuni_score_playlog + DROP COLUMN ticketId; + +SET FOREIGN_KEY_CHECKS = 1; \ No newline at end of file diff --git a/core/data/schema/versions/SDBT_4_upgrade.sql b/core/data/schema/versions/SDBT_4_upgrade.sql new file mode 100644 index 0000000..984447e --- /dev/null +++ b/core/data/schema/versions/SDBT_4_upgrade.sql @@ -0,0 +1,29 @@ +SET FOREIGN_KEY_CHECKS = 0; + +ALTER TABLE chuni_score_playlog + ADD COLUMN regionId INT, + ADD COLUMN machineType INT; + +ALTER TABLE chuni_static_events + ADD COLUMN startDate TIMESTAMP NOT NULL DEFAULT current_timestamp(); + +ALTER TABLE chuni_profile_data + ADD COLUMN rankUpChallengeResults JSON; + +ALTER TABLE chuni_static_login_bonus + DROP FOREIGN KEY chuni_static_login_bonus_ibfk_1; + +ALTER TABLE chuni_static_login_bonus_preset + CHANGE COLUMN id presetId INT NOT NULL; +ALTER TABLE chuni_static_login_bonus_preset + DROP PRIMARY KEY; +ALTER TABLE chuni_static_login_bonus_preset + DROP INDEX chuni_static_login_bonus_preset_uk; +ALTER TABLE chuni_static_login_bonus_preset + ADD CONSTRAINT chuni_static_login_bonus_preset_pk PRIMARY KEY (presetId, version); + +ALTER TABLE chuni_static_login_bonus + ADD CONSTRAINT chuni_static_login_bonus_ibfk_1 FOREIGN KEY (presetId, version) + REFERENCES chuni_static_login_bonus_preset(presetId, version) ON UPDATE CASCADE ON DELETE CASCADE; + +SET FOREIGN_KEY_CHECKS = 1; \ No newline at end of file diff --git a/core/data/schema/versions/SDBT_5_upgrade.sql b/core/data/schema/versions/SDBT_5_upgrade.sql new file mode 100644 index 0000000..81a094a --- /dev/null +++ b/core/data/schema/versions/SDBT_5_upgrade.sql @@ -0,0 +1,12 @@ +SET FOREIGN_KEY_CHECKS = 0; + +ALTER TABLE chuni_score_playlog + CHANGE COLUMN isClear isClear TINYINT(6) NULL DEFAULT NULL; + +ALTER TABLE chuni_score_best + CHANGE COLUMN isSuccess isSuccess INT(11) NULL DEFAULT NULL ; + +ALTER TABLE chuni_score_playlog + ADD COLUMN ticketId INT(11) NULL AFTER machineType; + +SET FOREIGN_KEY_CHECKS = 1; \ No newline at end of file diff --git a/core/data/schema/versions/SDDT_2_rollback.sql b/core/data/schema/versions/SDDT_2_rollback.sql new file mode 100644 index 0000000..5ddf1b6 --- /dev/null +++ b/core/data/schema/versions/SDDT_2_rollback.sql @@ -0,0 +1,7 @@ +SET FOREIGN_KEY_CHECKS=0; +ALTER TABLE ongeki_profile_data DROP COLUMN isDialogWatchedSuggestMemory; +ALTER TABLE ongeki_score_best DROP COLUMN platinumScoreMax; +ALTER TABLE ongeki_score_playlog DROP COLUMN platinumScore; +ALTER TABLE ongeki_score_playlog DROP COLUMN platinumScoreMax; +DROP TABLE IF EXISTS `ongeki_user_memorychapter`; +SET FOREIGN_KEY_CHECKS=1; diff --git a/core/data/schema/versions/SDDT_3_rollback.sql b/core/data/schema/versions/SDDT_3_rollback.sql new file mode 100644 index 0000000..a248ce3 --- /dev/null +++ b/core/data/schema/versions/SDDT_3_rollback.sql @@ -0,0 +1 @@ +ALTER TABLE ongeki_profile_data DROP COLUMN lastEmoneyCredit; \ No newline at end of file diff --git a/core/data/schema/versions/SDDT_3_upgrade.sql b/core/data/schema/versions/SDDT_3_upgrade.sql new file mode 100644 index 0000000..26c5c30 --- /dev/null +++ b/core/data/schema/versions/SDDT_3_upgrade.sql @@ -0,0 +1,27 @@ +SET FOREIGN_KEY_CHECKS=0; + +ALTER TABLE ongeki_profile_data ADD COLUMN isDialogWatchedSuggestMemory BOOLEAN; +ALTER TABLE ongeki_score_best ADD COLUMN platinumScoreMax INTEGER; +ALTER TABLE ongeki_score_playlog ADD COLUMN platinumScore INTEGER; +ALTER TABLE ongeki_score_playlog ADD COLUMN platinumScoreMax INTEGER; + +CREATE TABLE ongeki_user_memorychapter ( + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, + user INT NOT NULL, + chapterId INT NOT NULL, + gaugeId INT NOT NULL, + gaugeNum INT NOT NULL, + jewelCount INT NOT NULL, + isStoryWatched BOOLEAN NOT NULL, + isBossWatched BOOLEAN NOT NULL, + isDialogWatched BOOLEAN NOT NULL, + isEndingWatched BOOLEAN NOT NULL, + isClear BOOLEAN NOT NULL, + lastPlayMusicId INT NOT NULL, + lastPlayMusicLevel INT NOT NULL, + lastPlayMusicCategory INT NOT NULL, + UNIQUE KEY ongeki_user_memorychapter_uk (user, chapterId), + CONSTRAINT ongeki_user_memorychapter_ibfk_1 FOREIGN KEY (user) REFERENCES aime_user (id) ON DELETE CASCADE ON UPDATE CASCADE +); + +SET FOREIGN_KEY_CHECKS=1; diff --git a/core/data/schema/versions/SDDT_4_rollback.sql b/core/data/schema/versions/SDDT_4_rollback.sql new file mode 100644 index 0000000..fdd369b --- /dev/null +++ b/core/data/schema/versions/SDDT_4_rollback.sql @@ -0,0 +1,2 @@ +ALTER TABLE ongeki_static_events +DROP COLUMN startDate; \ No newline at end of file diff --git a/core/data/schema/versions/SDDT_4_upgrade.sql b/core/data/schema/versions/SDDT_4_upgrade.sql new file mode 100644 index 0000000..45510c2 --- /dev/null +++ b/core/data/schema/versions/SDDT_4_upgrade.sql @@ -0,0 +1 @@ +ALTER TABLE ongeki_profile_data ADD COLUMN lastEmoneyCredit INTEGER DEFAULT 0; diff --git a/core/data/schema/versions/SDDT_5_rollback.sql b/core/data/schema/versions/SDDT_5_rollback.sql new file mode 100644 index 0000000..007716c --- /dev/null +++ b/core/data/schema/versions/SDDT_5_rollback.sql @@ -0,0 +1,22 @@ +SET FOREIGN_KEY_CHECKS=0; + +ALTER TABLE ongeki_user_event_point DROP COLUMN version; +ALTER TABLE ongeki_user_event_point DROP COLUMN rank; +ALTER TABLE ongeki_user_event_point DROP COLUMN type; +ALTER TABLE ongeki_user_event_point DROP COLUMN date; + +ALTER TABLE ongeki_user_tech_event DROP COLUMN version; + +ALTER TABLE ongeki_user_mission_point DROP COLUMN version; + +ALTER TABLE ongeki_static_events DROP COLUMN endDate; + +DROP TABLE ongeki_tech_event_ranking; +DROP TABLE ongeki_static_music_ranking_list; +DROP TABLE ongeki_static_rewards; +DROP TABLE ongeki_static_present_list; +DROP TABLE ongeki_static_tech_music; +DROP TABLE ongeki_static_client_testmode; +DROP TABLE ongeki_static_game_point; + +SET FOREIGN_KEY_CHECKS=1; diff --git a/core/data/schema/versions/SDDT_5_upgrade.sql b/core/data/schema/versions/SDDT_5_upgrade.sql new file mode 100644 index 0000000..6d4b421 --- /dev/null +++ b/core/data/schema/versions/SDDT_5_upgrade.sql @@ -0,0 +1,2 @@ +ALTER TABLE ongeki_static_events +ADD COLUMN startDate TIMESTAMP NOT NULL DEFAULT current_timestamp(); \ No newline at end of file diff --git a/core/data/schema/versions/SDDT_6_upgrade.sql b/core/data/schema/versions/SDDT_6_upgrade.sql new file mode 100644 index 0000000..82d5336 --- /dev/null +++ b/core/data/schema/versions/SDDT_6_upgrade.sql @@ -0,0 +1,98 @@ +SET FOREIGN_KEY_CHECKS=0; + +ALTER TABLE ongeki_user_event_point ADD COLUMN version INTEGER NOT NULL; +ALTER TABLE ongeki_user_event_point ADD COLUMN rank INTEGER; +ALTER TABLE ongeki_user_event_point ADD COLUMN type INTEGER NOT NULL; +ALTER TABLE ongeki_user_event_point ADD COLUMN date VARCHAR(25); + +ALTER TABLE ongeki_user_tech_event ADD COLUMN version INTEGER NOT NULL; + +ALTER TABLE ongeki_user_mission_point ADD COLUMN version INTEGER NOT NULL; + +ALTER TABLE ongeki_static_events ADD COLUMN endDate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP; + +CREATE TABLE ongeki_tech_event_ranking ( + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, + user INT NOT NULL, + version INT NOT NULL, + date VARCHAR(25), + eventId INT NOT NULL, + rank INT, + totalPlatinumScore INT NOT NULL, + totalTechScore INT NOT NULL, + UNIQUE KEY ongeki_tech_event_ranking_uk (user, eventId), + CONSTRAINT ongeki_tech_event_ranking_ibfk1 FOREIGN KEY (user) REFERENCES aime_user(id) ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE ongeki_static_music_ranking_list ( + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, + version INT NOT NULL, + musicId INT NOT NULL, + point INT NOT NULL, + userName VARCHAR(255), + UNIQUE KEY ongeki_static_music_ranking_list_uk (version, musicId) +); + +CREATE TABLE ongeki_static_rewards ( + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, + version INT NOT NULL, + rewardId INT NOT NULL, + rewardName VARCHAR(255) NOT NULL, + itemKind INT NOT NULL, + itemId INT NOT NULL, + UNIQUE KEY ongeki_tech_event_ranking_uk (version, rewardId) +); + +CREATE TABLE ongeki_static_present_list ( + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, + version INT NOT NULL, + presentId INT NOT NULL, + presentName VARCHAR(255) NOT NULL, + rewardId INT NOT NULL, + stock INT NOT NULL, + message VARCHAR(255), + startDate VARCHAR(25) NOT NULL, + endDate VARCHAR(25) NOT NULL, + UNIQUE KEY ongeki_static_present_list_uk (version, presentId, rewardId) +); + +CREATE TABLE ongeki_static_tech_music ( + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, + version INT NOT NULL, + eventId INT NOT NULL, + musicId INT NOT NULL, + level INT NOT NULL, + UNIQUE KEY ongeki_static_tech_music_uk (version, musicId, eventId) +); + +CREATE TABLE ongeki_static_client_testmode ( + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, + regionId INT NOT NULL, + placeId INT NOT NULL, + clientId VARCHAR(11) NOT NULL, + updateDate TIMESTAMP NOT NULL, + isDelivery BOOLEAN NOT NULL, + groupId INT NOT NULL, + groupRole INT NOT NULL, + continueMode INT NOT NULL, + selectMusicTime INT NOT NULL, + advertiseVolume INT NOT NULL, + eventMode INT NOT NULL, + eventMusicNum INT NOT NULL, + patternGp INT NOT NULL, + limitGp INT NOT NULL, + maxLeverMovable INT NOT NULL, + minLeverMovable INT NOT NULL, + UNIQUE KEY ongeki_static_client_testmode_uk (clientId) +); + +CREATE TABLE ongeki_static_game_point ( + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, + type INT NOT NULL, + cost INT NOT NULL, + startDate VARCHAR(25) NOT NULL DEFAULT "2000-01-01 05:00:00.0", + endDate VARCHAR(25) NOT NULL DEFAULT "2099-01-01 05:00:00.0", + UNIQUE KEY ongeki_static_game_point_uk (type) +); + +SET FOREIGN_KEY_CHECKS=1; diff --git a/core/data/schema/versions/SDEZ_1_rollback.sql b/core/data/schema/versions/SDEZ_1_rollback.sql new file mode 100644 index 0000000..6ebbab4 --- /dev/null +++ b/core/data/schema/versions/SDEZ_1_rollback.sql @@ -0,0 +1,3 @@ +SET FOREIGN_KEY_CHECKS=0; +ALTER TABLE mai2_playlog DROP COLUMN trialPlayAchievement; +SET FOREIGN_KEY_CHECKS=1; \ No newline at end of file diff --git a/core/data/schema/versions/SDEZ_2_rollback.sql b/core/data/schema/versions/SDEZ_2_rollback.sql new file mode 100644 index 0000000..b631711 --- /dev/null +++ b/core/data/schema/versions/SDEZ_2_rollback.sql @@ -0,0 +1,21 @@ +ALTER TABLE mai2_item_card +CHANGE COLUMN cardId card_id INT NOT NULL AFTER user, +CHANGE COLUMN cardTypeId card_kind INT NOT NULL, +CHANGE COLUMN charaId chara_id INT NOT NULL, +CHANGE COLUMN mapId map_id INT NOT NULL, +CHANGE COLUMN startDate start_date TIMESTAMP NULL DEFAULT '2018-01-01 00:00:00', +CHANGE COLUMN endDate end_date TIMESTAMP NULL DEFAULT '2038-01-01 00:00:00'; + +ALTER TABLE mai2_item_item +CHANGE COLUMN itemId item_id INT NOT NULL AFTER user, +CHANGE COLUMN itemKind item_kind INT NOT NULL, +CHANGE COLUMN isValid is_valid TINYINT(1) NOT NULL DEFAULT '1'; + +ALTER TABLE mai2_item_character +CHANGE COLUMN characterId character_id INT NOT NULL, +CHANGE COLUMN useCount use_count INT NOT NULL DEFAULT '0'; + +ALTER TABLE mai2_item_charge +CHANGE COLUMN chargeId charge_id INT NOT NULL, +CHANGE COLUMN purchaseDate purchase_date TIMESTAMP NOT NULL, +CHANGE COLUMN validDate valid_date TIMESTAMP NOT NULL; \ No newline at end of file diff --git a/core/data/schema/versions/SDEZ_2_upgrade.sql b/core/data/schema/versions/SDEZ_2_upgrade.sql new file mode 100644 index 0000000..626cc60 --- /dev/null +++ b/core/data/schema/versions/SDEZ_2_upgrade.sql @@ -0,0 +1,3 @@ +SET FOREIGN_KEY_CHECKS=0; +ALTER TABLE mai2_playlog ADD trialPlayAchievement INT NULL; +SET FOREIGN_KEY_CHECKS=1; \ No newline at end of file diff --git a/core/data/schema/versions/SDEZ_3_rollback.sql b/core/data/schema/versions/SDEZ_3_rollback.sql new file mode 100644 index 0000000..79ca098 --- /dev/null +++ b/core/data/schema/versions/SDEZ_3_rollback.sql @@ -0,0 +1,31 @@ +ALTER TABLE mai2_profile_option +DROP COLUMN tapSe; + +ALTER TABLE mai2_score_best +DROP COLUMN extNum1; + +ALTER TABLE mai2_profile_extend +DROP COLUMN playStatusSetting; + +ALTER TABLE mai2_playlog +DROP COLUMN extNum4; + +ALTER TABLE mai2_static_event +DROP COLUMN startDate; + +ALTER TABLE mai2_item_map +CHANGE COLUMN mapId map_id INT NOT NULL, +CHANGE COLUMN isLock is_lock BOOLEAN NOT NULL DEFAULT 0, +CHANGE COLUMN isClear is_clear BOOLEAN NOT NULL DEFAULT 0, +CHANGE COLUMN isComplete is_complete BOOLEAN NOT NULL DEFAULT 0; + +ALTER TABLE mai2_item_friend_season_ranking +CHANGE COLUMN seasonId season_id INT NOT NULL, +CHANGE COLUMN rewardGet reward_get BOOLEAN NOT NULL, +CHANGE COLUMN userName user_name VARCHAR(8) NOT NULL, +CHANGE COLUMN recordDate record_date VARCHAR(255) NOT NULL; + +ALTER TABLE mai2_item_login_bonus +CHANGE COLUMN bonusId bonus_id INT NOT NULL, +CHANGE COLUMN isCurrent is_current BOOLEAN NOT NULL DEFAULT 0, +CHANGE COLUMN isComplete is_complete BOOLEAN NOT NULL DEFAULT 0; \ No newline at end of file diff --git a/core/data/schema/versions/SDEZ_3_upgrade.sql b/core/data/schema/versions/SDEZ_3_upgrade.sql new file mode 100644 index 0000000..bdb5906 --- /dev/null +++ b/core/data/schema/versions/SDEZ_3_upgrade.sql @@ -0,0 +1,21 @@ +ALTER TABLE mai2_item_card +CHANGE COLUMN card_id cardId INT NOT NULL AFTER user, +CHANGE COLUMN card_kind cardTypeId INT NOT NULL, +CHANGE COLUMN chara_id charaId INT NOT NULL, +CHANGE COLUMN map_id mapId INT NOT NULL, +CHANGE COLUMN start_date startDate TIMESTAMP NULL DEFAULT '2018-01-01 00:00:00', +CHANGE COLUMN end_date endDate TIMESTAMP NULL DEFAULT '2038-01-01 00:00:00'; + +ALTER TABLE mai2_item_item +CHANGE COLUMN item_id itemId INT NOT NULL AFTER user, +CHANGE COLUMN item_kind itemKind INT NOT NULL, +CHANGE COLUMN is_valid isValid TINYINT(1) NOT NULL DEFAULT '1'; + +ALTER TABLE mai2_item_character +CHANGE COLUMN character_id characterId INT NOT NULL, +CHANGE COLUMN use_count useCount INT NOT NULL DEFAULT '0'; + +ALTER TABLE mai2_item_charge +CHANGE COLUMN charge_id chargeId INT NOT NULL, +CHANGE COLUMN purchase_date purchaseDate TIMESTAMP NOT NULL, +CHANGE COLUMN valid_date validDate TIMESTAMP NOT NULL; diff --git a/core/data/schema/versions/SDEZ_4_rollback.sql b/core/data/schema/versions/SDEZ_4_rollback.sql new file mode 100644 index 0000000..b8be7b3 --- /dev/null +++ b/core/data/schema/versions/SDEZ_4_rollback.sql @@ -0,0 +1,3 @@ +ALTER TABLE mai2_item_card + CHANGE COLUMN startDate startDate TIMESTAMP DEFAULT "2018-01-01 00:00:00.0", + CHANGE COLUMN endDate endDate TIMESTAMP DEFAULT "2038-01-01 00:00:00.0"; \ No newline at end of file diff --git a/core/data/schema/versions/SDEZ_4_upgrade.sql b/core/data/schema/versions/SDEZ_4_upgrade.sql new file mode 100644 index 0000000..a670d6a --- /dev/null +++ b/core/data/schema/versions/SDEZ_4_upgrade.sql @@ -0,0 +1,31 @@ +ALTER TABLE mai2_profile_option +ADD COLUMN tapSe INT NOT NULL DEFAULT 0 AFTER tapDesign; + +ALTER TABLE mai2_score_best +ADD COLUMN extNum1 INT NOT NULL DEFAULT 0; + +ALTER TABLE mai2_profile_extend +ADD COLUMN playStatusSetting INT NOT NULL DEFAULT 0; + +ALTER TABLE mai2_playlog +ADD COLUMN extNum4 INT NOT NULL DEFAULT 0; + +ALTER TABLE mai2_static_event +ADD COLUMN startDate TIMESTAMP NOT NULL DEFAULT current_timestamp(); + +ALTER TABLE mai2_item_map +CHANGE COLUMN map_id mapId INT NOT NULL, +CHANGE COLUMN is_lock isLock BOOLEAN NOT NULL DEFAULT 0, +CHANGE COLUMN is_clear isClear BOOLEAN NOT NULL DEFAULT 0, +CHANGE COLUMN is_complete isComplete BOOLEAN NOT NULL DEFAULT 0; + +ALTER TABLE mai2_item_friend_season_ranking +CHANGE COLUMN season_id seasonId INT NOT NULL, +CHANGE COLUMN reward_get rewardGet BOOLEAN NOT NULL, +CHANGE COLUMN user_name userName VARCHAR(8) NOT NULL, +CHANGE COLUMN record_date recordDate TIMESTAMP NOT NULL; + +ALTER TABLE mai2_item_login_bonus +CHANGE COLUMN bonus_id bonusId INT NOT NULL, +CHANGE COLUMN is_current isCurrent BOOLEAN NOT NULL DEFAULT 0, +CHANGE COLUMN is_complete isComplete BOOLEAN NOT NULL DEFAULT 0; diff --git a/core/data/schema/versions/SDEZ_5_rollback.sql b/core/data/schema/versions/SDEZ_5_rollback.sql new file mode 100644 index 0000000..2b66afe --- /dev/null +++ b/core/data/schema/versions/SDEZ_5_rollback.sql @@ -0,0 +1,78 @@ +DELETE FROM mai2_static_event WHERE version < 13; +UPDATE mai2_static_event SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_static_music WHERE version < 13; +UPDATE mai2_static_music SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_static_ticket WHERE version < 13; +UPDATE mai2_static_ticket SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_static_cards WHERE version < 13; +UPDATE mai2_static_cards SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_profile_detail WHERE version < 13; +UPDATE mai2_profile_detail SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_profile_extend WHERE version < 13; +UPDATE mai2_profile_extend SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_profile_option WHERE version < 13; +UPDATE mai2_profile_option SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_profile_ghost WHERE version < 13; +UPDATE mai2_profile_ghost SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_profile_rating WHERE version < 13; +UPDATE mai2_profile_rating SET version = version - 13 WHERE version >= 13; + +DROP TABLE maimai_score_best; +DROP TABLE maimai_playlog; +DROP TABLE maimai_profile_detail; +DROP TABLE maimai_profile_option; +DROP TABLE maimai_profile_web_option; +DROP TABLE maimai_profile_grade_status; + +ALTER TABLE mai2_item_character DROP COLUMN point; + +ALTER TABLE mai2_item_card MODIFY COLUMN cardId int(11) NOT NULL; +ALTER TABLE mai2_item_card MODIFY COLUMN cardTypeId int(11) NOT NULL; +ALTER TABLE mai2_item_card MODIFY COLUMN charaId int(11) NOT NULL; +ALTER TABLE mai2_item_card MODIFY COLUMN mapId int(11) NOT NULL; + +ALTER TABLE mai2_item_character MODIFY COLUMN characterId int(11) NOT NULL; +ALTER TABLE mai2_item_character MODIFY COLUMN level int(11) NOT NULL; +ALTER TABLE mai2_item_character MODIFY COLUMN awakening int(11) NOT NULL; +ALTER TABLE mai2_item_character MODIFY COLUMN useCount int(11) NOT NULL; + +ALTER TABLE mai2_item_charge MODIFY COLUMN chargeId int(11) NOT NULL; +ALTER TABLE mai2_item_charge MODIFY COLUMN stock int(11) NOT NULL; + +ALTER TABLE mai2_item_favorite MODIFY COLUMN itemKind int(11) NOT NULL; + +ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN seasonId int(11) NOT NULL; +ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN point int(11) NOT NULL; +ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN `rank` int(11) NOT NULL; +ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN rewardGet tinyint(1) NOT NULL; +ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN userName varchar(8) NOT NULL; + +ALTER TABLE mai2_item_item MODIFY COLUMN itemId int(11) NOT NULL; +ALTER TABLE mai2_item_item MODIFY COLUMN itemKind int(11) NOT NULL; +ALTER TABLE mai2_item_item MODIFY COLUMN stock int(11) NOT NULL; +ALTER TABLE mai2_item_item MODIFY COLUMN isValid tinyint(1) NOT NULL; + +ALTER TABLE mai2_item_login_bonus MODIFY COLUMN bonusId int(11) NOT NULL; +ALTER TABLE mai2_item_login_bonus MODIFY COLUMN point int(11) NOT NULL; +ALTER TABLE mai2_item_login_bonus MODIFY COLUMN isCurrent tinyint(1) NOT NULL; +ALTER TABLE mai2_item_login_bonus MODIFY COLUMN isComplete tinyint(1) NOT NULL; + +ALTER TABLE mai2_item_map MODIFY COLUMN mapId int(11) NOT NULL; +ALTER TABLE mai2_item_map MODIFY COLUMN distance int(11) NOT NULL; +ALTER TABLE mai2_item_map MODIFY COLUMN isLock tinyint(1) NOT NULL; +ALTER TABLE mai2_item_map MODIFY COLUMN isClear tinyint(1) NOT NULL; +ALTER TABLE mai2_item_map MODIFY COLUMN isComplete tinyint(1) NOT NULL; + +ALTER TABLE mai2_item_print_detail MODIFY COLUMN printDate timestamp DEFAULT current_timestamp() NOT NULL; +ALTER TABLE mai2_item_print_detail MODIFY COLUMN serialId varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL; +ALTER TABLE mai2_item_print_detail MODIFY COLUMN placeId int(11) NOT NULL; +ALTER TABLE mai2_item_print_detail MODIFY COLUMN clientId varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL; +ALTER TABLE mai2_item_print_detail MODIFY COLUMN printerSerialId varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL; \ No newline at end of file diff --git a/core/data/schema/versions/SDEZ_5_upgrade.sql b/core/data/schema/versions/SDEZ_5_upgrade.sql new file mode 100644 index 0000000..cc4912f --- /dev/null +++ b/core/data/schema/versions/SDEZ_5_upgrade.sql @@ -0,0 +1,3 @@ +ALTER TABLE mai2_item_card + CHANGE COLUMN startDate startDate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CHANGE COLUMN endDate endDate TIMESTAMP NOT NULL; \ No newline at end of file diff --git a/core/data/schema/versions/SDEZ_6_rollback.sql b/core/data/schema/versions/SDEZ_6_rollback.sql new file mode 100644 index 0000000..0ca7036 --- /dev/null +++ b/core/data/schema/versions/SDEZ_6_rollback.sql @@ -0,0 +1 @@ +DROP TABLE aime.mai2_profile_consec_logins; diff --git a/core/data/schema/versions/SDEZ_6_upgrade.sql b/core/data/schema/versions/SDEZ_6_upgrade.sql new file mode 100644 index 0000000..472747d --- /dev/null +++ b/core/data/schema/versions/SDEZ_6_upgrade.sql @@ -0,0 +1,62 @@ +UPDATE mai2_static_event SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_static_music SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_static_ticket SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_static_cards SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_profile_detail SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_profile_extend SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_profile_option SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_profile_ghost SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_profile_rating SET version = version + 13 WHERE version < 1000; + +ALTER TABLE mai2_item_character ADD point int(11) NULL; + +ALTER TABLE mai2_item_card MODIFY COLUMN cardId int(11) NULL; +ALTER TABLE mai2_item_card MODIFY COLUMN cardTypeId int(11) NULL; +ALTER TABLE mai2_item_card MODIFY COLUMN charaId int(11) NULL; +ALTER TABLE mai2_item_card MODIFY COLUMN mapId int(11) NULL; + +ALTER TABLE mai2_item_character MODIFY COLUMN characterId int(11) NULL; +ALTER TABLE mai2_item_character MODIFY COLUMN level int(11) NULL; +ALTER TABLE mai2_item_character MODIFY COLUMN awakening int(11) NULL; +ALTER TABLE mai2_item_character MODIFY COLUMN useCount int(11) NULL; + +ALTER TABLE mai2_item_charge MODIFY COLUMN chargeId int(11) NULL; +ALTER TABLE mai2_item_charge MODIFY COLUMN stock int(11) NULL; + +ALTER TABLE mai2_item_favorite MODIFY COLUMN itemKind int(11) NULL; + +ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN seasonId int(11) NULL; +ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN point int(11) NULL; +ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN `rank` int(11) NULL; +ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN rewardGet tinyint(1) NULL; +ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN userName varchar(8) NULL; + +ALTER TABLE mai2_item_item MODIFY COLUMN itemId int(11) NULL; +ALTER TABLE mai2_item_item MODIFY COLUMN itemKind int(11) NULL; +ALTER TABLE mai2_item_item MODIFY COLUMN stock int(11) NULL; +ALTER TABLE mai2_item_item MODIFY COLUMN isValid tinyint(1) NULL; + +ALTER TABLE mai2_item_login_bonus MODIFY COLUMN bonusId int(11) NULL; +ALTER TABLE mai2_item_login_bonus MODIFY COLUMN point int(11) NULL; +ALTER TABLE mai2_item_login_bonus MODIFY COLUMN isCurrent tinyint(1) NULL; +ALTER TABLE mai2_item_login_bonus MODIFY COLUMN isComplete tinyint(1) NULL; + +ALTER TABLE mai2_item_map MODIFY COLUMN mapId int(11) NULL; +ALTER TABLE mai2_item_map MODIFY COLUMN distance int(11) NULL; +ALTER TABLE mai2_item_map MODIFY COLUMN isLock tinyint(1) NULL; +ALTER TABLE mai2_item_map MODIFY COLUMN isClear tinyint(1) NULL; +ALTER TABLE mai2_item_map MODIFY COLUMN isComplete tinyint(1) NULL; + +ALTER TABLE mai2_item_print_detail MODIFY COLUMN printDate timestamp DEFAULT current_timestamp() NULL; +ALTER TABLE mai2_item_print_detail MODIFY COLUMN serialId varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL; +ALTER TABLE mai2_item_print_detail MODIFY COLUMN placeId int(11) NULL; +ALTER TABLE mai2_item_print_detail MODIFY COLUMN clientId varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL; +ALTER TABLE mai2_item_print_detail MODIFY COLUMN printerSerialId varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL; \ No newline at end of file diff --git a/core/data/schema/versions/SDEZ_7_rollback.sql b/core/data/schema/versions/SDEZ_7_rollback.sql new file mode 100644 index 0000000..6bb9270 --- /dev/null +++ b/core/data/schema/versions/SDEZ_7_rollback.sql @@ -0,0 +1,10 @@ +ALTER TABLE mai2_profile_detail + DROP COLUMN mapStock; + +ALTER TABLE mai2_profile_extend + DROP COLUMN selectResultScoreViewType; + +ALTER TABLE mai2_profile_option + DROP COLUMN outFrameType, + DROP COLUMN touchVolume, + DROP COLUMN breakSlideVolume; diff --git a/core/data/schema/versions/SDEZ_7_upgrade.sql b/core/data/schema/versions/SDEZ_7_upgrade.sql new file mode 100644 index 0000000..20f3c70 --- /dev/null +++ b/core/data/schema/versions/SDEZ_7_upgrade.sql @@ -0,0 +1,9 @@ +CREATE TABLE `mai2_profile_consec_logins` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `user` int(11) NOT NULL, + `version` int(11) NOT NULL, + `logins` int(11) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `mai2_profile_consec_logins_uk` (`user`,`version`), + CONSTRAINT `mai2_profile_consec_logins_ibfk_1` FOREIGN KEY (`user`) REFERENCES `aime_user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; \ No newline at end of file diff --git a/core/data/schema/versions/SDEZ_8_upgrade.sql b/core/data/schema/versions/SDEZ_8_upgrade.sql new file mode 100644 index 0000000..c782d81 --- /dev/null +++ b/core/data/schema/versions/SDEZ_8_upgrade.sql @@ -0,0 +1,10 @@ +ALTER TABLE mai2_profile_detail + ADD mapStock INT NULL AFTER playCount; + +ALTER TABLE mai2_profile_extend + ADD selectResultScoreViewType INT NULL AFTER selectResultDetails; + +ALTER TABLE mai2_profile_option + ADD outFrameType INT NULL AFTER dispCenter, + ADD touchVolume INT NULL AFTER slideVolume, + ADD breakSlideVolume INT NULL AFTER slideVolume; diff --git a/core/data/schema/versions/SDFE_3_rollback.sql b/core/data/schema/versions/SDFE_3_rollback.sql new file mode 100644 index 0000000..6d1d2b8 --- /dev/null +++ b/core/data/schema/versions/SDFE_3_rollback.sql @@ -0,0 +1,2 @@ +SET FOREIGN_KEY_CHECKS=0; +SET FOREIGN_KEY_CHECKS=1; diff --git a/core/data/schema/versions/SDFE_4_rollback.sql b/core/data/schema/versions/SDFE_4_rollback.sql new file mode 100644 index 0000000..417c227 --- /dev/null +++ b/core/data/schema/versions/SDFE_4_rollback.sql @@ -0,0 +1 @@ +ALTER TABLE wacca_profile DROP COLUMN playcount_time_free; \ No newline at end of file diff --git a/core/data/schema/versions/SDFE_4_upgrade.sql b/core/data/schema/versions/SDFE_4_upgrade.sql new file mode 100644 index 0000000..3f2cc9a --- /dev/null +++ b/core/data/schema/versions/SDFE_4_upgrade.sql @@ -0,0 +1 @@ +DELETE FROM wacca_item WHERE type=17 AND item_id=312002; \ No newline at end of file diff --git a/core/data/schema/versions/SDFE_5_upgrade.sql b/core/data/schema/versions/SDFE_5_upgrade.sql new file mode 100644 index 0000000..a9795bf --- /dev/null +++ b/core/data/schema/versions/SDFE_5_upgrade.sql @@ -0,0 +1 @@ +ALTER TABLE wacca_profile ADD playcount_time_free int(11) DEFAULT 0 NULL AFTER playcount_stageup; \ No newline at end of file diff --git a/core/frontend.py b/core/frontend.py new file mode 100644 index 0000000..0ee2211 --- /dev/null +++ b/core/frontend.py @@ -0,0 +1,460 @@ +import logging, coloredlogs +from typing import Any, Dict, List +from twisted.web import resource +from twisted.web.util import redirectTo +from twisted.web.http import Request +from logging.handlers import TimedRotatingFileHandler +from twisted.web.server import Session +from zope.interface import Interface, Attribute, implementer +from twisted.python.components import registerAdapter +import jinja2 +import bcrypt +import re +from enum import Enum +from urllib import parse + +from core import CoreConfig, Utils +from core.data import Data + + +class IUserSession(Interface): + userId = Attribute("User's ID") + current_ip = Attribute("User's current ip address") + permissions = Attribute("User's permission level") + ongeki_version = Attribute("User's selected Ongeki Version") + +class PermissionOffset(Enum): + USER = 0 # Regular user + USERMOD = 1 # Can moderate other users + ACMOD = 2 # Can add arcades and cabs + SYSADMIN = 3 # Can change settings + # 4 - 6 reserved for future use + OWNER = 7 # Can do anything + +@implementer(IUserSession) +class UserSession(object): + def __init__(self, session): + self.userId = 0 + self.current_ip = "0.0.0.0" + self.permissions = 0 + self.ongeki_version = 7 + + +class FrontendServlet(resource.Resource): + def getChild(self, name: bytes, request: Request): + self.logger.debug(f"{Utils.get_ip_addr(request)} -> {name.decode()}") + if name == b"": + return self + return resource.Resource.getChild(self, name, request) + + def __init__(self, cfg: CoreConfig, config_dir: str) -> None: + self.config = cfg + log_fmt_str = "[%(asctime)s] Frontend | %(levelname)s | %(message)s" + log_fmt = logging.Formatter(log_fmt_str) + self.logger = logging.getLogger("frontend") + self.environment = jinja2.Environment(loader=jinja2.FileSystemLoader(".")) + self.game_list: List[Dict[str, str]] = [] + self.children: Dict[str, Any] = {} + + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.config.server.log_dir, "frontend"), + when="d", + backupCount=10, + ) + fileHandler.setFormatter(log_fmt) + + consoleHandler = logging.StreamHandler() + consoleHandler.setFormatter(log_fmt) + + self.logger.addHandler(fileHandler) + self.logger.addHandler(consoleHandler) + + self.logger.setLevel(cfg.frontend.loglevel) + coloredlogs.install( + level=cfg.frontend.loglevel, logger=self.logger, fmt=log_fmt_str + ) + registerAdapter(UserSession, Session, IUserSession) + + fe_game = FE_Game(cfg, self.environment) + games = Utils.get_all_titles() + for game_dir, game_mod in games.items(): + if hasattr(game_mod, "frontend"): + try: + game_fe = game_mod.frontend(cfg, self.environment, config_dir) + self.game_list.append({"url": game_dir, "name": game_fe.nav_name}) + fe_game.putChild(game_dir.encode(), game_fe) + + except Exception as e: + self.logger.error( + f"Failed to import frontend from {game_dir} because {e}" + ) + + self.environment.globals["game_list"] = self.game_list + self.putChild(b"gate", FE_Gate(cfg, self.environment)) + self.putChild(b"user", FE_User(cfg, self.environment)) + self.putChild(b"sys", FE_System(cfg, self.environment)) + self.putChild(b"arcade", FE_Arcade(cfg, self.environment)) + self.putChild(b"cab", FE_Machine(cfg, self.environment)) + self.putChild(b"game", fe_game) + + self.logger.info( + f"Ready on port {self.config.frontend.port} serving {len(fe_game.children)} games" + ) + + def render_GET(self, request): + self.logger.debug(f"{Utils.get_ip_addr(request)} -> {request.uri.decode()}") + template = self.environment.get_template("core/frontend/index.jinja") + return template.render( + server_name=self.config.server.name, + title=self.config.server.name, + game_list=self.game_list, + sesh=vars(IUserSession(request.getSession())), + ).encode("utf-16") + + +class FE_Base(resource.Resource): + """ + A Generic skeleton class that all frontend handlers should inherit from + Initializes the environment, data, logger, config, and sets isLeaf to true + It is expected that game implementations of this class overwrite many of these + """ + + isLeaf = True + + def __init__(self, cfg: CoreConfig, environment: jinja2.Environment) -> None: + self.core_config = cfg + self.data = Data(cfg) + self.logger = logging.getLogger("frontend") + self.environment = environment + self.nav_name = "nav_name" + + +class FE_Gate(FE_Base): + def render_GET(self, request: Request): + self.logger.debug(f"{Utils.get_ip_addr(request)} -> {request.uri.decode()}") + uri: str = request.uri.decode() + + sesh = request.getSession() + usr_sesh = IUserSession(sesh) + if usr_sesh.userId > 0: + return redirectTo(b"/user", request) + + if uri.startswith("/gate/create"): + return self.create_user(request) + + if b"e" in request.args: + try: + err = int(request.args[b"e"][0].decode()) + except Exception: + err = 0 + + else: + err = 0 + + template = self.environment.get_template("core/frontend/gate/gate.jinja") + return template.render( + title=f"{self.core_config.server.name} | Login Gate", + error=err, + sesh=vars(usr_sesh), + ).encode("utf-16") + + def render_POST(self, request: Request): + uri = request.uri.decode() + ip = Utils.get_ip_addr(request) + + if uri == "/gate/gate.login": + access_code: str = request.args[b"access_code"][0].decode() + passwd: bytes = request.args[b"passwd"][0] + if passwd == b"": + passwd = None + + uid = self.data.card.get_user_id_from_card(access_code) + user = self.data.user.get_user(uid) + if uid is None: + return redirectTo(b"/gate?e=1", request) + + if passwd is None: + sesh = self.data.user.check_password(uid) + + if sesh is not None: + return redirectTo( + f"/gate/create?ac={access_code}".encode(), request + ) + return redirectTo(b"/gate?e=1", request) + + if not self.data.user.check_password(uid, passwd): + return redirectTo(b"/gate?e=1", request) + + self.logger.info(f"Successful login of user {uid} at {ip}") + + sesh = request.getSession() + usr_sesh = IUserSession(sesh) + usr_sesh.userId = uid + usr_sesh.current_ip = ip + usr_sesh.permissions = user['permissions'] + + return redirectTo(b"/user", request) + + elif uri == "/gate/gate.create": + access_code: str = request.args[b"access_code"][0].decode() + username: str = request.args[b"username"][0] + email: str = request.args[b"email"][0].decode() + passwd: bytes = request.args[b"passwd"][0] + + uid = self.data.card.get_user_id_from_card(access_code) + if uid is None: + return redirectTo(b"/gate?e=1", request) + + salt = bcrypt.gensalt() + hashed = bcrypt.hashpw(passwd, salt) + + result = self.data.user.create_user( + uid, username, email.lower(), hashed.decode(), 1 + ) + if result is None: + return redirectTo(b"/gate?e=3", request) + + if not self.data.user.check_password(uid, passwd): + return redirectTo(b"/gate", request) + + return redirectTo(b"/user", request) + + else: + return b"" + + def create_user(self, request: Request): + if b"ac" not in request.args or len(request.args[b"ac"][0].decode()) != 20: + return redirectTo(b"/gate?e=2", request) + + ac = request.args[b"ac"][0].decode() + card = self.data.card.get_card_by_access_code(ac) + if card is None: + return redirectTo(b"/gate?e=1", request) + + user = self.data.user.get_user(card['user']) + if user is None: + self.logger.warning(f"Card {ac} exists with no/invalid associated user ID {card['user']}") + return redirectTo(b"/gate?e=0", request) + + if user['password'] is not None: + return redirectTo(b"/gate?e=1", request) + + template = self.environment.get_template("core/frontend/gate/create.jinja") + return template.render( + title=f"{self.core_config.server.name} | Create User", + code=ac, + sesh={"userId": 0, "permissions": 0}, + ).encode("utf-16") + + +class FE_User(FE_Base): + def render_GET(self, request: Request): + uri = request.uri.decode() + template = self.environment.get_template("core/frontend/user/index.jinja") + + sesh: Session = request.getSession() + usr_sesh = IUserSession(sesh) + if usr_sesh.userId == 0: + return redirectTo(b"/gate", request) + + m = re.match("\/user\/(\d*)", uri) + + if m is not None: + usrid = m.group(1) + if usr_sesh.permissions < 1 << PermissionOffset.USERMOD.value or not usrid == usr_sesh.userId: + return redirectTo(b"/user", request) + + else: + usrid = usr_sesh.userId + + user = self.data.user.get_user(usrid) + if user is None: + return redirectTo(b"/user", request) + + cards = self.data.card.get_user_cards(usrid) + arcades = self.data.arcade.get_arcades_managed_by_user(usrid) + + card_data = [] + arcade_data = [] + + for c in cards: + if c['is_locked']: + status = 'Locked' + elif c['is_banned']: + status = 'Banned' + else: + status = 'Active' + + card_data.append({'access_code': c['access_code'], 'status': status}) + + for a in arcades: + arcade_data.append({'id': a['id'], 'name': a['name']}) + + return template.render( + title=f"{self.core_config.server.name} | Account", + sesh=vars(usr_sesh), + cards=card_data, + username=user['username'], + arcades=arcade_data + ).encode("utf-16") + + def render_POST(self, request: Request): + pass + + +class FE_System(FE_Base): + def render_GET(self, request: Request): + uri = request.uri.decode() + template = self.environment.get_template("core/frontend/sys/index.jinja") + usrlist: List[Dict] = [] + aclist: List[Dict] = [] + cablist: List[Dict] = [] + + sesh: Session = request.getSession() + usr_sesh = IUserSession(sesh) + if usr_sesh.userId == 0 or usr_sesh.permissions < 1 << PermissionOffset.USERMOD.value: + return redirectTo(b"/gate", request) + + if uri.startswith("/sys/lookup.user?"): + uri_parse = parse.parse_qs(uri.replace("/sys/lookup.user?", "")) # lop off the first bit + uid_search = uri_parse.get("usrId") + email_search = uri_parse.get("usrEmail") + uname_search = uri_parse.get("usrName") + + if uid_search is not None: + u = self.data.user.get_user(uid_search[0]) + if u is not None: + usrlist.append(u._asdict()) + + elif email_search is not None: + u = self.data.user.find_user_by_email(email_search[0]) + if u is not None: + usrlist.append(u._asdict()) + + elif uname_search is not None: + ul = self.data.user.find_user_by_username(uname_search[0]) + for u in ul: + usrlist.append(u._asdict()) + + elif uri.startswith("/sys/lookup.arcade?"): + uri_parse = parse.parse_qs(uri.replace("/sys/lookup.arcade?", "")) # lop off the first bit + ac_id_search = uri_parse.get("arcadeId") + ac_name_search = uri_parse.get("arcadeName") + ac_user_search = uri_parse.get("arcadeUser") + ac_ip_search = uri_parse.get("arcadeIp") + + if ac_id_search is not None: + u = self.data.arcade.get_arcade(ac_id_search[0]) + if u is not None: + aclist.append(u._asdict()) + + elif ac_name_search is not None: + ul = self.data.arcade.get_arcade_by_name(ac_name_search[0]) + if ul is not None: + for u in ul: + aclist.append(u._asdict()) + + elif ac_user_search is not None: + ul = self.data.arcade.get_arcades_managed_by_user(ac_user_search[0]) + if ul is not None: + for u in ul: + aclist.append(u._asdict()) + + elif ac_ip_search is not None: + ul = self.data.arcade.get_arcades_by_ip(ac_ip_search[0]) + if ul is not None: + for u in ul: + aclist.append(u._asdict()) + + elif uri.startswith("/sys/lookup.cab?"): + uri_parse = parse.parse_qs(uri.replace("/sys/lookup.cab?", "")) # lop off the first bit + cab_id_search = uri_parse.get("cabId") + cab_serial_search = uri_parse.get("cabSerial") + cab_acid_search = uri_parse.get("cabAcId") + + if cab_id_search is not None: + u = self.data.arcade.get_machine(id=cab_id_search[0]) + if u is not None: + cablist.append(u._asdict()) + + elif cab_serial_search is not None: + u = self.data.arcade.get_machine(serial=cab_serial_search[0]) + if u is not None: + cablist.append(u._asdict()) + + elif cab_acid_search is not None: + ul = self.data.arcade.get_arcade_machines(cab_acid_search[0]) + for u in ul: + cablist.append(u._asdict()) + + return template.render( + title=f"{self.core_config.server.name} | System", + sesh=vars(usr_sesh), + usrlist=usrlist, + aclist=aclist, + cablist=cablist, + ).encode("utf-16") + + +class FE_Game(FE_Base): + isLeaf = False + children: Dict[str, Any] = {} + + def getChild(self, name: bytes, request: Request): + if name == b"": + return self + return resource.Resource.getChild(self, name, request) + + def render_GET(self, request: Request) -> bytes: + return redirectTo(b"/user", request) + + +class FE_Arcade(FE_Base): + def render_GET(self, request: Request): + uri = request.uri.decode() + template = self.environment.get_template("core/frontend/arcade/index.jinja") + managed = [] + + sesh: Session = request.getSession() + usr_sesh = IUserSession(sesh) + if usr_sesh.userId == 0: + return redirectTo(b"/gate", request) + + m = re.match("\/arcade\/(\d*)", uri) + + if m is not None: + arcadeid = m.group(1) + perms = self.data.arcade.get_manager_permissions(usr_sesh.userId, arcadeid) + arcade = self.data.arcade.get_arcade(arcadeid) + + if perms is None: + perms = 0 + + else: + return redirectTo(b"/user", request) + + return template.render( + title=f"{self.core_config.server.name} | Arcade", + sesh=vars(usr_sesh), + error=0, + perms=perms, + arcade=arcade._asdict() + ).encode("utf-16") + + +class FE_Machine(FE_Base): + def render_GET(self, request: Request): + uri = request.uri.decode() + template = self.environment.get_template("core/frontend/machine/index.jinja") + + sesh: Session = request.getSession() + usr_sesh = IUserSession(sesh) + if usr_sesh.userId == 0: + return redirectTo(b"/gate", request) + + return template.render( + title=f"{self.core_config.server.name} | Machine", + sesh=vars(usr_sesh), + arcade={}, + error=0, + ).encode("utf-16") \ No newline at end of file diff --git a/core/frontend/arcade/index.jinja b/core/frontend/arcade/index.jinja new file mode 100644 index 0000000..20a1f46 --- /dev/null +++ b/core/frontend/arcade/index.jinja @@ -0,0 +1,4 @@ +{% extends "core/frontend/index.jinja" %} +{% block content %} +

{{ arcade.name }}

+{% endblock content %} \ No newline at end of file diff --git a/core/frontend/gate/create.jinja b/core/frontend/gate/create.jinja new file mode 100644 index 0000000..f8b5e51 --- /dev/null +++ b/core/frontend/gate/create.jinja @@ -0,0 +1,24 @@ +{% extends "core/frontend/index.jinja" %} +{% block content %} +

Create User

+
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+

+ +
+{% endblock content %} \ No newline at end of file diff --git a/core/frontend/gate/gate.jinja b/core/frontend/gate/gate.jinja new file mode 100644 index 0000000..90abb98 --- /dev/null +++ b/core/frontend/gate/gate.jinja @@ -0,0 +1,32 @@ +{% extends "core/frontend/index.jinja" %} +{% block content %} +

Gate

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

+ +
+
*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/frontend/index.jinja b/core/frontend/index.jinja new file mode 100644 index 0000000..3dacbe5 --- /dev/null +++ b/core/frontend/index.jinja @@ -0,0 +1,92 @@ + + + + {{ title }} + + + + + + + {% include "core/frontend/widgets/topbar.jinja" %} + {% block content %} +

{{ server_name }}

+ {% endblock content %} + + \ No newline at end of file diff --git a/core/frontend/machine/index.jinja b/core/frontend/machine/index.jinja new file mode 100644 index 0000000..01e90a0 --- /dev/null +++ b/core/frontend/machine/index.jinja @@ -0,0 +1,5 @@ +{% extends "core/frontend/index.jinja" %} +{% block content %} +{% include "core/frontend/widgets/err_banner.jinja" %} +

Machine Management

+{% endblock content %} \ No newline at end of file diff --git a/core/frontend/sys/index.jinja b/core/frontend/sys/index.jinja new file mode 100644 index 0000000..120051a --- /dev/null +++ b/core/frontend/sys/index.jinja @@ -0,0 +1,103 @@ +{% extends "core/frontend/index.jinja" %} +{% block content %} +

System Management

+ +
+ {% if sesh.permissions >= 2 %} +
+
+

User Search

+
+ + +
+ OR +
+ + +
+ OR +
+ + +
+
+ +
+
+ {% endif %} + {% if sesh.permissions >= 4 %} +
+
+

Arcade Search

+
+ + +
+ OR +
+ + +
+ OR +
+ + +
+ OR +
+ + +
+
+ +
+
+
+
+

Machine Search

+
+ + +
+ OR +
+ + +
+ OR +
+ + +
+
+ +
+
+ {% endif %} +
+
+ {% if sesh.permissions >= 2 %} + + {% endif %} + {% if sesh.permissions >= 4 %} + + {% endif %} +
+
+ +
+{% endblock content %} \ No newline at end of file diff --git a/core/frontend/user/index.jinja b/core/frontend/user/index.jinja new file mode 100644 index 0000000..2f76b14 --- /dev/null +++ b/core/frontend/user/index.jinja @@ -0,0 +1,41 @@ +{% extends "core/frontend/index.jinja" %} +{% block content %} +

Management for {{ username }}

+

Cards

+
    +{% for c in cards %} +
  • {{ c.access_code }}: {{ c.status }} {% if c.status == 'Active'%}{% elif c.status == 'Locked' %}{% endif %} 
  • +{% endfor %} +
+ +{% if arcades is defined %} +

Arcades

+ +{% endif %} + + + +{% endblock content %} \ No newline at end of file diff --git a/core/frontend/widgets/err_banner.jinja b/core/frontend/widgets/err_banner.jinja new file mode 100644 index 0000000..6d239f6 --- /dev/null +++ b/core/frontend/widgets/err_banner.jinja @@ -0,0 +1,18 @@ +{% if error > 0 %} +
+

Error

+{% if error == 1 %} +Card not registered, or wrong password +{% elif error == 2 %} +Missing or malformed access code +{% elif error == 3 %} +Failed to create user +{% elif error == 4 %} +Arcade not found +{% elif error == 5 %} +Machine not found +{% else %} +An unknown error occoured +{% endif %} +
+{% endif %} \ No newline at end of file diff --git a/core/frontend/widgets/topbar.jinja b/core/frontend/widgets/topbar.jinja new file mode 100644 index 0000000..1a85873 --- /dev/null +++ b/core/frontend/widgets/topbar.jinja @@ -0,0 +1,21 @@ +
+ Navigation +
+
+   + {% for game in game_list %} +   + {% endfor %} +
+ +
+ {% if sesh is defined and sesh["permissions"] >= 2 %} + + {% endif %} + {% if sesh is defined and sesh["userId"] > 0 %} + + {% else %} + + {% endif %} + +
\ No newline at end of file diff --git a/core/mucha.py b/core/mucha.py index a861101..7c6f0ab 100644 --- a/core/mucha.py +++ b/core/mucha.py @@ -1,115 +1,184 @@ -from typing import Dict, Any, Optional +from typing import Dict, Any, Optional, List import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler from twisted.web import resource from twisted.web.http import Request from datetime import datetime +from Crypto.Cipher import Blowfish import pytz -from core.config import CoreConfig +from .config import CoreConfig +from .utils import Utils +from .title import TitleServlet class MuchaServlet: - def __init__(self, cfg: CoreConfig) -> None: + mucha_registry: List[str] = [] + def __init__(self, cfg: CoreConfig, cfg_dir: str) -> None: self.config = cfg + self.config_dir = cfg_dir - self.logger = logging.getLogger('mucha') + self.logger = logging.getLogger("mucha") log_fmt_str = "[%(asctime)s] Mucha | %(levelname)s | %(message)s" log_fmt = logging.Formatter(log_fmt_str) - - fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.config.server.log_dir, "mucha"), when="d", backupCount=10) + + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.config.server.log_dir, "mucha"), + when="d", + backupCount=10, + ) fileHandler.setFormatter(log_fmt) - + consoleHandler = logging.StreamHandler() consoleHandler.setFormatter(log_fmt) self.logger.addHandler(fileHandler) self.logger.addHandler(consoleHandler) - - self.logger.setLevel(logging.INFO) - coloredlogs.install(level=logging.INFO, logger=self.logger, fmt=log_fmt_str) - def handle_boardauth(self, request: Request) -> bytes: + self.logger.setLevel(cfg.mucha.loglevel) + coloredlogs.install(level=cfg.mucha.loglevel, logger=self.logger, fmt=log_fmt_str) + + for _, mod in TitleServlet.title_registry.items(): + if hasattr(mod, "get_mucha_info"): + enabled, game_cd = mod.get_mucha_info( + self.config, self.config_dir + ) + if enabled: + self.mucha_registry.append(game_cd) + + self.logger.info(f"Serving {len(self.mucha_registry)} games") + + def handle_boardauth(self, request: Request, _: Dict) -> bytes: req_dict = self.mucha_preprocess(request.content.getvalue()) + client_ip = Utils.get_ip_addr(request) + if req_dict is None: - self.logger.error(f"Error processing mucha request {request.content.getvalue()}") - return b"" + self.logger.error( + f"Error processing mucha request {request.content.getvalue()}" + ) + return b"RESULTS=000" req = MuchaAuthRequest(req_dict) - self.logger.info(f"Mucha request {vars(req)}") - resp = MuchaAuthResponse(mucha_url=f"{self.config.mucha.hostname}:{self.config.mucha.port}") - self.logger.info(f"Mucha response {vars(resp)}") + self.logger.info(f"Boardauth request from {client_ip} for {req.gameVer}") + self.logger.debug(f"Mucha request {vars(req)}") + + if req.gameCd not in self.mucha_registry: + self.logger.warning(f"Unknown gameCd {req.gameCd}") + return b"RESULTS=000" + + # TODO: Decrypt S/N + b_key = b"" + for x in range(8): + b_key += req.sendDate[(x - 1) & 7].encode() + + cipher = Blowfish.new(b_key, Blowfish.MODE_ECB) + sn_decrypt = cipher.decrypt(bytes.fromhex(req.serialNum)) + self.logger.debug(f"Decrypt SN to {sn_decrypt.hex()}") + + resp = MuchaAuthResponse( + f"{self.config.mucha.hostname}{':' + str(self.config.allnet.port) if self.config.server.is_develop else ''}" + ) + + self.logger.debug(f"Mucha response {vars(resp)}") return self.mucha_postprocess(vars(resp)) - - def handle_updatecheck(self, request: Request) -> bytes: + + def handle_updatecheck(self, request: Request, _: Dict) -> bytes: req_dict = self.mucha_preprocess(request.content.getvalue()) + client_ip = Utils.get_ip_addr(request) + if req_dict is None: - self.logger.error(f"Error processing mucha request {request.content.getvalue()}") - return b"" + self.logger.error( + f"Error processing mucha request {request.content.getvalue()}" + ) + return b"RESULTS=000" req = MuchaUpdateRequest(req_dict) - self.logger.info(f"Mucha request {vars(req)}") - resp = MuchaUpdateResponse(mucha_url=f"{self.config.mucha.hostname}:{self.config.mucha.port}") - self.logger.info(f"Mucha response {vars(resp)}") + self.logger.info(f"Updatecheck request from {client_ip} for {req.gameVer}") + self.logger.debug(f"Mucha request {vars(req)}") + + if req.gameCd not in self.mucha_registry: + self.logger.warning(f"Unknown gameCd {req.gameCd}") + return b"RESULTS=000" + + resp = MuchaUpdateResponse(req.gameVer, f"{self.config.mucha.hostname}{':' + str(self.config.allnet.port) if self.config.server.is_develop else ''}") + + self.logger.debug(f"Mucha response {vars(resp)}") return self.mucha_postprocess(vars(resp)) + def handle_dlstate(self, request: Request, _: Dict) -> bytes: + req_dict = self.mucha_preprocess(request.content.getvalue()) + client_ip = Utils.get_ip_addr(request) + + if req_dict is None: + self.logger.error( + f"Error processing mucha request {request.content.getvalue()}" + ) + return b"" + + req = MuchaDownloadStateRequest(req_dict) + self.logger.info(f"DownloadState request from {client_ip} for {req.gameCd} -> {req.updateVer}") + self.logger.debug(f"request {vars(req)}") + return b"RESULTS=001" + def mucha_preprocess(self, data: bytes) -> Optional[Dict]: try: ret: Dict[str, Any] = {} - - for x in data.decode().split('&'): - kvp = x.split('=') + + for x in data.decode().split("&"): + kvp = x.split("=") if len(kvp) == 2: ret[kvp[0]] = kvp[1] return ret - - except: + + except Exception: self.logger.error(f"Error processing mucha request {data}") return None def mucha_postprocess(self, data: dict) -> Optional[bytes]: try: - urlencode = "" - for k,v in data.items(): - urlencode += f"{k}={v}&" + urlencode = "&".join(f"{k}={v}" for k, v in data.items()) return urlencode.encode() - except: + except Exception: self.logger.error("Error processing mucha response") return None -class MuchaAuthRequest(): - def __init__(self, request: Dict) -> None: - self.gameVer = "" if "gameVer" not in request else request["gameVer"] - self.sendDate = "" if "sendDate" not in request else request["sendDate"] - self.serialNum = "" if "serialNum" not in request else request["serialNum"] - self.gameCd = "" if "gameCd" not in request else request["gameCd"] - self.boardType = "" if "boardType" not in request else request["boardType"] - self.boardId = "" if "boardId" not in request else request["boardId"] - self.placeId = "" if "placeId" not in request else request["placeId"] - self.storeRouterIp = "" if "storeRouterIp" not in request else request["storeRouterIp"] - self.countryCd = "" if "countryCd" not in request else request["countryCd"] - self.useToken = "" if "useToken" not in request else request["useToken"] - self.allToken = "" if "allToken" not in request else request["allToken"] -class MuchaAuthResponse(): - def __init__(self, mucha_url: str = "localhost") -> None: - self.RESULTS = "001" +class MuchaAuthRequest: + def __init__(self, request: Dict) -> None: + # gameCd + boardType + countryCd + version + self.gameVer = request.get("gameVer", "") + self.sendDate = request.get("sendDate", "") # %Y%m%d + self.serialNum = request.get("serialNum", "") + self.gameCd = request.get("gameCd", "") + self.boardType = request.get("boardType", "") + self.boardId = request.get("boardId", "") + self.mac = request.get("mac", "") + self.placeId = request.get("placeId", "") + self.storeRouterIp = request.get("storeRouterIp", "") + self.countryCd = request.get("countryCd", "") + self.useToken = request.get("useToken", "") + self.allToken = request.get("allToken", "") + + +class MuchaAuthResponse: + def __init__(self, mucha_url: str) -> None: + self.RESULTS = "001" self.AUTH_INTERVAL = "86400" self.SERVER_TIME = datetime.strftime(datetime.now(), "%Y%m%d%H%M") self.UTC_SERVER_TIME = datetime.strftime(datetime.now(pytz.UTC), "%Y%m%d%H%M") - self.CHARGE_URL = f"https://{mucha_url}/charge/" + self.CHARGE_URL = f"https://{mucha_url}/charge/" self.FILE_URL = f"https://{mucha_url}/file/" self.URL_1 = f"https://{mucha_url}/url1/" self.URL_2 = f"https://{mucha_url}/url2/" self.URL_3 = f"https://{mucha_url}/url3/" - - self.PLACE_ID = "JPN123" - self.COUNTRY_CD = "JPN" + + self.PLACE_ID = "JPN123" + self.COUNTRY_CD = "JPN" self.SHOP_NAME = "TestShop!" self.SHOP_NICKNAME = "TestShop" self.AREA_0 = "008" @@ -120,7 +189,7 @@ class MuchaAuthResponse(): self.AREA_FULL_1 = "" self.AREA_FULL_2 = "" self.AREA_FULL_3 = "" - + self.SHOP_NAME_EN = "TestShop!" self.SHOP_NICKNAME_EN = "TestShop" self.AREA_0_EN = "008" @@ -132,32 +201,141 @@ class MuchaAuthResponse(): self.AREA_FULL_2_EN = "" self.AREA_FULL_3_EN = "" - self.PREFECTURE_ID = "1" + self.PREFECTURE_ID = "1" self.EXPIRATION_DATE = "null" self.USE_TOKEN = "0" self.CONSUME_TOKEN = "0" self.DONGLE_FLG = "1" self.FORCE_BOOT = "0" -class MuchaUpdateRequest(): - def __init__(self, request: Dict) -> None: - self.gameVer = "" if "gameVer" not in request else request["gameVer"] - self.gameCd = "" if "gameCd" not in request else request["gameCd"] - self.serialNum = "" if "serialNum" not in request else request["serialNum"] - self.countryCd = "" if "countryCd" not in request else request["countryCd"] - self.placeId = "" if "placeId" not in request else request["placeId"] - self.storeRouterIp = "" if "storeRouterIp" not in request else request["storeRouterIp"] -class MuchaUpdateResponse(): - def __init__(self, game_ver: str = "PKFN0JPN01.01", mucha_url: str = "localhost") -> None: - self.RESULTS = "001" +class MuchaUpdateRequest: + def __init__(self, request: Dict) -> None: + self.gameVer = request.get("gameVer", "") + self.gameCd = request.get("gameCd", "") + self.serialNum = request.get("serialNum", "") + self.countryCd = request.get("countryCd", "") + self.placeId = request.get("placeId", "") + self.storeRouterIp = request.get("storeRouterIp", "") + + +class MuchaUpdateResponse: + def __init__(self, game_ver: str, mucha_url: str) -> None: + self.RESULTS = "001" + self.EXE_VER = game_ver + self.UPDATE_VER_1 = game_ver - self.UPDATE_URL_1 = f"https://{mucha_url}/updUrl1/" - self.UPDATE_SIZE_1 = "0" - self.UPDATE_CRC_1 = "0000000000000000" - self.CHECK_URL_1 = f"https://{mucha_url}/checkUrl/" - self.EXE_VER_1 = game_ver + self.UPDATE_URL_1 = f"http://{mucha_url}/updUrl1/" + self.UPDATE_SIZE_1 = "20" + + self.CHECK_CRC_1 = "0000000000000000" + self.CHECK_URL_1 = f"http://{mucha_url}/checkUrl/" + self.CHECK_SIZE_1 = "20" + self.INFO_SIZE_1 = "0" self.COM_SIZE_1 = "0" self.COM_TIME_1 = "0" self.LAN_INFO_SIZE_1 = "0" + + self.USER_ID = "" + self.PASSWORD = "" + +""" +RESULTS +EXE_VER + +UPDATE_VER_%d +UPDATE_URL_%d +UPDATE_SIZE_%d + +CHECK_CRC_%d +CHECK_URL_%d +CHECK_SIZE_%d + +INFO_SIZE_1 +COM_SIZE_1 +COM_TIME_1 +LAN_INFO_SIZE_1 + +USER_ID +PASSWORD +""" +class MuchaUpdateResponseStub: + def __init__(self, game_ver: str) -> None: + self.RESULTS = "001" + self.UPDATE_VER_1 = game_ver + +class MuchaDownloadStateRequest: + def __init__(self, request: Dict) -> None: + self.gameCd = request.get("gameCd", "") + self.updateVer = request.get("updateVer", "") + self.serialNum = request.get("serialNum", "") + self.fileSize = request.get("fileSize", "") + self.compFileSize = request.get("compFileSize", "") + self.boardId = request.get("boardId", "") + self.placeId = request.get("placeId", "") + self.storeRouterIp = request.get("storeRouterIp", "") + +class MuchaDownloadErrorRequest: + def __init__(self, request: Dict) -> None: + self.gameCd = request.get("gameCd", "") + self.updateVer = request.get("updateVer", "") + self.serialNum = request.get("serialNum", "") + self.downloadUrl = request.get("downloadUrl", "") + self.errCd = request.get("errCd", "") + self.errMessage = request.get("errMessage", "") + self.boardId = request.get("boardId", "") + self.placeId = request.get("placeId", "") + self.storeRouterIp = request.get("storeRouterIp", "") + +class MuchaRegiAuthRequest: + def __init__(self, request: Dict) -> None: + self.gameCd = request.get("gameCd", "") + self.serialNum = request.get("serialNum", "") # Encrypted + self.countryCd = request.get("countryCd", "") + self.registrationCd = request.get("registrationCd", "") + self.sendDate = request.get("sendDate", "") + self.useToken = request.get("useToken", "") + self.allToken = request.get("allToken", "") + self.placeId = request.get("placeId", "") + self.storeRouterIp = request.get("storeRouterIp", "") + +class MuchaRegiAuthResponse: + def __init__(self) -> None: + self.RESULTS = "001" # 001 = success, 099, 098, 097 = fail, others = fail + self.ALL_TOKEN = "0" # Encrypted + self.ADD_TOKEN = "0" # Encrypted + +class MuchaTokenStateRequest: + def __init__(self, request: Dict) -> None: + self.gameCd = request.get("gameCd", "") + self.serialNum = request.get("serialNum", "") + self.countryCd = request.get("countryCd", "") + self.useToken = request.get("useToken", "") + self.allToken = request.get("allToken", "") + self.placeId = request.get("placeId", "") + self.storeRouterIp = request.get("storeRouterIp", "") + +class MuchaTokenStateResponse: + def __init__(self) -> None: + self.RESULTS = "001" + +class MuchaTokenMarginStateRequest: + def __init__(self, request: Dict) -> None: + self.gameCd = request.get("gameCd", "") + self.serialNum = request.get("serialNum", "") + self.countryCd = request.get("countryCd", "") + self.placeId = request.get("placeId", "") + self.limitLowerToken = request.get("limitLowerToken", 0) + self.limitUpperToken = request.get("limitUpperToken", 0) + self.settlementMonth = request.get("settlementMonth", 0) + +class MuchaTokenMarginStateResponse: + def __init__(self) -> None: + self.RESULTS = "001" + self.LIMIT_LOWER_TOKEN = 0 + self.LIMIT_UPPER_TOKEN = 0 + self.LAST_SETTLEMENT_MONTH = 0 + self.LAST_LIMIT_LOWER_TOKEN = 0 + self.LAST_LIMIT_UPPER_TOKEN = 0 + self.SETTLEMENT_MONTH = 0 diff --git a/core/title.py b/core/title.py index dd47ec8..3fdb30c 100644 --- a/core/title.py +++ b/core/title.py @@ -1,4 +1,4 @@ -from typing import Dict, Any +from typing import Dict, List, Tuple import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler from twisted.web.http import Request @@ -7,68 +7,172 @@ from core.config import CoreConfig from core.data import Data from core.utils import Utils -class TitleServlet(): - def __init__(self, core_cfg: CoreConfig, cfg_folder: str): +class BaseServlet: + def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: + self.core_cfg = core_cfg + self.game_cfg = None + self.logger = logging.getLogger("title") + + @classmethod + def is_game_enabled(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> bool: + """Called during boot to check if a specific game code should load. + + Args: + game_code (str): 4 character game code + core_cfg (CoreConfig): CoreConfig class + cfg_dir (str): Config directory + + Returns: + bool: True if the game is enabled and set to run, False otherwise + + """ + return False + + def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: + """Called during boot to get all matcher endpoints this title servlet handles + + Returns: + Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: A 2-length tuple where offset 0 is GET and offset 1 is POST, + containing a list of 3-length tuples where offset 0 is the name of the function in the handler that should be called, offset 1 + is the matching string, and offset 2 is a dict containing rules for the matcher. + """ + return ( + [("render_GET", "/{game}/{version}/{endpoint}", {'game': R'S...'})], + [("render_POST", "/{game}/{version}/{endpoint}", {'game': R'S...'})] + ) + + def setup(self) -> None: + """Called once during boot, should contain any additional setup the handler must do, such as starting any sub-services + """ + pass + + def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: + """Called any time a request to PowerOn is made to retrieve the url/host strings to be sent back to the game + + Args: + game_code (str): 4 character game code + game_ver (int): version, expressed as an integer by multiplying by 100 (1.10 -> 110) + keychip (str): Keychip serial of the requesting machine, can be used to deliver specific URIs to different machines + + Returns: + Tuple[str, str]: A tuple where offset 0 is the allnet uri field, and offset 1 is the allnet host field + """ + if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80: + return (f"http://{self.core_cfg.title.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_code}/{game_ver}/", "") + + return (f"http://{self.core_cfg.title.hostname}/{game_code}/{game_ver}/", "") + + def get_mucha_info(self, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str]: + """Called once during boot to check if this game is a mucha game + + Args: + core_cfg (CoreConfig): CoreConfig class + 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 + """ + return (False, "") + + def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes: + self.logger.warn(f"{game_code} Does not dispatch POST") + return None + + def render_GET(self, request: Request, game_code: str, matchers: Dict) -> bytes: + self.logger.warn(f"{game_code} Does not dispatch GET") + return None + +class TitleServlet: + title_registry: Dict[str, BaseServlet] = {} + def __init__(self, core_cfg: CoreConfig, cfg_folder: str): super().__init__() self.config = core_cfg self.config_folder = cfg_folder self.data = Data(core_cfg) - self.title_registry: Dict[str, Any] = {} self.logger = logging.getLogger("title") if not hasattr(self.logger, "initialized"): log_fmt_str = "[%(asctime)s] Title | %(levelname)s | %(message)s" - log_fmt = logging.Formatter(log_fmt_str) + log_fmt = logging.Formatter(log_fmt_str) - fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.config.server.log_dir, "title"), when="d", backupCount=10) + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.config.server.log_dir, "title"), + when="d", + backupCount=10, + ) fileHandler.setFormatter(log_fmt) - + consoleHandler = logging.StreamHandler() consoleHandler.setFormatter(log_fmt) self.logger.addHandler(fileHandler) self.logger.addHandler(consoleHandler) - + self.logger.setLevel(core_cfg.title.loglevel) - coloredlogs.install(level=core_cfg.title.loglevel, logger=self.logger, fmt=log_fmt_str) + coloredlogs.install( + level=core_cfg.title.loglevel, logger=self.logger, fmt=log_fmt_str + ) self.logger.initialized = True - + plugins = Utils.get_all_titles() - + for folder, mod in plugins.items(): - if hasattr(mod, "game_codes") and hasattr(mod, "index"): - handler_cls = mod.index(self.config, self.config_folder) - if hasattr(handler_cls, "setup"): - handler_cls.setup() + if hasattr(mod, "game_codes") and hasattr(mod, "index") and hasattr(mod.index, "is_game_enabled"): + should_call_setup = True + game_servlet: BaseServlet = mod.index + game_codes: List[str] = mod.game_codes - for code in mod.game_codes: - self.title_registry[code] = handler_cls - + for code in game_codes: + if game_servlet.is_game_enabled(code, self.config, self.config_folder): + handler_cls = game_servlet(self.config, self.config_folder) + + if hasattr(handler_cls, "setup") and should_call_setup: + handler_cls.setup() + should_call_setup = False + + self.title_registry[code] = handler_cls + else: - self.logger.error(f"{folder} missing game_code or index in __init__.py") - - self.logger.info(f"Serving {len(self.title_registry)} game codes on port {core_cfg.title.port}") + self.logger.error(f"{folder} missing game_code or index in __init__.py, or is_game_enabled in index") + + self.logger.info( + f"Serving {len(self.title_registry)} game codes {'on port ' + str(core_cfg.title.port) if core_cfg.title.port > 0 else ''}" + ) def render_GET(self, request: Request, endpoints: dict) -> bytes: - code = endpoints["game"] - if code not in self.title_registry: - self.logger.warn(f"Unknown game code {code}") + code = endpoints["title"] + subaction = endpoints['subaction'] - index = self.title_registry[code] - if not hasattr(index, "render_GET"): - self.logger.warn(f"{code} does not dispatch GET") + if code not in self.title_registry: + self.logger.warning(f"Unknown game code {code}") + request.setResponseCode(404) return b"" - return index.render_GET(request, endpoints["version"], endpoints["endpoint"]) - + index = self.title_registry[code] + handler = getattr(index, f"{subaction}", None) + if handler is None: + self.logger.error(f"{code} does not have handler for GET subaction {subaction}") + request.setResponseCode(500) + return b"" + + return handler(request, code, endpoints) + def render_POST(self, request: Request, endpoints: dict) -> bytes: - code = endpoints["game"] + code = endpoints["title"] + subaction = endpoints['subaction'] + if code not in self.title_registry: - self.logger.warn(f"Unknown game code {code}") - - index = self.title_registry[code] - if not hasattr(index, "render_POST"): - self.logger.warn(f"{code} does not dispatch POST") + self.logger.warning(f"Unknown game code {code}") + request.setResponseCode(404) return b"" - return index.render_POST(request, endpoints["version"], endpoints["endpoint"]) + index = self.title_registry[code] + handler = getattr(index, f"{subaction}", None) + if handler is None: + self.logger.error(f"{code} does not have handler for POST subaction {subaction}") + request.setResponseCode(500) + return b"" + + endpoints.pop("title") + endpoints.pop("subaction") + return handler(request, code, endpoints) diff --git a/core/utils.py b/core/utils.py index a14f9ff..8264213 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,22 +1,97 @@ -from typing import Dict, List, Any, Optional +from typing import Dict, Any, Optional from types import ModuleType -import zlib, base64 +from twisted.web.http import Request +import logging import importlib from os import walk +import jwt +from base64 import b64decode +from datetime import datetime, timezone + +from .config import CoreConfig class Utils: + real_title_port = None + real_title_port_ssl = None @classmethod def get_all_titles(cls) -> Dict[str, ModuleType]: ret: Dict[str, Any] = {} for root, dirs, files in walk("titles"): - for dir in dirs: + for dir in dirs: if not dir.startswith("__"): try: mod = importlib.import_module(f"titles.{dir}") - ret[dir] = mod + if hasattr(mod, "game_codes") and hasattr( + mod, "index" + ): # Minimum required to function + ret[dir] = mod except ImportError as e: - print(f"{dir} - {e}") + logging.getLogger("core").error(f"get_all_titles: {dir} - {e}") raise return ret + + @classmethod + def get_ip_addr(cls, req: Request) -> str: + return ( + req.getAllHeaders()[b"x-forwarded-for"].decode() + if b"x-forwarded-for" in req.getAllHeaders() + else req.getClientAddress().host + ) + + @classmethod + def get_title_port(cls, cfg: CoreConfig): + if cls.real_title_port is not None: return cls.real_title_port + + if cfg.title.port == 0: + cls.real_title_port = cfg.allnet.port + + else: + cls.real_title_port = cfg.title.port + + return cls.real_title_port + + @classmethod + def get_title_port_ssl(cls, cfg: CoreConfig): + if cls.real_title_port_ssl is not None: return cls.real_title_port_ssl + + if cfg.title.port_ssl == 0: + cls.real_title_port_ssl = 443 + + else: + cls.real_title_port_ssl = cfg.title.port_ssl + + return cls.real_title_port_ssl + +def create_sega_auth_key(aime_id: int, game: str, place_id: int, keychip_id: str, b64_secret: str, exp_seconds: int = 86400, err_logger: str = 'aimedb') -> Optional[str]: + logger = logging.getLogger(err_logger) + try: + return jwt.encode({ "aime_id": aime_id, "game": game, "place_id": place_id, "keychip_id": keychip_id, "exp": int(datetime.now(tz=timezone.utc).timestamp()) + exp_seconds }, b64decode(b64_secret), algorithm="HS256") + except jwt.InvalidKeyError: + logger.error("Failed to encode Sega Auth Key because the secret is invalid!") + return None + except Exception as e: + logger.error(f"Unknown exception occoured when encoding Sega Auth Key! {e}") + return None + +def decode_sega_auth_key(token: str, b64_secret: str, err_logger: str = 'aimedb') -> Optional[Dict]: + logger = logging.getLogger(err_logger) + try: + return jwt.decode(token, "secret", b64decode(b64_secret), algorithms=["HS256"], options={"verify_signature": True}) + except jwt.ExpiredSignatureError: + logger.error("Sega Auth Key failed to validate due to an expired signature!") + return None + except jwt.InvalidSignatureError: + logger.error("Sega Auth Key failed to validate due to an invalid signature!") + return None + except jwt.DecodeError as e: + logger.error(f"Sega Auth Key failed to decode! {e}") + return None + except jwt.InvalidTokenError as e: + logger.error(f"Sega Auth Key is invalid! {e}") + return None + except Exception as e: + logger.error(f"Unknown exception occoured when decoding Sega Auth Key! {e}") + return None + \ No newline at end of file diff --git a/dbutils.py b/dbutils.py index 8e6024b..85b18a0 100644 --- a/dbutils.py +++ b/dbutils.py @@ -1,47 +1,91 @@ import yaml import argparse +import logging from core.config import CoreConfig from core.data import Data +from os import path, mkdir, access, W_OK -if __name__=='__main__': +if __name__ == "__main__": parser = argparse.ArgumentParser(description="Database utilities") - parser.add_argument("--config", "-c", type=str, help="Config folder to use", default="config") - parser.add_argument("--version", "-v", type=str, help="Version of the database to upgrade/rollback to") - parser.add_argument("--game", "-g", type=str, help="Game code of the game who's schema will be updated/rolled back. Ex. SDFE") - parser.add_argument("action", type=str, help="DB Action, create, recreate, upgrade, or rollback") + parser.add_argument( + "--config", "-c", type=str, help="Config folder to use", default="config" + ) + parser.add_argument( + "--version", + "-v", + type=str, + help="Version of the database to upgrade/rollback to", + ) + parser.add_argument( + "--game", + "-g", + type=str, + help="Game code of the game who's schema will be updated/rolled back. Ex. SDFE", + ) + parser.add_argument("--email", "-e", type=str, help="Email for the new user") + parser.add_argument("--old_ac", "-o", type=str, help="Access code to transfer from") + parser.add_argument("--new_ac", "-n", type=str, help="Access code to transfer to") + parser.add_argument("--force", "-f", type=bool, help="Force the action to happen") + parser.add_argument( + "action", type=str, help="DB Action, create, recreate, upgrade, or rollback" + ) args = parser.parse_args() cfg = CoreConfig() - cfg.update(yaml.safe_load(open(f"{args.config}/core.yaml"))) + if path.exists(f"{args.config}/core.yaml"): + cfg_dict = yaml.safe_load(open(f"{args.config}/core.yaml")) + cfg_dict.get("database", {})["loglevel"] = "info" + cfg.update(cfg_dict) + + if not path.exists(cfg.server.log_dir): + mkdir(cfg.server.log_dir) + + if not access(cfg.server.log_dir, W_OK): + print( + f"Log directory {cfg.server.log_dir} NOT writable, please check permissions" + ) + exit(1) + data = Data(cfg) if args.action == "create": data.create_database() - + elif args.action == "recreate": data.recreate_database() elif args.action == "upgrade" or args.action == "rollback": if args.version is None: - print("Must set game and version to migrate to") - exit(0) + data.logger.warning("No version set, upgrading to latest") if args.game is None: - print("No game set, upgrading core schema") - data.migrate_database("CORE", int(args.version)) + data.logger.warning("No game set, upgrading core schema") + data.migrate_database( + "CORE", + int(args.version) if args.version is not None else None, + args.action, + ) else: - data.migrate_database(args.game, int(args.version), args.action) + data.migrate_database( + args.game, + int(args.version) if args.version is not None else None, + args.action, + ) + + elif args.action == "autoupgrade": + data.autoupgrade() + + elif args.action == "create-owner": + data.create_owner(args.email) + + elif args.action == "migrate-card": + data.migrate_card(args.old_ac, args.new_ac, args.force) + + elif args.action == "cleanup": + data.delete_hanging_users() - elif args.action == "migrate": - print("Migrating from old schema to new schema") - data.restore_from_old_schema() - - elif args.action == "dump": - print("Dumping old schema to migrate to new schema") - data.dump_db() - - elif args.action == "generate": - pass + elif args.action == "version": + data.show_versions() data.logger.info("Done") diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6a35355 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,66 @@ +version: "3.9" +services: + app: + hostname: ma.app + build: . + volumes: + - ./aime:/app/aime + - ./configs/config:/app/config + + environment: + CFG_DEV: 1 + CFG_CORE_SERVER_HOSTNAME: 0.0.0.0 + CFG_CORE_DATABASE_HOST: ma.db + CFG_CORE_MEMCACHED_HOSTNAME: ma.memcached + CFG_CORE_AIMEDB_KEY: + CFG_CHUNI_SERVER_LOGLEVEL: debug + + ports: + - "80:80" + - "8443:8443" + - "22345:22345" + + - "8080:8080" + - "8090:8090" + + depends_on: + db: + condition: service_healthy + + db: + hostname: ma.db + image: yobasystems/alpine-mariadb:10.11.5 + environment: + MYSQL_DATABASE: aime + MYSQL_USER: aime + MYSQL_PASSWORD: aime + MYSQL_ROOT_PASSWORD: AimeRootPassword + MYSQL_CHARSET: utf8mb4 + MYSQL_COLLATION: utf8mb4_general_ci + ##Note: expose port 3306 to allow read.py importer into database, comment out when not needed + #ports: + # - "3306:3306" + ##Note: uncomment to allow mysql to create a persistent database, leave commented if you want to rebuild database from scratch often + #volumes: + # - ./AimeDB:/var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost", "-pAimeRootPassword"] + timeout: 5s + retries: 5 + + memcached: + hostname: ma.memcached + image: memcached:1.6.22-alpine3.18 + command: [ "memcached", "-m", "1024", "-I", "128m" ] + + phpmyadmin: + hostname: ma.phpmyadmin + image: phpmyadmin:latest + environment: + PMA_HOSTS: ma.db + PMA_USER: root + PMA_PASSWORD: AimeRootPassword + APACHE_PORT: 8080 + ports: + - "9090:8080" + diff --git a/docs/INSTALL_DOCKER.md b/docs/INSTALL_DOCKER.md new file mode 100644 index 0000000..edaf75b --- /dev/null +++ b/docs/INSTALL_DOCKER.md @@ -0,0 +1,246 @@ +# ARTEMiS - Docker Installation Guide + +This step-by-step guide will allow you to install a Contenerized Version of ARTEMiS inside Docker, some steps can be skipped assuming you already have pre-requisite components and modules installed. + +This guide assumes using Debian 12(bookworm-stable) as a Host Operating System for most of packages and modules. + +## Pre-Requisites: + +- Linux-Based Operating System (e.g. Debian, Ubuntu) +- Docker (https://get.docker.com) +- Python 3.9+ +- (optional) Git + +## Install Python3.9+ and Docker + +``` +(if this is a fresh install of the system) +sudo apt update && sudo apt upgrade + +(installs python3 and pip) +sudo apt install python3 python3-pip + +(installs docker) +curl -fsSL https://get.docker.com -o get-docker.sh +sh get-docker.sh + +(optionally install git) +sudo apt install git +``` + +## Get ARTEMiS + +If you installed git, clone into your choice of ARTEMiS git repository, e.g.: +``` +git clone +``` +If not, download the source package, and unpack it to the folder of your choice. + +## Prepare development/home configuration + +To build our Docker setup, first we need to create some folders and copy some files around +- Create 'aime', 'configs', 'AimeDB', and 'logs' folder in ARTEMiS root folder (where all source files exist) +- Inside configs folder, create 'config' folder, and copy all .yaml files from example_config to config (thats all files without nginx_example.conf) +- Edit .yaml files inside configs/config to suit your server needs +- Edit core.yaml inside configs/config: +``` +set server.listen_address: to "0.0.0.0" +set title.hostname: to machine's IP address, e.g. "192.168.x.x", depending on your network, or actual hostname if your configuration is already set for dns resolve +set database.host: to "ma.db" +set database.memcached_host: to "ma.memcached" +set aimedb.key: to "" +``` + +## Running Docker Compose + +After configuring, go to ARTEMiS root folder, and execute: + +``` +docker compose up -d +``` + +("-d" argument means detached or daemon, meaning you will regain control of your terminal and Containers will run in background) + +This will start pulling and building required images from network, after it's done, a development server should be running, with server accessible under machine's IP, frontend with port 8090, and PHPMyAdmin under port 9090. + +- To turn off the server, from ARTEMiS root folder, execute: + +``` +docker compose down +``` + +- If you changed some files around, and don't see your changes applied, execute: + +``` +(turn off the server) +docker compose down +(rebuild) +docker compose build +(turn on) +docker compose up -d +``` + +- If you need to see logs from containers running, execute: + +``` +docker compose logs +``` + +- add '-f' to the end if you want to follow logs. + +## Running commands + +If you need to execute python scripts supplied with the application, use `docker compose exec app python3 + +{% endblock content %} \ No newline at end of file diff --git a/titles/idac/frontend/js/idac_scripts.js b/titles/idac/frontend/js/idac_scripts.js new file mode 100644 index 0000000..111fea6 --- /dev/null +++ b/titles/idac/frontend/js/idac_scripts.js @@ -0,0 +1,10 @@ +$(document).ready(function () { + $('#exportBtn').click(function () { + window.location = "/game/idac/export"; + + // appendAlert('Successfully exported the profile', 'success'); + + // Close the modal on success + $('#export').modal('hide'); + }); +}); \ No newline at end of file diff --git a/titles/idac/index.py b/titles/idac/index.py new file mode 100644 index 0000000..7daedae --- /dev/null +++ b/titles/idac/index.py @@ -0,0 +1,167 @@ +import json +import traceback +import inflection +import yaml +import logging +import coloredlogs + +from os import path +from typing import Dict, List, Tuple +from logging.handlers import TimedRotatingFileHandler +from twisted.web import server +from twisted.web.http import Request +from twisted.internet import reactor, endpoints + +from core.config import CoreConfig +from core.utils import Utils +from titles.idac.base import IDACBase +from titles.idac.season2 import IDACSeason2 +from titles.idac.config import IDACConfig +from titles.idac.const import IDACConstants +from titles.idac.echo import IDACEchoUDP +from titles.idac.matching import IDACMatching + + +class IDACServlet: + def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: + self.core_cfg = core_cfg + self.game_cfg = IDACConfig() + if path.exists(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}"): + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}")) + ) + + self.versions = [ + IDACBase(core_cfg, self.game_cfg), + IDACSeason2(core_cfg, self.game_cfg) + ] + + self.logger = logging.getLogger("idac") + log_fmt_str = "[%(asctime)s] IDAC | %(levelname)s | %(message)s" + log_fmt = logging.Formatter(log_fmt_str) + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.core_cfg.server.log_dir, "idac"), + encoding="utf8", + when="d", + backupCount=10, + ) + + fileHandler.setFormatter(log_fmt) + + consoleHandler = logging.StreamHandler() + consoleHandler.setFormatter(log_fmt) + + self.logger.addHandler(fileHandler) + self.logger.addHandler(consoleHandler) + + self.logger.setLevel(self.game_cfg.server.loglevel) + coloredlogs.install( + level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str + ) + + @classmethod + def is_game_enabled(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> bool: + game_cfg = IDACConfig() + + if path.exists(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}"): + game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}")) + ) + + if not game_cfg.server.enable: + return False + + return True + + def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: + return ( + [], + [("render_POST", "/SDGT/{version}/initiald/{category}/{endpoint}", {})] + ) + + def get_allnet_info( + self, game_code: str, game_ver: int, keychip: str + ) -> Tuple[bool, str, str]: + title_port_int = Utils.get_title_port(self.core_cfg) + t_port = f":{title_port_int}" if title_port_int and not self.core_cfg.server.is_using_proxy else "" + + return ( + f"", + # requires http or else it defaults to https + f"http://{self.core_cfg.title.hostname}{t_port}/{game_code}/{game_ver}/", + ) + + def render_POST(self, request: Request, game_code: int, matchers: Dict) -> bytes: + req_raw = request.content.getvalue() + internal_ver = 0 + version = int(matchers['version']) + category = matchers['category'] + endpoint = matchers['endpoint'] + client_ip = Utils.get_ip_addr(request) + + if version >= 100 and version < 140: # IDAC Season 1 + internal_ver = IDACConstants.VER_IDAC_SEASON_1 + elif version >= 140 and version < 171: # IDAC Season 2 + internal_ver = IDACConstants.VER_IDAC_SEASON_2 + + header_application = self.decode_header(request.getAllHeaders()) + + req_data = json.loads(req_raw) + + self.logger.info(f"v{version} {endpoint} request from {client_ip}") + self.logger.debug(f"Headers: {header_application}") + self.logger.debug(req_data) + + # func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" + func_to_find = "handle_" + func_to_find += f"{category.lower()}_" if not category == "" else "" + func_to_find += f"{endpoint.lower()}_request" + + if not hasattr(self.versions[internal_ver], func_to_find): + self.logger.warning(f"Unhandled v{version} request {endpoint}") + return '{"status_code": "0"}'.encode("utf-8") + + resp = None + try: + handler = getattr(self.versions[internal_ver], func_to_find) + resp = handler(req_data, header_application) + + except Exception as e: + traceback.print_exc() + self.logger.error(f"Error handling v{version} method {endpoint} - {e}") + return '{"status_code": "0"}'.encode("utf-8") + + if resp is None: + resp = {"status_code": "0"} + + self.logger.debug(f"Response {resp}") + return json.dumps(resp, ensure_ascii=False).encode("utf-8") + + + def decode_header(self, data: Dict) -> Dict: + app: str = data[b"application"].decode() + ret = {} + + for x in app.split(", "): + y = x.split("=") + ret[y[0]] = y[1].replace('"', "") + + return ret + + def setup(self): + if self.game_cfg.server.enable: + endpoints.serverFromString( + reactor, + f"tcp:{self.game_cfg.server.matching}:interface={self.core_cfg.server.listen_address}", + ).listen(server.Site(IDACMatching(self.core_cfg, self.game_cfg))) + + reactor.listenUDP( + self.game_cfg.server.echo1, + IDACEchoUDP(self.core_cfg, self.game_cfg, self.game_cfg.server.echo1), + ) + reactor.listenUDP( + self.game_cfg.server.echo2, + IDACEchoUDP(self.core_cfg, self.game_cfg, self.game_cfg.server.echo2), + ) + + self.logger.info(f"Matching listening on {self.game_cfg.server.matching} with echos on {self.game_cfg.server.echo1} and {self.game_cfg.server.echo2}") diff --git a/titles/idac/matching.py b/titles/idac/matching.py new file mode 100644 index 0000000..396eec8 --- /dev/null +++ b/titles/idac/matching.py @@ -0,0 +1,72 @@ +import json +import logging + +from typing import Dict +from twisted.web import resource + +from core import CoreConfig +from titles.idac.season2 import IDACBase +from titles.idac.config import IDACConfig + + +class IDACMatching(resource.Resource): + isLeaf = True + + def __init__(self, cfg: CoreConfig, game_cfg: IDACConfig) -> None: + self.core_config = cfg + self.game_config = game_cfg + self.base = IDACBase(cfg, game_cfg) + self.logger = logging.getLogger("idac") + + self.queue = 0 + + def get_matching_state(self): + if self.queue >= 1: + self.queue -= 1 + return 0 + else: + return 1 + + def render_POST(self, req) -> bytes: + url = req.uri.decode() + req_data = json.loads(req.content.getvalue().decode()) + header_application = self.decode_header(req.getAllHeaders()) + user_id = int(header_application["session"]) + + # self.getMatchingStatus(user_id) + + self.logger.info( + f"IDAC Matching request from {req.getClientIP()}: {url} - {req_data}" + ) + + resp = {"status_code": "0"} + if url == "/regist": + self.queue = self.queue + 1 + elif url == "/status": + if req_data.get("cancel_flag"): + self.queue = self.queue - 1 + self.logger.info( + f"IDAC Matching endpoint {req.getClientIP()} had quited" + ) + + resp = { + "status_code": "0", + # Only IPv4 is supported + "host": self.game_config.server.matching_host, + "port": self.game_config.server.matching_p2p, + "room_name": "INDTA", + "state": 1, + } + + self.logger.debug(f"Response {resp}") + return json.dumps(resp, ensure_ascii=False).encode("utf-8") + + def decode_header(self, data: Dict) -> Dict: + app: str = data[b"application"].decode() + ret = {} + + for x in app.split(", "): + y = x.split("=") + ret[y[0]] = y[1].replace('"', "") + + return ret diff --git a/titles/idac/read.py b/titles/idac/read.py new file mode 100644 index 0000000..8798e9b --- /dev/null +++ b/titles/idac/read.py @@ -0,0 +1,161 @@ +import json +import logging +import os +from typing import Any, Dict, List, Optional + +from read import BaseReader +from core.data import Data +from core.config import CoreConfig +from titles.idac.const import IDACConstants +from titles.idac.database import IDACData +from titles.idac.schema.profile import * +from titles.idac.schema.item import * + + +class IDACReader(BaseReader): + 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.card_data = Data(config).card + self.data = IDACData(config) + + try: + self.logger.info( + f"Start importer for {IDACConstants.game_ver_to_string(version)}" + ) + except IndexError: + self.logger.error(f"Invalid Initial D THE ARCADE version {version}") + exit(1) + + def read(self) -> None: + if self.bin_dir is None and self.opt_dir is None: + self.logger.error( + ( + "To import your profile specify the '--optfolder'", + " path to your idac_profile.json file, exiting", + ) + ) + exit(1) + + if self.opt_dir is not None: + if not os.path.exists(self.opt_dir): + self.logger.error( + f"Path to idac_profile.json does not exist: {self.opt_dir}" + ) + exit(1) + + if os.path.isdir(self.opt_dir): + self.opt_dir = os.path.join(self.opt_dir, "idac_profile.json") + + if not os.path.isfile(self.opt_dir) or self.opt_dir[-5:] != ".json": + self.logger.error( + f"Path to idac_profile.json does not exist: {self.opt_dir}" + ) + exit(1) + + self.read_idac_profile(self.opt_dir) + + def read_idac_profile(self, file_path: str) -> None: + self.logger.info(f"Reading profile from {file_path}...") + + # read it as binary to avoid encoding issues + profile_data: Dict[str, Any] = {} + with open(file_path, "rb") as f: + profile_data = json.loads(f.read().decode("utf-8")) + + if not profile_data: + self.logger.error("Profile could not be parsed, exiting") + exit(1) + + access_code = None + while access_code is None: + access_code = input("Enter your 20 digits access code: ") + if len(access_code) != 20 or not access_code.isdigit(): + access_code = None + self.logger.warning("Invalid access code, please try again.") + + # check if access code already exists, if not create a new profile + user_id = self.card_data.get_user_id_from_card(access_code) + if user_id is None: + choice = input("Access code does not exist, do you want to create a new profile? (Y/n): ") + if choice.lower() == "n": + self.logger.info("Exiting...") + exit(0) + + user_id = self.data.user.create_user() + + if user_id is None: + self.logger.error("Failed to register user!") + user_id = -1 + + else: + card_id = self.data.card.create_card(user_id, access_code) + + if card_id is None: + self.logger.error("Failed to register card!") + user_id = -1 + + if user_id == -1: + self.logger.error("Failed to create profile, exiting") + exit(1) + + # table mapping to insert the data properly + tables = { + "idac_profile": profile, + "idac_profile_config": config, + "idac_profile_avatar": avatar, + "idac_profile_rank": rank, + "idac_profile_stock": stock, + "idac_profile_theory": theory, + "idac_user_car": car, + "idac_user_ticket": ticket, + "idac_user_story": story, + "idac_user_story_episode": episode, + "idac_user_story_episode_difficulty": difficulty, + "idac_user_course": course, + "idac_user_time_trial": trial, + "idac_user_challenge": challenge, + "idac_user_theory_course": theory_course, + "idac_user_theory_partner": theory_partner, + "idac_user_theory_running": theory_running, + "idac_user_vs_info": vs_info, + "idac_user_stamp": stamp, + "idac_user_timetrial_event": timetrial_event, + } + + for name, data_list in profile_data.items(): + # get the SQLAlchemy table object from the name + table = tables.get(name) + if table is None: + self.logger.warning(f"Unknown table {name}, skipping") + continue + + for data in data_list: + # add user to the data + data["user"] = user_id + + # check if the table has a version column + if "version" in table.c: + data["version"] = self.version + + sql = insert(table).values( + **data + ) + + # lol use the profile connection for items, dirty hack + conflict = sql.on_duplicate_key_update(**data) + result = self.data.profile.execute(conflict) + + if result is None: + self.logger.error(f"Failed to insert data into table {name}") + exit(1) + + self.logger.info(f"Inserted data into table {name}") + + self.logger.info("Profile import complete!") diff --git a/titles/idac/schema/item.py b/titles/idac/schema/item.py new file mode 100644 index 0000000..80ee7ba --- /dev/null +++ b/titles/idac/schema/item.py @@ -0,0 +1,983 @@ +from typing import Dict, Optional, List +from sqlalchemy import ( + Table, + Column, + UniqueConstraint, + PrimaryKeyConstraint, + and_, + update, +) +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON +from sqlalchemy.schema import ForeignKey +from sqlalchemy.engine import Row +from sqlalchemy.sql import func, select +from sqlalchemy.dialects.mysql import insert + +from core.data.schema import BaseData, metadata + +car = Table( + "idac_user_car", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("version", Integer, nullable=False), + Column("car_id", Integer), + Column("style_car_id", Integer), + Column("color", Integer), + Column("bureau", Integer), + Column("kana", Integer), + Column("s_no", Integer), + Column("l_no", Integer), + Column("car_flag", Integer), + Column("tune_point", Integer), + Column("tune_level", Integer, server_default="1"), + Column("tune_parts", Integer), + Column("infinity_tune", Integer, server_default="0"), + Column("online_vs_win", Integer, server_default="0"), + Column( + "pickup_seq", Integer, server_default="1" + ), # the order in which the car was picked up + Column( + "purchase_seq", Integer, server_default="1" + ), # the order in which the car was purchased + Column("color_stock_list", String(32)), + Column("color_stock_new_list", String(32)), + Column("parts_stock_list", String(48)), + Column("parts_stock_new_list", String(48)), + Column("parts_set_equip_list", String(48)), + Column("parts_list", JSON), + Column("equip_parts_count", Integer, server_default="0"), + Column("total_car_parts_count", Integer, server_default="0"), + Column("use_count", Integer, server_default="0"), + Column("story_use_count", Integer, server_default="0"), + Column("timetrial_use_count", Integer, server_default="0"), + Column("vs_use_count", Integer, server_default="0"), + Column("net_vs_use_count", Integer, server_default="0"), + Column("theory_use_count", Integer, server_default="0"), + Column("car_mileage", Integer, server_default="0"), + UniqueConstraint("user", "version", "style_car_id", name="idac_user_car_uk"), + mysql_charset="utf8mb4", +) + +ticket = Table( + "idac_user_ticket", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("ticket_id", Integer), + Column("ticket_cnt", Integer), + UniqueConstraint("user", "ticket_id", name="idac_user_ticket_uk"), + mysql_charset="utf8mb4", +) + +story = Table( + "idac_user_story", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("story_type", Integer), + Column("chapter", Integer), + Column("loop_count", Integer, server_default="1"), + UniqueConstraint("user", "chapter", name="idac_user_story_uk"), + mysql_charset="utf8mb4", +) + +episode = Table( + "idac_user_story_episode", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("chapter", Integer), + Column("episode", Integer), + Column("play_status", Integer), + UniqueConstraint("user", "chapter", "episode", name="idac_user_story_episode_uk"), + mysql_charset="utf8mb4", +) + +difficulty = Table( + "idac_user_story_episode_difficulty", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("episode", Integer), + Column("difficulty", Integer), + Column("play_count", Integer), + Column("clear_count", Integer), + Column("play_status", Integer), + Column("play_score", Integer), + UniqueConstraint( + "user", "episode", "difficulty", name="idac_user_story_episode_difficulty_uk" + ), + mysql_charset="utf8mb4", +) + +course = Table( + "idac_user_course", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("course_id", Integer), + Column("run_counts", Integer, server_default="1"), + Column("skill_level_exp", Integer, server_default="0"), + UniqueConstraint("user", "course_id", name="idac_user_course_uk"), + mysql_charset="utf8mb4", +) + +trial = Table( + "idac_user_time_trial", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("version", Integer, nullable=False), + Column("style_car_id", Integer), + Column("course_id", Integer), + Column("eval_id", Integer, server_default="0"), + Column("goal_time", Integer), + Column("section_time_1", Integer), + Column("section_time_2", Integer), + Column("section_time_3", Integer), + Column("section_time_4", Integer), + Column("mission", Integer), + Column("play_dt", TIMESTAMP, server_default=func.now()), + UniqueConstraint( + "user", "version", "course_id", "style_car_id", name="idac_user_time_trial_uk" + ), + mysql_charset="utf8mb4", +) + +challenge = Table( + "idac_user_challenge", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("vs_type", Integer), + Column("play_difficulty", Integer), + Column("cleared_difficulty", Integer), + Column("story_type", Integer), + Column("play_count", Integer, server_default="1"), + Column("weak_difficulty", Integer, server_default="0"), + Column("eval_id", Integer), + Column("advantage", Integer), + Column("sec1_advantage_avg", Integer), + Column("sec2_advantage_avg", Integer), + Column("sec3_advantage_avg", Integer), + Column("sec4_advantage_avg", Integer), + Column("nearby_advantage_rate", Integer), + Column("win_flag", Integer), + Column("result", Integer), + Column("record", Integer), + Column("course_id", Integer), + Column("last_play_course_id", Integer), + Column("style_car_id", Integer), + Column("course_day", Integer), + UniqueConstraint( + "user", "vs_type", "play_difficulty", name="idac_user_challenge_uk" + ), + mysql_charset="utf8mb4", +) + +theory_course = Table( + "idac_user_theory_course", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("course_id", Integer), + Column("max_victory_grade", Integer, server_default="0"), + Column("run_count", Integer, server_default="1"), + Column("powerhouse_lv", Integer), + Column("powerhouse_exp", Integer), + Column("played_powerhouse_lv", Integer), + Column("update_dt", TIMESTAMP, server_default=func.now()), + UniqueConstraint("user", "course_id", name="idac_user_theory_course_uk"), + mysql_charset="utf8mb4", +) + +theory_partner = Table( + "idac_user_theory_partner", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("partner_id", Integer), + Column("fellowship_lv", Integer), + Column("fellowship_exp", Integer), + UniqueConstraint("user", "partner_id", name="idac_user_theory_partner_uk"), + mysql_charset="utf8mb4", +) + +theory_running = Table( + "idac_user_theory_running", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("course_id", Integer), + Column("attack", Integer), + Column("defense", Integer), + Column("safety", Integer), + Column("runaway", Integer), + Column("trick_flag", Integer), + UniqueConstraint("user", "course_id", name="idac_user_theory_running_uk"), + mysql_charset="utf8mb4", +) + +vs_info = Table( + "idac_user_vs_info", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("group_key", String(25)), + Column("win_flg", Integer), + Column("style_car_id", Integer), + Column("course_id", Integer), + Column("course_day", Integer), + Column("players_num", Integer), + Column("winning", Integer), + Column("advantage_1", Integer), + Column("advantage_2", Integer), + Column("advantage_3", Integer), + Column("advantage_4", Integer), + Column("select_course_id", Integer), + Column("select_course_day", Integer), + Column("select_course_random", Integer), + Column("matching_success_sec", Integer), + Column("boost_flag", Integer), + Column("vs_history", Integer), + Column("break_count", Integer), + Column("break_penalty_flag", Integer), + UniqueConstraint("user", "group_key", name="idac_user_vs_info_uk"), + mysql_charset="utf8mb4", +) + +stamp = Table( + "idac_user_stamp", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("m_stamp_event_id", Integer), + Column("select_flag", Integer), + Column("stamp_masu", Integer), + Column("daily_bonus", Integer), + Column("weekly_bonus", Integer), + Column("weekday_bonus", Integer), + Column("weekend_bonus", Integer), + Column("total_bonus", Integer), + Column("day_total_bonus", Integer), + Column("store_battle_bonus", Integer), + Column("story_bonus", Integer), + Column("online_battle_bonus", Integer), + Column("timetrial_bonus", Integer), + Column("fasteststreetlegaltheory_bonus", Integer), + Column("collaboration_bonus", Integer), + Column("add_bonus_daily_flag_1", Integer), + Column("add_bonus_daily_flag_2", Integer), + Column("add_bonus_daily_flag_3", Integer), + Column("create_date_daily", TIMESTAMP, server_default=func.now()), + Column("create_date_weekly", TIMESTAMP, server_default=func.now()), + UniqueConstraint("user", "m_stamp_event_id", name="idac_user_stamp_uk"), + mysql_charset="utf8mb4", +) + +timetrial_event = Table( + "idac_user_timetrial_event", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("timetrial_event_id", Integer), + Column("point", Integer), + UniqueConstraint("user", "timetrial_event_id", name="idac_user_timetrial_event_uk"), + mysql_charset="utf8mb4", +) + + +class IDACItemData(BaseData): + def get_random_user_car(self, aime_id: int, version: int) -> Optional[List[Row]]: + sql = ( + select(car) + .where(and_(car.c.user == aime_id, car.c.version == version)) + .order_by(func.rand()) + .limit(1) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_random_car(self, version: int) -> Optional[List[Row]]: + sql = select(car).where(car.c.version == version).order_by(func.rand()).limit(1) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_car( + self, aime_id: int, version: int, style_car_id: int + ) -> Optional[List[Row]]: + sql = select(car).where( + and_( + car.c.user == aime_id, + car.c.version == version, + car.c.style_car_id == style_car_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_cars( + self, version: int, aime_id: int, only_pickup: bool = False + ) -> Optional[List[Row]]: + if only_pickup: + sql = select(car).where( + and_( + car.c.user == aime_id, + car.c.version == version, + car.c.pickup_seq != 0, + ) + ) + else: + sql = select(car).where( + and_(car.c.user == aime_id, car.c.version == version) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_ticket(self, aime_id: int, ticket_id: int) -> Optional[Row]: + sql = select(ticket).where( + ticket.c.user == aime_id, ticket.c.ticket_id == ticket_id + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_tickets(self, aime_id: int) -> Optional[List[Row]]: + sql = select(ticket).where(ticket.c.user == aime_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_story(self, aime_id: int, chapter_id: int) -> Optional[Row]: + sql = select(story).where( + and_(story.c.user == aime_id, story.c.chapter == chapter_id) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_stories(self, aime_id: int) -> Optional[List[Row]]: + sql = select(story).where(story.c.user == aime_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_story_episodes(self, aime_id: int, chapter_id: int) -> Optional[List[Row]]: + sql = select(episode).where( + and_(episode.c.user == aime_id, episode.c.chapter == chapter_id) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_story_episode(self, aime_id: int, episode_id: int) -> Optional[Row]: + sql = select(episode).where( + and_(episode.c.user == aime_id, episode.c.episode == episode_id) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_story_episode_difficulties( + self, aime_id: int, episode_id: int + ) -> Optional[List[Row]]: + sql = select(difficulty).where( + and_(difficulty.c.user == aime_id, difficulty.c.episode == episode_id) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_courses(self, aime_id: int) -> Optional[List[Row]]: + sql = select(course).where(course.c.user == aime_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_course(self, aime_id: int, course_id: int) -> Optional[Row]: + sql = select(course).where( + and_(course.c.user == aime_id, course.c.course_id == course_id) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_time_trial_courses(self, version: int) -> Optional[List[Row]]: + sql = select(trial.c.course_id).where(trial.c.version == version).distinct() + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_time_trial_user_best_time_by_course_car( + self, version: int, aime_id: int, course_id: int, style_car_id: int + ) -> Optional[Row]: + sql = select(trial).where( + and_( + trial.c.user == aime_id, + trial.c.version == version, + trial.c.course_id == course_id, + trial.c.style_car_id == style_car_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_time_trial_user_best_courses( + self, version: int, aime_id: int + ) -> Optional[List[Row]]: + # get for a given aime_id the best time for each course + subquery = ( + select( + trial.c.version, + func.min(trial.c.goal_time).label("min_goal_time"), + trial.c.course_id, + ) + .where(and_(trial.c.version == version, trial.c.user == aime_id)) + .group_by(trial.c.course_id) + .subquery() + ) + + # now get the full row for each best time + sql = select(trial).where( + and_( + trial.c.version == subquery.c.version, + trial.c.goal_time == subquery.c.min_goal_time, + trial.c.course_id == subquery.c.course_id, + trial.c.user == aime_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_time_trial_best_cars_by_course( + self, version: int, course_id: int, aime_id: Optional[int] = None + ) -> Optional[List[Row]]: + subquery = ( + select( + trial.c.version, + func.min(trial.c.goal_time).label("min_goal_time"), + trial.c.style_car_id, + ) + .where( + and_( + trial.c.version == version, + trial.c.course_id == course_id, + ) + ) + ) + + if aime_id is not None: + subquery = subquery.where(trial.c.user == aime_id) + + subquery = subquery.group_by(trial.c.style_car_id).subquery() + + sql = select(trial).where( + and_( + trial.c.version == subquery.c.version, + trial.c.goal_time == subquery.c.min_goal_time, + trial.c.style_car_id == subquery.c.style_car_id, + trial.c.course_id == course_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_time_trial_ranking_by_course( + self, + version: int, + course_id: int, + style_car_id: Optional[int] = None, + limit: Optional[int] = 10, + ) -> Optional[List[Row]]: + # get the top 10 ranking by goal_time for a given course which is grouped by user + subquery = select( + trial.c.version, + trial.c.user, + func.min(trial.c.goal_time).label("min_goal_time"), + ).where(and_(trial.c.version == version, trial.c.course_id == course_id)) + + # if wantd filter only by style_car_id + if style_car_id is not None: + subquery = subquery.where(trial.c.style_car_id == style_car_id) + + subquery = subquery.group_by(trial.c.user).subquery() + + sql = ( + select(trial) + .where( + and_( + trial.c.version == subquery.c.version, + trial.c.user == subquery.c.user, + trial.c.goal_time == subquery.c.min_goal_time, + ), + ) + .order_by(trial.c.goal_time) + ) + + # limit the result if needed + if limit is not None: + sql = sql.limit(limit) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_time_trial_best_ranking_by_course( + self, version: int, aime_id: int, course_id: int + ) -> Optional[Row]: + sql = ( + select(trial) + .where( + and_( + trial.c.version == version, + trial.c.user == aime_id, + trial.c.course_id == course_id, + ), + ) + .order_by(trial.c.goal_time) + .limit(1) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_challenge( + self, aime_id: int, vs_type: int, play_difficulty: int + ) -> Optional[Row]: + sql = select(challenge).where( + and_( + challenge.c.user == aime_id, + challenge.c.vs_type == vs_type, + challenge.c.play_difficulty == play_difficulty, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_challenges(self, aime_id: int) -> Optional[List[Row]]: + sql = select(challenge).where(challenge.c.user == aime_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_best_challenges_by_vs_type( + self, aime_id: int, story_type: int = 4 + ) -> Optional[List[Row]]: + subquery = ( + select( + challenge.c.story_type, + challenge.c.user, + challenge.c.vs_type, + func.max(challenge.c.play_difficulty).label("last_play_lv"), + ) + .where( + and_(challenge.c.user == aime_id, challenge.c.story_type == story_type) + ) + .group_by(challenge.c.vs_type) + ) + + sql = ( + select( + challenge.c.story_type, + challenge.c.vs_type, + challenge.c.cleared_difficulty.label("max_clear_lv"), + challenge.c.play_difficulty.label("last_play_lv"), + challenge.c.course_id, + challenge.c.play_count, + ) + .where( + and_( + challenge.c.user == subquery.c.user, + challenge.c.vs_type == subquery.c.vs_type, + challenge.c.play_difficulty == subquery.c.last_play_lv, + ), + ) + .order_by(challenge.c.vs_type) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_theory_courses(self, aime_id: int) -> Optional[List[Row]]: + sql = select(theory_course).where(theory_course.c.user == aime_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_theory_course_by_powerhouse_lv( + self, aime_id: int, course_id: int, powerhouse_lv: int, count: int = 3 + ) -> Optional[List[Row]]: + sql = ( + select(theory_course) + .where( + and_( + theory_course.c.user != aime_id, + theory_course.c.course_id == course_id, + theory_course.c.powerhouse_lv == powerhouse_lv, + ) + ) + .order_by(func.rand()) + .limit(count) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_theory_course(self, aime_id: int, course_id: int) -> Optional[List[Row]]: + sql = select(theory_course).where( + and_( + theory_course.c.user == aime_id, theory_course.c.course_id == course_id + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_theory_partners(self, aime_id: int) -> Optional[List[Row]]: + sql = select(theory_partner).where(theory_partner.c.user == aime_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_theory_running(self, aime_id: int) -> Optional[List[Row]]: + sql = select(theory_running).where(theory_running.c.user == aime_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_theory_running_by_course( + self, aime_id: int, course_id: int + ) -> Optional[Row]: + sql = select(theory_running).where( + and_( + theory_running.c.user == aime_id, + theory_running.c.course_id == course_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_vs_infos(self, aime_id: int) -> Optional[List[Row]]: + sql = select(vs_info).where(vs_info.c.user == aime_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_stamps(self, aime_id: int) -> Optional[List[Row]]: + sql = select(stamp).where( + and_( + stamp.c.user == aime_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_timetrial_event(self, aime_id: int, timetrial_event_id: int) -> Optional[Row]: + sql = select(timetrial_event).where( + and_( + timetrial_event.c.user == aime_id, + timetrial_event.c.timetrial_event_id == timetrial_event_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def put_car(self, aime_id: int, version: int, car_data: Dict) -> Optional[int]: + car_data["user"] = aime_id + car_data["version"] = version + + sql = insert(car).values(**car_data) + conflict = sql.on_duplicate_key_update(**car_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_car: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_ticket(self, aime_id: int, ticket_data: Dict) -> Optional[int]: + ticket_data["user"] = aime_id + + sql = insert(ticket).values(**ticket_data) + conflict = sql.on_duplicate_key_update(**ticket_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_ticket: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_story(self, aime_id: int, story_data: Dict) -> Optional[int]: + story_data["user"] = aime_id + + sql = insert(story).values(**story_data) + conflict = sql.on_duplicate_key_update(**story_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_story: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_story_episode_play_status( + self, aime_id: int, chapter_id: int, play_status: int = 1 + ) -> Optional[int]: + sql = ( + update(episode) + .where(and_(episode.c.user == aime_id, episode.c.chapter == chapter_id)) + .values(play_status=play_status) + ) + + result = self.execute(sql) + if result is None: + self.logger.warn( + f"put_story_episode_play_status: Failed to update! aime_id: {aime_id}" + ) + return None + return result.lastrowid + + def put_story_episode( + self, aime_id: int, chapter_id: int, episode_data: Dict + ) -> Optional[int]: + episode_data["user"] = aime_id + episode_data["chapter"] = chapter_id + + sql = insert(episode).values(**episode_data) + conflict = sql.on_duplicate_key_update(**episode_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_story_episode: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_story_episode_difficulty( + self, aime_id: int, episode_id: int, difficulty_data: Dict + ) -> Optional[int]: + difficulty_data["user"] = aime_id + difficulty_data["episode"] = episode_id + + sql = insert(difficulty).values(**difficulty_data) + conflict = sql.on_duplicate_key_update(**difficulty_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn( + f"put_story_episode_difficulty: Failed to update! aime_id: {aime_id}" + ) + return None + return result.lastrowid + + def put_course(self, aime_id: int, course_data: Dict) -> Optional[int]: + course_data["user"] = aime_id + + sql = insert(course).values(**course_data) + conflict = sql.on_duplicate_key_update(**course_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_course: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_time_trial( + self, version: int, aime_id: int, time_trial_data: Dict + ) -> Optional[int]: + time_trial_data["user"] = aime_id + time_trial_data["version"] = version + + sql = insert(trial).values(**time_trial_data) + conflict = sql.on_duplicate_key_update(**time_trial_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_time_trial: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_challenge(self, aime_id: int, challenge_data: Dict) -> Optional[int]: + challenge_data["user"] = aime_id + + sql = insert(challenge).values(**challenge_data) + conflict = sql.on_duplicate_key_update(**challenge_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_challenge: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_theory_course( + self, aime_id: int, theory_course_data: Dict + ) -> Optional[int]: + theory_course_data["user"] = aime_id + + sql = insert(theory_course).values(**theory_course_data) + conflict = sql.on_duplicate_key_update(**theory_course_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_theory_course: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_theory_partner( + self, aime_id: int, theory_partner_data: Dict + ) -> Optional[int]: + theory_partner_data["user"] = aime_id + + sql = insert(theory_partner).values(**theory_partner_data) + conflict = sql.on_duplicate_key_update(**theory_partner_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn( + f"put_theory_partner: Failed to update! aime_id: {aime_id}" + ) + return None + return result.lastrowid + + def put_theory_running( + self, aime_id: int, theory_running_data: Dict + ) -> Optional[int]: + theory_running_data["user"] = aime_id + + sql = insert(theory_running).values(**theory_running_data) + conflict = sql.on_duplicate_key_update(**theory_running_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn( + f"put_theory_running: Failed to update! aime_id: {aime_id}" + ) + return None + return result.lastrowid + + def put_vs_info(self, aime_id: int, vs_info_data: Dict) -> Optional[int]: + vs_info_data["user"] = aime_id + + sql = insert(vs_info).values(**vs_info_data) + conflict = sql.on_duplicate_key_update(**vs_info_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_vs_info: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_stamp( + self, aime_id: int, stamp_data: Dict + ) -> Optional[int]: + stamp_data["user"] = aime_id + + sql = insert(stamp).values(**stamp_data) + conflict = sql.on_duplicate_key_update(**stamp_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn( + f"putstamp: Failed to update! aime_id: {aime_id}" + ) + return None + return result.lastrowid + + def put_timetrial_event( + self, aime_id: int, time_trial_event_id: int, point: int + ) -> Optional[int]: + timetrial_event_data = { + "user": aime_id, + "timetrial_event_id": time_trial_event_id, + "point": point, + } + + sql = insert(timetrial_event).values(**timetrial_event_data) + conflict = sql.on_duplicate_key_update(**timetrial_event_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn( + f"put_timetrial_event: Failed to update! aime_id: {aime_id}" + ) + return None + return result.lastrowid diff --git a/titles/idac/schema/profile.py b/titles/idac/schema/profile.py new file mode 100644 index 0000000..5e363ca --- /dev/null +++ b/titles/idac/schema/profile.py @@ -0,0 +1,440 @@ +from typing import Dict, List, Optional +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, BigInteger +from sqlalchemy.engine.base import Connection +from sqlalchemy.schema import ForeignKey +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 core.config import CoreConfig + +profile = Table( + "idac_profile", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("version", Integer, nullable=False), + Column("username", String(8)), + Column("country", Integer), + Column("store", Integer), + Column("team_id", Integer, server_default="0"), + Column("total_play", Integer, server_default="0"), + Column("daily_play", Integer, server_default="0"), + Column("day_play", Integer, server_default="0"), + Column("mileage", Integer, server_default="0"), + Column("asset_version", Integer, server_default="1"), + Column("last_play_date", TIMESTAMP, server_default=func.now()), + Column("mytitle_id", Integer, server_default="0"), + Column("mytitle_efffect_id", Integer, server_default="0"), + Column("sticker_id", Integer, server_default="0"), + Column("sticker_effect_id", Integer, server_default="0"), + Column("papercup_id", Integer, server_default="0"), + Column("tachometer_id", Integer, server_default="0"), + Column("aura_id", Integer, server_default="0"), + Column("aura_color_id", Integer, server_default="0"), + Column("aura_line_id", Integer, server_default="0"), + Column("bgm_id", Integer, server_default="0"), + Column("keyholder_id", Integer, server_default="0"), + Column("start_menu_bg_id", Integer, server_default="0"), + Column("use_car_id", Integer, server_default="1"), + Column("use_style_car_id", Integer, server_default="1"), + Column("bothwin_count", Integer, server_default="0"), + Column("bothwin_score", Integer, server_default="0"), + Column("subcard_count", Integer, server_default="0"), + Column("vs_history", Integer, server_default="0"), + Column("stamp_key_assign_0", Integer), + Column("stamp_key_assign_1", Integer), + Column("stamp_key_assign_2", Integer), + Column("stamp_key_assign_3", Integer), + Column("name_change_category", Integer, server_default="0"), + Column("factory_disp", Integer, server_default="0"), + Column("create_date", TIMESTAMP, server_default=func.now()), + Column("cash", Integer, server_default="0"), + Column("dressup_point", Integer, server_default="0"), + Column("avatar_point", Integer, server_default="0"), + Column("total_cash", Integer, server_default="0"), + UniqueConstraint("user", "version", name="idac_profile_uk"), + mysql_charset="utf8mb4", +) + +# No point setting defaults since the game sends everything on profile creation anyway +config = Table( + "idac_profile_config", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("config_id", Integer), + Column("steering_intensity", Integer), + Column("transmission_type", Integer), + Column("default_viewpoint", Integer), + Column("favorite_bgm", Integer), + Column("bgm_volume", Integer), + Column("se_volume", Integer), + Column("master_volume", Integer), + Column("store_battle_policy", Integer), + Column("battle_onomatope_display", Integer), + Column("cornering_guide", Integer), + Column("minimap", Integer), + Column("line_guide", Integer), + Column("ghost", Integer), + Column("race_exit", Integer), + Column("result_skip", Integer), + Column("stamp_select_skip", Integer), + UniqueConstraint("user", name="idac_profile_config_uk"), + mysql_charset="utf8mb4", +) + +# No point setting defaults since the game sends everything on profile creation anyway +avatar = Table( + "idac_profile_avatar", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("sex", Integer), + Column("face", Integer), + Column("eye", Integer), + Column("mouth", Integer), + Column("hair", Integer), + Column("glasses", Integer), + Column("face_accessory", Integer), + Column("body", Integer), + Column("body_accessory", Integer), + Column("behind", Integer), + Column("bg", Integer), + Column("effect", Integer), + Column("special", Integer), + UniqueConstraint("user", name="idac_profile_avatar_uk"), + mysql_charset="utf8mb4", +) + +rank = Table( + "idac_profile_rank", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("version", Integer, nullable=False), + Column("story_rank_exp", Integer, server_default="0"), + Column("story_rank", Integer, server_default="1"), + Column("time_trial_rank_exp", Integer, server_default="0"), + Column("time_trial_rank", Integer, server_default="1"), + Column("online_battle_rank_exp", Integer, server_default="0"), + Column("online_battle_rank", Integer, server_default="1"), + Column("store_battle_rank_exp", Integer, server_default="0"), + Column("store_battle_rank", Integer, server_default="1"), + Column("theory_exp", Integer, server_default="0"), + Column("theory_rank", Integer, server_default="1"), + Column("pride_group_id", Integer, server_default="0"), + Column("pride_point", Integer, server_default="0"), + Column("grade_exp", Integer, server_default="0"), + Column("grade", Integer, server_default="1"), + Column("grade_reward_dist", Integer, server_default="0"), + Column("story_rank_reward_dist", Integer, server_default="0"), + Column("time_trial_rank_reward_dist", Integer, server_default="0"), + Column("online_battle_rank_reward_dist", Integer, server_default="0"), + Column("store_battle_rank_reward_dist", Integer, server_default="0"), + Column("theory_rank_reward_dist", Integer, server_default="0"), + Column("max_attained_online_battle_rank", Integer, server_default="1"), + Column("max_attained_pride_point", Integer, server_default="0"), + Column("is_last_max", Integer, server_default="0"), + UniqueConstraint("user", "version", name="idac_profile_rank_uk"), + mysql_charset="utf8mb4", +) + +stock = Table( + "idac_profile_stock", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("version", Integer, nullable=False), + Column("mytitle_list", String(1024), server_default=""), + Column("mytitle_new_list", String(1024), server_default=""), + Column("avatar_face_list", String(255), server_default=""), + Column("avatar_face_new_list", String(255), server_default=""), + Column("avatar_eye_list", String(255), server_default=""), + Column("avatar_eye_new_list", String(255), server_default=""), + Column("avatar_hair_list", String(255), server_default=""), + Column("avatar_hair_new_list", String(255), server_default=""), + Column("avatar_body_list", String(255), server_default=""), + Column("avatar_body_new_list", String(255), server_default=""), + Column("avatar_mouth_list", String(255), server_default=""), + Column("avatar_mouth_new_list", String(255), server_default=""), + Column("avatar_glasses_list", String(255), server_default=""), + Column("avatar_glasses_new_list", String(255), server_default=""), + Column("avatar_face_accessory_list", String(255), server_default=""), + Column("avatar_face_accessory_new_list", String(255), server_default=""), + Column("avatar_body_accessory_list", String(255), server_default=""), + Column("avatar_body_accessory_new_list", String(255), server_default=""), + Column("avatar_behind_list", String(255), server_default=""), + Column("avatar_behind_new_list", String(255), server_default=""), + Column("avatar_bg_list", String(255), server_default=""), + Column("avatar_bg_new_list", String(255), server_default=""), + Column("avatar_effect_list", String(255), server_default=""), + Column("avatar_effect_new_list", String(255), server_default=""), + Column("avatar_special_list", String(255), server_default=""), + Column("avatar_special_new_list", String(255), server_default=""), + Column("stamp_list", String(255), server_default=""), + Column("stamp_new_list", String(255), server_default=""), + Column("keyholder_list", String(256), server_default=""), + Column("keyholder_new_list", String(256), server_default=""), + Column("papercup_list", String(255), server_default=""), + Column("papercup_new_list", String(255), server_default=""), + Column("tachometer_list", String(255), server_default=""), + Column("tachometer_new_list", String(255), server_default=""), + Column("aura_list", String(255), server_default=""), + Column("aura_new_list", String(255), server_default=""), + Column("aura_color_list", String(255), server_default=""), + Column("aura_color_new_list", String(255), server_default=""), + Column("aura_line_list", String(255), server_default=""), + Column("aura_line_new_list", String(255), server_default=""), + Column("bgm_list", String(255), server_default=""), + Column("bgm_new_list", String(255), server_default=""), + Column("dx_color_list", String(255), server_default=""), + Column("dx_color_new_list", String(255), server_default=""), + Column("start_menu_bg_list", String(255), server_default=""), + Column("start_menu_bg_new_list", String(255), server_default=""), + Column("under_neon_list", String(255), server_default=""), + UniqueConstraint("user", "version", name="idac_profile_stock_uk"), + mysql_charset="utf8mb4", +) + +theory = Table( + "idac_profile_theory", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("version", Integer, nullable=False), + Column("play_count", Integer, server_default="0"), + Column("play_count_multi", Integer, server_default="0"), + Column("partner_id", Integer), + Column("partner_progress", Integer), + Column("partner_progress_score", Integer), + Column("practice_start_rank", Integer, server_default="0"), + Column("general_flag", Integer, server_default="0"), + Column("vs_history", Integer, server_default="0"), + Column("vs_history_multi", Integer, server_default="0"), + Column("win_count", Integer, server_default="0"), + Column("win_count_multi", Integer, server_default="0"), + UniqueConstraint("user", "version", name="idac_profile_theory_uk"), + mysql_charset="utf8mb4", +) + + +class IDACProfileData(BaseData): + def __init__(self, cfg: CoreConfig, conn: Connection) -> None: + super().__init__(cfg, conn) + self.date_time_format_ext = ( + "%Y-%m-%d %H:%M:%S.%f" # needs to be lopped off at [:-5] + ) + self.date_time_format_short = "%Y-%m-%d" + + def get_profile(self, aime_id: int, version: int) -> Optional[Row]: + sql = select(profile).where( + and_( + profile.c.user == aime_id, + profile.c.version == version, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_different_random_profiles( + self, aime_id: int, version: int, count: int = 9 + ) -> Optional[Row]: + sql = ( + select(profile) + .where( + and_( + profile.c.user != aime_id, + profile.c.version == version, + ) + ) + .order_by(func.rand()) + .limit(count) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_profile_config(self, aime_id: int) -> Optional[Row]: + sql = select(config).where( + and_( + config.c.user == aime_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_profile_avatar(self, aime_id: int) -> Optional[Row]: + sql = select(avatar).where( + and_( + avatar.c.user == aime_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_profile_rank(self, aime_id: int, version: int) -> Optional[Row]: + sql = select(rank).where( + and_( + rank.c.user == aime_id, + rank.c.version == version, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_profile_stock(self, aime_id: int, version: int) -> Optional[Row]: + sql = select(stock).where( + and_( + stock.c.user == aime_id, + stock.c.version == version, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_profile_theory(self, aime_id: int, version: int) -> Optional[Row]: + sql = select(theory).where( + and_( + theory.c.user == aime_id, + theory.c.version == version, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def put_profile( + self, aime_id: int, version: int, profile_data: Dict + ) -> Optional[int]: + profile_data["user"] = aime_id + profile_data["version"] = version + + sql = insert(profile).values(**profile_data) + conflict = sql.on_duplicate_key_update(**profile_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_profile: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_profile_config(self, aime_id: int, config_data: Dict) -> Optional[int]: + config_data["user"] = aime_id + + sql = insert(config).values(**config_data) + conflict = sql.on_duplicate_key_update(**config_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn( + f"put_profile_config: Failed to update! aime_id: {aime_id}" + ) + return None + return result.lastrowid + + def put_profile_avatar(self, aime_id: int, avatar_data: Dict) -> Optional[int]: + avatar_data["user"] = aime_id + + sql = insert(avatar).values(**avatar_data) + conflict = sql.on_duplicate_key_update(**avatar_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn( + f"put_profile_avatar: Failed to update! aime_id: {aime_id}" + ) + return None + return result.lastrowid + + def put_profile_rank( + self, aime_id: int, version: int, rank_data: Dict + ) -> Optional[int]: + rank_data["user"] = aime_id + rank_data["version"] = version + + sql = insert(rank).values(**rank_data) + conflict = sql.on_duplicate_key_update(**rank_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_profile_rank: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_profile_stock( + self, aime_id: int, version: int, stock_data: Dict + ) -> Optional[int]: + stock_data["user"] = aime_id + stock_data["version"] = version + + sql = insert(stock).values(**stock_data) + conflict = sql.on_duplicate_key_update(**stock_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_profile_stock: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_profile_theory( + self, aime_id: int, version: int, theory_data: Dict + ) -> Optional[int]: + theory_data["user"] = aime_id + theory_data["version"] = version + + sql = insert(theory).values(**theory_data) + conflict = sql.on_duplicate_key_update(**theory_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn( + f"put_profile_theory: Failed to update! aime_id: {aime_id}" + ) + return None + return result.lastrowid diff --git a/titles/idac/season2.py b/titles/idac/season2.py new file mode 100644 index 0000000..ca57392 --- /dev/null +++ b/titles/idac/season2.py @@ -0,0 +1,2505 @@ +from datetime import datetime, timedelta +import os +from random import choice +from typing import Any, Dict, List +import json +import logging + +from core.config import CoreConfig +from titles.idac.const import IDACConstants +from titles.idac.config import IDACConfig +from titles.idac.base import IDACBase + + +class IDACSeason2(IDACBase): + def __init__(self, core_cfg: CoreConfig, game_cfg: IDACConfig) -> None: + super().__init__(core_cfg, game_cfg) + self.version = IDACConstants.VER_IDAC_SEASON_2 + + # load the play stamps and timetrial events into memory + self.stamp_info = [] + if self.game_config.stamp.enable: + for stamp in self.game_config.stamp.enabled_stamps: + if not os.path.exists(f"./titles/idac/data/stamps/{stamp}.json"): + self.logger.warning(f"Stamp {stamp} is enabled but does not exist!") + continue + + with open( + f"./titles/idac/data/stamps/{stamp}.json", encoding="UTF-8" + ) as f: + self.logger.debug(f"Loading stamp {stamp}") + self.stamp_info.append(self._fix_dates(json.load(f))) + + self.timetrial_event = {} + self.timetrial_event_id = None + if self.game_config.timetrial.enable: + timetrial = self.game_config.timetrial.enabled_timetrial + if timetrial is not None: + if not os.path.exists(f"./titles/idac/data/timetrial/{timetrial}.json"): + self.logger.warning( + f"Timetrial {timetrial} is enabled but does not exist!" + ) + else: + self.logger.debug(f"Loading timetrial {timetrial}") + with open( + f"./titles/idac/data/timetrial/{timetrial}.json", + encoding="UTF-8", + ) as f: + self.timetrial_event = self._fix_dates(json.load(f)) + + # required for saving + self.timetrial_event_id = self.timetrial_event.get( + "timetrial_event_id" + ) + + def handle_alive_get_request(self, data: Dict, headers: Dict): + return { + "status_code": "0", + # 1 = success, 0 = failed + "server_status": 1, + "force_reboot_time": int(datetime.now().timestamp()) - 86400, + } + + def _fix_dates(self, input: dict): + """ + Fix "start_dt" and "end_dt" dates in a JSON file. + """ + output = {} + + self.logger.debug(f"Fixing dates in {type(input)}") + for key, value in input.items(): + if key in {"start_dt", "end_dt"}: + if isinstance(value, str): + value = int(datetime.strptime(value, "%Y-%m-%d").timestamp()) + + output[key] = value + return output + + def handle_boot_getconfigdata_request(self, data: Dict, headers: Dict): + """ + category: + 1 = D Coin + 3 = Car Dressup Token + 5 = Avatar Dressup Token + 6 = Tachometer + 7 = Aura + 8 = Aura Color + 9 = Avatar Face + 10 = Avatar Eye + 11 = Avatar Mouth + 12 = Avatar Hair + 13 = Avatar Glasses + 14 = Avatar Face accessories + 15 = Avatar Body + 18 = Avatar Background + 21 = Chat Stamp + 22 = Keychain + 24 = Title + 25 = FullTune Ticket + 26 = Paper Cup + 27 = BGM + 28 = Drifting Text + 31 = Start Menu BG + 32 = Car Color/Paint + 33 = Aura Level + 34 = FullTune Ticket Fragment + 35 = Underneon Lights + """ + version = headers["device_version"] + ver_str = version.replace(".", "")[:3] + + if self.core_cfg.server.is_develop: + domain_api_game = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDGT/{ver_str}/" + else: + domain_api_game = f"http://{self.core_cfg.title.hostname}/SDGT/{ver_str}/" + + return { + "status_code": "0", + "free_continue_enable": 1, + "free_continue_new": 1, + "free_continue_play": 1, + "difference_time_to_jp": 0, + # has to match the game asset version to show theory of street + "asset_version": "1", + # option version? MV01? + "optional_version": "1", + "disconnect_offset": 0, + "boost_balance_version": "0", + "time_release_number": "0", + "play_stamp_enable": 1, + "play_stamp_bonus_coin": 1, + "gacha_chara_needs": 1, + "both_win_system_control": 1, + "subcard_system_congrol": 1, + "server_maintenance_start_hour": 0, + "server_maintenance_start_minutes": 0, + "server_maintenance_end_hour": 0, + "server_maintenance_end_minutes": 0, + "domain_api_game": domain_api_game, + "domain_matching": f"http://{self.core_cfg.title.hostname}:{self.game_config.server.matching}", + "domain_echo1": f"{self.core_cfg.title.hostname}:{self.game_config.server.echo1}", + "domain_echo2": f"{self.core_cfg.title.hostname}:{self.game_config.server.echo2}", + "domain_ping": f"{self.core_cfg.title.hostname}", + "battle_gift_event_master": [], + "round_event": [ + { + "round_event_id": 30, + "round_event_nm": f"{self.core_cfg.server.name} Event", + "start_dt": int( + datetime.strptime("2023-01-01", "%Y-%m-%d").timestamp() + ), + "end_dt": int( + datetime.strptime("2029-01-01", "%Y-%m-%d").timestamp() + ), + "round_start_rank": 0, + "save_filename": "0", + # https://info-initialdac.sega.jp/1898/ + "vscount": [ + { + "reward_upper_limit": 10, + "reward_lower_limit": 10, + "reward": [{"reward_category": 21, "reward_type": 483}], + }, + { + "reward_upper_limit": 40, + "reward_lower_limit": 40, + "reward": [{"reward_category": 21, "reward_type": 484}], + }, + { + "reward_upper_limit": 80, + "reward_lower_limit": 80, + "reward": [{"reward_category": 22, "reward_type": 516}], + }, + { + "reward_upper_limit": 120, + "reward_lower_limit": 120, + "reward": [{"reward_category": 21, "reward_type": 461}], + }, + { + "reward_upper_limit": 180, + "reward_lower_limit": 180, + "reward": [{"reward_category": 21, "reward_type": 462}], + } + ], + "rank": [], + "point": [], + "playable_course_list": [ + {"course_id": 4, "course_day": 0}, + {"course_id": 4, "course_day": 1}, + {"course_id": 6, "course_day": 0}, + {"course_id": 6, "course_day": 1}, + {"course_id": 8, "course_day": 0}, + {"course_id": 8, "course_day": 1}, + {"course_id": 10, "course_day": 0}, + {"course_id": 10, "course_day": 1}, + {"course_id": 12, "course_day": 0}, + {"course_id": 12, "course_day": 1}, + {"course_id": 14, "course_day": 0}, + {"course_id": 14, "course_day": 1}, + {"course_id": 16, "course_day": 0}, + {"course_id": 16, "course_day": 1}, + {"course_id": 18, "course_day": 0}, + {"course_id": 18, "course_day": 1}, + {"course_id": 20, "course_day": 0}, + {"course_id": 20, "course_day": 1}, + {"course_id": 22, "course_day": 0}, + {"course_id": 22, "course_day": 1}, + {"course_id": 24, "course_day": 0}, + {"course_id": 24, "course_day": 1}, + {"course_id": 26, "course_day": 0}, + {"course_id": 26, "course_day": 1}, + {"course_id": 36, "course_day": 0}, + {"course_id": 36, "course_day": 1}, + {"course_id": 38, "course_day": 0}, + {"course_id": 38, "course_day": 1}, + {"course_id": 40, "course_day": 0}, + {"course_id": 40, "course_day": 1}, + {"course_id": 42, "course_day": 0}, + {"course_id": 42, "course_day": 1}, + {"course_id": 44, "course_day": 0}, + {"course_id": 44, "course_day": 1}, + {"course_id": 46, "course_day": 0}, + {"course_id": 46, "course_day": 1}, + {"course_id": 48, "course_day": 0}, + {"course_id": 48, "course_day": 1}, + {"course_id": 50, "course_day": 0}, + {"course_id": 50, "course_day": 1}, + {"course_id": 52, "course_day": 0}, + {"course_id": 52, "course_day": 1}, + {"course_id": 54, "course_day": 0}, + {"course_id": 54, "course_day": 1}, + {"course_id": 56, "course_day": 0}, + {"course_id": 56, "course_day": 1}, + {"course_id": 58, "course_day": 0}, + {"course_id": 58, "course_day": 1}, + {"course_id": 68, "course_day": 0}, + {"course_id": 68, "course_day": 1}, + {"course_id": 70, "course_day": 0}, + {"course_id": 70, "course_day": 1}, + ], + } + ], + "last_round_event": [], + "last_round_event_ranking": [], + "round_event_exp": [], + "stamp_info": self.stamp_info, + # 0 = use default data, 1+ = server version of timereleasedata response + "timerelease_no": 3, + # 0 = use default data, 1+ = server version of gachadata response + "timerelease_avatar_gacha_no": 3, + "takeover_reward": [], + "subcard_judge": [ + { + "condition_id": 1, + "lower_rank": 0, + "higher_rank": 10, + "condition_start": 2, + "condition_end": 3, + } + ], + "special_promote": [{"counter": 1, "online_rank_id": 1}], + "matching_id": 1, + "matching_group": [ + { + "group_id": 1, + "group_percent": 1, + } + ], + "timetrial_disp_date": int( + datetime.strptime("2023-10-01", "%Y-%m-%d").timestamp() + ), + # price for every car + "buy_car_need_cash": 5000, + # number of buyable shop/customization time limits + "time_extension_limit": 1, + "collabo_id": 0, + "driver_debut_end_date": int( + datetime.strptime("2029-01-01", "%Y-%m-%d").timestamp() + ), + "online_battle_param1": 1, + "online_battle_param2": 1, + "online_battle_param3": 1, + "online_battle_param4": 1, + "online_battle_param5": 1, + "online_battle_param6": 1, + "online_battle_param7": 1, + "online_battle_param8": 1, + "theory_open_version": "1.30", + "theory_close_version": "1.50", + "special_mode_data": { + "start_dt": int( + datetime.strptime("2023-01-01", "%Y-%m-%d").timestamp() + ), + "end_dt": int(datetime.strptime("2029-01-01", "%Y-%m-%d").timestamp()), + "story_type": 4, # touhou special event + }, + "timetrial_event_data": self.timetrial_event, + } + + def handle_boot_bookkeep_request(self, data: Dict, headers: Dict): + pass + + def handle_boot_getgachadata_request(self, data: Dict, headers: Dict): + """ + Reward category types: + 9: Face + 10: Eye + 11: Mouth + 12: Hair + 13: Glasses + 14: Face accessories + 15: Body + 18: Background + """ + + with open("./titles/idac/data/avatarGacha.json", encoding="UTF-8") as f: + avatar_gacha_data = json.load(f) + + # avatar_gacha_data = { + # "status_code": "0", + # "avatar_gacha_data": [ + # { + # "avatar_gacha_id": 0, + # "avatar_gacha_nm": "Standard", + # "gacha_type": 0, + # "save_filename": "0", + # "use_ticket_cnt": 1, + # "start_dt": int( + # datetime.strptime("2019-01-01", "%Y-%m-%d").timestamp() + # ), + # "end_dt": int( + # datetime.strptime("2029-01-01", "%Y-%m-%d").timestamp() + # ), + # "gacha_reward": [ + # { + # "reward_id": 117, + # "reward_type": 118, + # "reward_category": 18, + # "rate": 1000, + # "pickup_flag": 0, + # }, + # ], + # } + # ], + # } + + self.logger.debug( + f'Available avatar gacha items: {len(avatar_gacha_data["avatar_gacha_data"][0]["gacha_reward"])}' + ) + + return avatar_gacha_data + + def handle_boot_gettimereleasedata_request(self, data: Dict, headers: Dict): + """ + timerelease chapter: + 1 = Story: 1, 2, 3, 4, 5, 6, 7, 8, 9, 19 (Chapter 10), (29 Chapter 11 lol?) + 2 = MF Ghost: 10, 11, 12, 13, 14, 15 + 3 = Bunta: 15, 16, 17, 18, 19, 20, (21, 21, 22?) + 4 = Special Event: 23, 24, 25, 26, 27, 28 (Touhou Project) + """ + path = "./titles/idac/data/" + + # 1.00.00 is default + device_version_data = headers.get("device_version", "1.00.00") + device_version = int(device_version_data.replace(".", "")[:-2]) + + timerelease_filename = f"timeRelease_v{device_version:04d}" + timerelease_path = f"{path}{timerelease_filename}.json" + + # if the file doesn't exist, try to find the next lowest version + if not os.path.exists(timerelease_path): + while device_version > 100: + device_version -= 1 + timerelease_filename = f"timeRelease_v{device_version:04d}" + timerelease_path = f"{path}{timerelease_filename}.json" + + # if the file exists, break out of the loop + if os.path.exists(timerelease_path): + break + + self.logger.debug(f"Using time release file: {timerelease_filename}") + # load the time release data + with open(f"{path}{timerelease_filename}.json") as f: + time_release_data = json.load(f) + + return time_release_data + + def handle_advertise_getrankingdata_request(self, data: Dict, headers: Dict): + best_data = [] + for last_update in data.get("last_update_date"): + course_id = last_update.get("course_id") + + ranking = self.data.item.get_time_trial_ranking_by_course( + self.version, course_id + ) + ranking_data = [] + for i, rank in enumerate(ranking): + user_id = rank["user"] + + # get the username, country and store from the profile + profile = self.data.profile.get_profile(user_id, self.version) + arcade = self.data.arcade.get_arcade(profile["store"]) + + if arcade is None: + arcade = {} + arcade["name"] = self.core_cfg.server.name + + # should never happen + if profile is None: + continue + + ranking_data.append( + { + "course_id": course_id, + "rank": i + 1, + "username": profile["username"], + "value": rank["goal_time"], + # gat the store name from the profile + "store": arcade["name"], + # get the country id from the profile, 9 is JPN + "country": profile["country"], + "style_car_id": rank["style_car_id"], + # convert the datetime to a timestamp + "play_dt": int(rank["play_dt"].timestamp()), + "section_time_1": rank["section_time_1"], + "section_time_2": rank["section_time_2"], + "section_time_3": rank["section_time_3"], + "section_time_4": rank["section_time_4"], + "mission": rank["mission"], + } + ) + + best_data.append( + { + "course_id": course_id, + "ranking_data": ranking_data, + } + ) + + return { + "status_code": "0", + "national_best_data": best_data, + "shop_best_data": best_data, + "rank_management_flag": 0, + } + + def handle_login_checklock_request(self, data: Dict, headers: Dict): + user_id = data["id"] + access_code = data["accesscode"] + is_new_player = 0 + + # check that the user_id from access_code matches the user_id + if user_id == self.data.card.get_user_id_from_card(access_code): + lock_result = 1 + + # check if an IDAC profile already exists + p = self.data.profile.get_profile(user_id, self.version) + is_new_player = 1 if p is None else 0 + else: + lock_result = 0 + user_id = "" + + # other: in use + return { + "status_code": "0", + # 0 = already in use, 1 = good, 2 = too new + "lock_result": lock_result, + "lock_date": int(datetime.now().timestamp()), + "daily_play": 1, + "session": f"{user_id}", + "shared_security_key": "a", + "session_procseq": "a", + "new_player": is_new_player, + "server_status": 1, + } + + def handle_login_unlock_request(self, data: Dict, headers: Dict): + return { + "status_code": "0", + "lock_result": 1, + } + + def handle_login_relock_request(self, data: Dict, headers: Dict): + return { + "status_code": "0", + "lock_result": 1, + "lock_date": int(datetime.now().timestamp()), + } + + def handle_login_guestplay_request(self, data: Dict, headers: Dict): + # TODO + pass + + def _generate_story_data(self, user_id: int) -> Dict: + stories = self.data.item.get_stories(user_id) + + story_data = [] + for s in stories: + chapter_id = s["chapter"] + episodes = self.data.item.get_story_episodes(user_id, chapter_id) + + episode_data = [] + for e in episodes: + episode_id = e["episode"] + difficulties = self.data.item.get_story_episode_difficulties( + user_id, episode_id + ) + + difficulty_data = [] + for d in difficulties: + difficulty_data.append( + { + "difficulty": d["difficulty"], + "play_count": d["play_count"], + "clear_count": d["clear_count"], + "play_status": d["play_status"], + "play_score": d["play_score"], + } + ) + + episode_data.append( + { + "episode": e["episode"], + "play_status": e["play_status"], + "difficulty_data": difficulty_data, + } + ) + + story_data.append( + { + "story_type": s["story_type"], + "chapter": s["chapter"], + "loop_count": s["loop_count"], + "episode_data": episode_data, + } + ) + + return story_data + + def _generate_special_data(self, user_id: int) -> Dict: + # 4 = special mode + specials = self.data.item.get_best_challenges_by_vs_type(user_id, story_type=4) + + special_data = [] + for s in specials: + special_data.append( + { + "story_type": s["story_type"], + "vs_type": s["vs_type"], + "max_clear_lv": s["max_clear_lv"], + "last_play_lv": s["last_play_lv"], + # change to last_play_course_id? + "last_play_course_id": s["course_id"], + } + ) + + return special_data + + def _generate_challenge_data(self, user_id: int) -> Dict: + # challenge mode (Bunta challenge only right now) + challenges = self.data.item.get_best_challenges_by_vs_type( + user_id, story_type=3 + ) + + challenge_data = [] + for c in challenges: + challenge_data.append( + { + "story_type": c["story_type"], + "vs_type": c["vs_type"], + "max_clear_lv": c["max_clear_lv"], + "last_play_lv": c["last_play_lv"], + # change to last_play_course_id? + "last_play_course_id": c["course_id"], + "play_count": c["play_count"], + } + ) + + return challenge_data + + def _save_stock_data(self, user_id: int, stock_data: Dict): + updated_stock_data = {} + for k, v in stock_data.items(): + if v != "": + updated_stock_data[k] = v + + if updated_stock_data: + self.data.profile.put_profile_stock( + user_id, self.version, updated_stock_data + ) + + def handle_user_getdata_request(self, data: Dict, headers: Dict): + user_id = int(headers["session"]) + + # get the user's profile, can never be None + p = self.data.profile.get_profile(user_id, self.version) + user_data = p._asdict() + arcade = self.data.arcade.get_arcade(user_data["store"]) + + del user_data["id"] + del user_data["user"] + del user_data["version"] + user_data["id"] = user_id + user_data["store_name"] = ( + self.core_cfg.server.name if arcade is None else arcade["name"] + ) + user_data["last_play_date"] = int(user_data["last_play_date"].timestamp()) + user_data["create_date"] = int(user_data["create_date"].timestamp()) + + # get the user's rank + r = self.data.profile.get_profile_rank(user_id, self.version) + rank_data = r._asdict() + del rank_data["id"] + del rank_data["user"] + del rank_data["version"] + + # add the mode_rank_data to the user_data + user_data["mode_rank_data"] = rank_data + + # get the user's avatar + a = self.data.profile.get_profile_avatar(user_id) + avatar_data = a._asdict() + del avatar_data["id"] + del avatar_data["user"] + + # get the user's stock + s = self.data.profile.get_profile_stock(user_id, self.version) + stock_data = s._asdict() + del stock_data["id"] + del stock_data["user"] + del stock_data["version"] + + # get the user's config + c = self.data.profile.get_profile_config(user_id) + config_data = c._asdict() + del config_data["id"] + del config_data["user"] + config_data["id"] = config_data.pop("config_id") + + # get the user's ticket + tickets: list = self.data.item.get_tickets(user_id) + + """ + ticket_id: + 3 = Car Dressup Points + 5 = Avatar Dressup Points + 25 = Full Tune Tickets + 34 = Full Tune Fragments + """ + + ticket_data = [] + for ticket in tickets: + ticket_data.append( + { + "ticket_id": ticket["ticket_id"], + "ticket_cnt": ticket["ticket_cnt"], + } + ) + + # get the user's course, required for the "course proeficiency" + courses = self.data.item.get_courses(user_id) + course_data = [] + for course in courses: + course_data.append( + { + "id": 0, # no clue, always 0? + "course_id": course["course_id"], + "run_counts": course["run_counts"], + # "course proeficiency" in exp points + "skill_level_exp": course["skill_level_exp"], + } + ) + + # get the profile theory data + theory_data = {} + theory = self.data.profile.get_profile_theory(user_id, self.version) + if theory is not None: + theory_data = theory._asdict() + del theory_data["id"] + del theory_data["user"] + del theory_data["version"] + + # get the users theory course data + theory_course_data = [] + theory_courses = self.data.item.get_theory_courses(user_id) + for course in theory_courses: + tmp = course._asdict() + del tmp["id"] + del tmp["user"] + tmp["update_dt"] = int(tmp["update_dt"].timestamp()) + + theory_course_data.append(tmp) + + # get the users theory partner data + theory_partner_data = [] + theory_partners = self.data.item.get_theory_partners(user_id) + for partner in theory_partners: + tmp = partner._asdict() + del tmp["id"] + del tmp["user"] + + theory_partner_data.append(tmp) + + # get the users theory running pram data + theory_running_pram_data = [] + theory_running = self.data.item.get_theory_running(user_id) + for running in theory_running: + tmp = running._asdict() + del tmp["id"] + del tmp["user"] + + theory_running_pram_data.append(tmp) + + # get the users vs info data + vs_info_data = [] + vs_info = self.data.item.get_vs_infos(user_id) + for vs in vs_info: + vs_info_data.append( + { + "battle_mode": 1, + "vs_cnt": 1, + "vs_win": vs["win_flg"], + "invalid": 0, + "str": 0, + "str_now": 0, + "lose_now": 0, + "vs_history": vs["vs_history"], + "course_select_priority": 0, + "vsinfo_course_data": [ + { + "course_id": vs["course_id"], + "vs_cnt": 1, + "vs_win": vs["win_flg"], + } + ], + } + ) + + # get the user's car + cars = self.data.item.get_cars(self.version, user_id, only_pickup=True) + fulltune_count = 0 + total_car_parts_count = 0 + car_data = [] + for car in cars: + tmp = car._asdict() + del tmp["id"] + del tmp["user"] + del tmp["version"] + + car_data.append(tmp) + # tune_level of 16 means fully tuned, so add 1 to fulltune_count + if car["tune_level"] >= 16: + fulltune_count += 1 + + # add the number of car parts to total_car_parts_count? + # total_car_parts_count += tmp["total_car_parts_count"] + + car_data.append(tmp) + + # update user profile car count + user_data["have_car_cnt"] = len(car_data) + + # get the user's play stamps + stamps = self.data.item.get_stamps(user_id) + stamp_event_data = [] + for stamp in stamps: + tmp = stamp._asdict() + del tmp["id"] + del tmp["user"] + + now = datetime.now() + + # create timestamp for today at 1am + this_day = now.replace(hour=1, minute=0, second=0, microsecond=0) + + # check if this_day is greater than or equal to create_date_daily + if this_day >= tmp["create_date_daily"]: + # reset the daily stamp + tmp["create_date_daily"] = now + tmp["daily_bonus"] = 0 + + # create a timestamp for this monday at 1am + this_monday = now - timedelta(days=now.weekday()) + this_monday = this_monday.replace(hour=1, minute=0, second=0, microsecond=0) + + # check if this_monday is greater than or equal to create_date_weekly + if this_monday >= tmp["create_date_weekly"]: + # reset the weekly stamp + tmp["create_date_weekly"] = now + tmp["weekly_bonus"] = 0 + + # update the play stamp in the database + self.data.item.put_stamp(user_id, tmp) + + del tmp["create_date_daily"] + del tmp["create_date_weekly"] + stamp_event_data.append(tmp) + + # get the user's timetrial event data + timetrial_event_data = {} + timetrial = self.data.item.get_timetrial_event(user_id, self.timetrial_event_id) + if timetrial is not None: + timetrial_event_data = { + "timetrial_event_id": timetrial["timetrial_event_id"], + "point": timetrial["point"], + } + + return { + "status_code": "0", + "user_base_data": user_data, + "avatar_data": avatar_data, + "pick_up_car_data": car_data, + "story_data": self._generate_story_data(user_id), + "vsinfo_data": vs_info_data, + "stock_data": stock_data, + "mission_data": { + "id": 0, + "achieve_flag": 0, + "received_flag": 0, + "update_dt": int(datetime.now().timestamp() - 86400), + }, + "weekly_mission_data": [], + "course_data": course_data, + "toppatu_event_data": { + "id": 0, + "event_id": 0, + "count1": 0, + "count2": 0, + "count3": 0, + "accept_flag": 0, + }, + "event_data": { + "id": 0, + "active_event_id": 0, + "dialog_show_date": int(datetime.now().timestamp() - 86400), + "show_start_dialog_flag": 1, + "show_progress_dialog_flag": 1, + "show_end_dialog_flag": 1, + "end_event_id": 0, + }, + "rewards_data": {}, + "login_bonus_data": { + "gacha_id": 0, + "gacha_item_id": 0, + "category": 0, + "type": 0, + }, + "frozen_data": {"frozen_status": 2}, + "penalty_data": {"penalty_flag": 0, "penalty_2_level": 0}, + "config_data": config_data, + "battle_gift_data": [], + "ticket_data": ticket_data, + "round_event": [], + "last_round_event": [], + "past_round_event": [], + "total_round_point": 0, + "stamp_event_data": stamp_event_data, + "avatar_gacha_lottery_data": {"avatar_gacha_id": 0}, + "fulltune_count": fulltune_count, + "total_car_parts_count": total_car_parts_count, + "car_layout_count": [], + "car_style_count": [], + "car_use_count": [], + "maker_use_count": [], + "story_course": [{"course_id": 0, "count": 1}], + # TODO! + # "driver_debut": { + # "play_count": 137, + # "daily_play": 5, + # "last_play_dt": 0, + # "use_start_date": 0, + # "use_end_date": 0, + # "use_dt": 0, + # "ticket_cnt": 0, + # "ticket_get_bit": 0, + # }, + "theory_data": theory_data, + "theory_course_data": theory_course_data, + "theory_partner_data": theory_partner_data, + "theory_running_pram_data": theory_running_pram_data, + "special_mode_data": self._generate_special_data(user_id), + "challenge_mode_data": self._generate_challenge_data(user_id), + "season_rewards_data": [], + "timetrial_event_data": timetrial_event_data, + "special_mode_hint_data": {"story_type": 0, "hint_display_flag": 0}, + } + + def handle_timetrial_getbestrecordpreta_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + for car_id in data["car_ids"]: + pass + + course_mybest_data = [] + courses = self.data.item.get_time_trial_user_best_courses(self.version, user_id) + for course in courses: + course_mybest_data.append( + { + "course_id": course["course_id"], + # local rank, store rank, worldwide rank? + "rank": 1, + # no clue + "member": 10000, + # goal_time in ms + "value": course["goal_time"], + # total number of entries per course? + "total": 10, + "store": self.core_cfg.server.name, + # use car_id from request? + "car_id": 0, + "style_car_id": course["style_car_id"], + "play_dt": course["play_dt"].timestamp(), + "section_time_1": course["section_time_1"], + "section_time_2": course["section_time_2"], + "section_time_3": course["section_time_3"], + "section_time_4": course["section_time_4"], + # no clue + "mission": course["mission"], + } + ) + + course_pickup_car_best_data = [] + courses = self.data.item.get_time_trial_courses(self.version) + for course in courses: + car_list = [] + best_cars = self.data.item.get_time_trial_best_cars_by_course( + self.version, course["course_id"], user_id + ) + + for i, car in enumerate(best_cars): + car_list.append( + { + "rank": i + 1, + # no clue + "member": user_id, + "value": car["goal_time"], + "store": self.core_cfg.server.name, + # use car_id from request? + "car_id": 0, + "style_car_id": car["style_car_id"], + "play_dt": car["play_dt"].timestamp(), + "section_time_1": car["section_time_1"], + "section_time_2": car["section_time_2"], + "section_time_3": car["section_time_3"], + "section_time_4": car["section_time_4"], + "mission": car["mission"], + } + ) + + course_pickup_car_best_data.append( + { + "course_id": course["course_id"], + "car_list": car_list, + } + ) + + return { + "status_code": "0", + "course_mybest_data": course_mybest_data, + "course_pickup_car_best_data": course_pickup_car_best_data, + } + + def handle_timetrial_getbestrecordprerace_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + course_id = data["course_id"] + for car in data["car_ids"]: + # TODO: get the best record for this car + style_car_id = car["style_car_id"] + + # Not sure if this is actually correct + ranking = self.data.item.get_time_trial_ranking_by_course( + self.version, course_id + ) + course_best_data = [] + for i, rank in enumerate(ranking): + car_user_id = rank["user"] + + # get the username, country and store from the profile + profile = self.data.profile.get_profile(car_user_id, self.version) + arcade = self.data.arcade.get_arcade(profile["store"]) + + if arcade is None: + arcade = {} + arcade["name"] = self.core_cfg.server.name + + # should never happen + if profile is None: + continue + + course_best_data.append( + { + "course_id": course_id, + "rank": i + 1, + "member": car_user_id, + "value": rank["goal_time"], + "store": arcade["name"], + # use car_id from request? + "car_id": 0, + "style_car_id": rank["style_car_id"], + "play_dt": rank["play_dt"].timestamp(), + "section_time_1": rank["section_time_1"], + "section_time_2": rank["section_time_2"], + "section_time_3": rank["section_time_3"], + "section_time_4": rank["section_time_4"], + "mission": rank["mission"], + } + ) + + best_cars = self.data.item.get_time_trial_best_cars_by_course( + self.version, course_id + ) + + car_list = [] + for i, rank in enumerate(best_cars): + car_user_id = rank["user"] + # get the username, country and store from the profile + profile = self.data.profile.get_profile(car_user_id, self.version) + arcade = self.data.arcade.get_arcade(profile["store"]) + + if arcade is None: + arcade = {} + arcade["name"] = self.core_cfg.server.name + + # should never happen + if profile is None: + continue + + car_list.append( + { + "rank": i + 1, + # no clue + "member": car_user_id, + "value": rank["goal_time"], + "store": arcade["name"], + # use car_id from request? + "car_id": 0, + "style_car_id": rank["style_car_id"], + "play_dt": rank["play_dt"].timestamp(), + "section_time_1": rank["section_time_1"], + "section_time_2": rank["section_time_2"], + "section_time_3": rank["section_time_3"], + "section_time_4": rank["section_time_4"], + "mission": rank["mission"], + } + ) + + return { + "status_code": "0", + "course_car_best_data": [{"course_id": course_id, "car_list": car_list}], + "course_best_data": course_best_data, + } + + def handle_user_createaccount_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + car_data: Dict = data.pop("car_obj") + parts_data: List = car_data.pop("parts_list") + avatar_data: Dict = data.pop("avatar_obj") + config_data: Dict = data.pop("config_obj") + + rank_data: Dict = data.pop("mode_rank_data") + stock_data: Dict = data.pop("takeover_stock_obj") + takeover_ticket_list: List = data.pop("takeover_ticket") + + # not required? + use_ticket = data.pop("use_ticket") + + # save profile in database + data["store"] = headers.get("a_store", 0) + data["country"] = headers.get("a_country", 0) + data["asset_version"] = headers.get("asset_version", 1) + self.data.profile.put_profile(user_id, self.version, data) + + # save rank data in database + self.data.profile.put_profile_rank(user_id, self.version, rank_data) + + # save stock data in database + self._save_stock_data(user_id, stock_data) + + # save tickets in database + for ticket in takeover_ticket_list: + self.data.item.put_ticket(user_id, ticket) + + config_data["config_id"] = config_data.pop("id") + self.data.profile.put_profile_config(user_id, config_data) + self.data.profile.put_profile_avatar(user_id, avatar_data) + + # save car data and car parts in database + car_data["parts_list"] = parts_data + self.data.item.put_car(user_id, self.version, car_data) + + return {"status_code": "0"} + + def handle_user_updatelogin_request(self, data: Dict, headers: Dict): + pass + + def handle_timetrial_getcarbest_request(self, data: Dict, headers: Dict): + pass + + def handle_factory_avatargacharesult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + stock_data: Dict = data.pop("stock_obj") + use_ticket_cnt = data["use_ticket_cnt"] + + # save stock data in database + self._save_stock_data(user_id, stock_data) + + # get the user's ticket + tickets: list = self.data.item.get_tickets(user_id) + ticket_list = [] + for ticket in tickets: + # avatar tickets + if ticket["ticket_id"] == 5: + ticket_data = { + "ticket_id": ticket["ticket_id"], + "ticket_cnt": ticket["ticket_cnt"] - use_ticket_cnt, + } + + # update the ticket in the database + self.data.item.put_ticket(user_id, ticket_data) + ticket_list.append(ticket_data) + + continue + + ticket_list.append( + { + "ticket_id": ticket["ticket_id"], + "ticket_cnt": ticket["ticket_cnt"], + } + ) + + return {"status_code": "0", "ticket_data": ticket_list} + + def handle_factory_savefavoritecar_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + # save favorite cars in database + for car in data["pickup_on_car_ids"]: + self.data.item.put_car(user_id, self.version, car) + + for car in data["pickup_off_car_ids"]: + self.data.item.put_car( + user_id, + self.version, + {"style_car_id": car["style_car_id"], "pickup_seq": 0}, + ) + + return {"status_code": "0"} + + def handle_factory_updatemultiplecustomizeresult_request( + self, data: Dict, headers: Dict + ): + user_id = headers["session"] + + car_list = data.pop("car_list") + ticket_data: List = data.pop("ticket_data") + + # unused + total_car_parts_count = data.pop("total_car_parts_count") + + # save tickets in database + for ticket in ticket_data: + self.data.item.put_ticket(user_id, ticket) + + for car in car_list: + # save car data and car parts in database + self.data.item.put_car(user_id, self.version, car) + + return {"status_code": "0"} + + def handle_factory_updatecustomizeresult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + parts_data: List = data.pop("parts_list") + ticket_data: List = data.pop("ticket_data") + + # save tickets in database + for ticket in ticket_data: + self.data.item.put_ticket(user_id, ticket) + + # save car data in database + data["parts_list"] = parts_data + self.data.item.put_car(user_id, self.version, data) + + return {"status_code": "0"} + + def handle_factory_getcardata_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + cars = self.data.item.get_cars(self.version, user_id) + car_data = [] + for car in cars: + tmp = car._asdict() + del tmp["id"] + del tmp["user"] + del tmp["version"] + + car_data.append(tmp) + + return { + "status_code": "0", + "car_data": car_data, + } + + def handle_factory_renamebefore_request(self, data: Dict, headers: Dict): + pass + + def handle_factory_buycarresult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + parts_data: List = data.pop("parts_list") + pickup_on_list: List = data.pop("pickup_on_car_ids") + pickup_off_list: List = data.pop("pickup_off_car_ids") + + style_car_id = data.get("style_car_id") + + # get the pickup_seq for the new car + pickup_seq = 0 + # save favorite cars in database + for car in pickup_on_list: + # if the new car is a favorite get the new pickup_seqn for later + if car["style_car_id"] == style_car_id: + pickup_seq = car["pickup_seq"] + else: + self.data.item.put_car(user_id, self.version, car) + + data["pickup_seq"] = pickup_seq + + cash = data.pop("cash") + total_cash = data.pop("total_cash") + + # save the new cash in database + self.data.profile.put_profile( + user_id, self.version, {"total_cash": total_cash, "cash": cash} + ) + + # full tune ticket + use_ticket = data.pop("use_ticket") + if use_ticket: + # get the user's tickets, full tune ticket id is 25 + ticket = self.data.item.get_ticket(user_id, ticket_id=25) + + # update the ticket in the database + self.data.item.put_ticket( + user_id, + { + "ticket_id": ticket["ticket_id"], + "ticket_cnt": ticket["ticket_cnt"] - 1, + }, + ) + + # also set the tune_level to 16 (fully tuned) + data["tune_level"] = 16 + + # save car data and car parts in database + data["parts_list"] = parts_data + self.data.item.put_car(user_id, self.version, data) + + for car in pickup_off_list: + self.data.item.put_car( + user_id, + self.version, + {"style_car_id": car["style_car_id"], "pickup_seq": 0}, + ) + + # get the user's car + cars = self.data.item.get_cars(self.version, user_id) + fulltune_count = 0 + total_car_parts_count = 0 + for car in cars: + # tune_level of 16 means fully tuned, so add 1 to fulltune_count + if car["tune_level"] >= 16: + fulltune_count += 1 + + # add the number of car parts to total_car_parts_count + # total_car_parts_count += car["total_car_parts_count"] + + # get the user's ticket + tickets = self.data.item.get_tickets(user_id) + ticket_data = [] + for ticket in tickets: + ticket_data.append( + { + "ticket_id": ticket["ticket_id"], + "ticket_cnt": ticket["ticket_cnt"], + } + ) + + return { + "status_code": "0", + "ticket_data": ticket_data, + "fulltune_count": fulltune_count, + "total_car_parts_count": total_car_parts_count, + "car_layout_count": [], + "car_style_count": [], + } + + def handle_factory_renameresult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + new_username = data.get("username") + + # save new username in database + if new_username: + self.data.profile.put_profile(user_id, self.version, data) + + return {"status_code": "0"} + + def handle_factory_updatecustomizeavatar_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + avatar_data: Dict = data.pop("avatar_obj") + stock_data: Dict = data.pop("stock_obj") + + # update the stock data in database + self._save_stock_data(user_id, stock_data) + + # save avatar data and avatar parts in database + self.data.profile.put_profile_avatar(user_id, avatar_data) + + return {"status_code": "0"} + + def handle_factory_updatecustomizeuser_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + stock_data: Dict = data.pop("stock_obj") + + # update the stock data in database + self._save_stock_data(user_id, stock_data) + + # update profile data and config in database + self.data.profile.put_profile(user_id, self.version, data) + + return {"status_code": "0"} + + def handle_user_updatestampinfo_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + stamp_event_data = data.pop("stamp_event_data") + for stamp in stamp_event_data: + self.data.item.put_stamp(user_id, stamp) + + return {"status_code": "0"} + + def handle_user_updatetimetrialresult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + stock_data: Dict = data.pop("stock_obj") + ticket_data: List = data.pop("ticket_data") + reward_dist_data: Dict = data.pop("reward_dist_obj") + driver_debut_data = data.pop("driver_debut_obj") + rank_data: Dict = data.pop("mode_rank_obj") + + # time trial event points + event_point = data.pop("event_point") + + # save stock data in database + self._save_stock_data(user_id, stock_data) + + # save tickets in database + for ticket in ticket_data: + self.data.item.put_ticket(user_id, ticket) + + # save mode rank data in database + rank_data.update(reward_dist_data) + self.data.profile.put_profile_rank(user_id, self.version, rank_data) + + # get the profile data, update total_play and daily_play, and save it + profile = self.data.profile.get_profile(user_id, self.version) + total_play = profile["total_play"] + 1 + + # update profile + self.data.profile.put_profile( + user_id, + self.version, + { + "total_play": total_play, + "last_play_date": datetime.now(), + "aura_id": data.pop("aura_id"), + "aura_color_id": data.pop("aura_color_id"), + "aura_line_id": data.pop("aura_line_id"), + "cash": data.pop("cash"), + "total_cash": data.pop("total_cash"), + "dressup_point": data.pop("dressup_point"), + "avatar_point": data.pop("avatar_point"), + "mileage": data.pop("mileage"), + }, + ) + + # get the use_count and story_use_count of the used car + style_car_id = data.get("style_car_id") + car_mileage = data.pop("car_mileage") + used_car = self.data.item.get_car(user_id, self.version, style_car_id)._asdict() + + # increase the use_count and story_use_count of the used car + used_car["use_count"] += 1 + used_car["timetrial_use_count"] += 1 + used_car["car_mileage"] = car_mileage + + # save the used car in database + self.data.item.put_car(user_id, self.version, used_car) + + # skill_level_exp is the "course proeficiency" and is saved + # in the course table + course_id = data.get("course_id") + run_counts = 1 + skill_level_exp = data.pop("skill_level_exp") + + # get the course data + course = self.data.item.get_course(user_id, course_id) + if course: + # update run_counts + run_counts = course["run_counts"] + 1 + + self.data.item.put_course( + user_id, + { + "course_id": course_id, + "run_counts": run_counts, + "skill_level_exp": skill_level_exp, + }, + ) + + goal_time = data.get("goal_time") + # grab the ranking data and count the numbers of rows with a faster time + # than the current goal_time + course_rank = self.data.item.get_time_trial_ranking_by_course( + self.version, course_id, limit=None + ) + course_rank = len([r for r in course_rank if r["goal_time"] < goal_time]) + 1 + + car_course_rank = self.data.item.get_time_trial_ranking_by_course( + self.version, course_id, style_car_id, limit=None + ) + car_course_rank = ( + len([r for r in car_course_rank if r["goal_time"] < goal_time]) + 1 + ) + + # only update the time if its better than the best time and also not 0 + if data.get("goal_time") > 0: + # get the current best goal time + best_time_trial = ( + self.data.item.get_time_trial_user_best_time_by_course_car( + self.version, user_id, course_id, style_car_id + ) + ) + + if ( + best_time_trial is None + or data.get("goal_time") < best_time_trial["goal_time"] + ): + # now finally save the time trial with updated timestamp + data["play_dt"] = datetime.now() + self.data.item.put_time_trial(self.version, user_id, data) + + # update the timetrial event points + self.data.item.put_timetrial_event( + user_id, self.timetrial_event_id, event_point + ) + + return { + "status_code": "0", + "course_rank": course_rank, + "course_car_rank": car_course_rank, + "location_course_store_rank": course_rank, + "car_use_count": [], + "maker_use_count": [], + "timetrial_event_data": { + "timetrial_event_id": self.timetrial_event_id, + "point": event_point, + }, + } + + def handle_user_updatestoryresult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + stock_data: Dict = data.pop("stock_obj") + ticket_data: List = data.pop("ticket_data") + reward_dist_data: Dict = data.pop("reward_dist_obj") + driver_debut_data = data.pop("driver_debut_obj") + rank_data: Dict = data.pop("mode_rank_obj") + # stamp_event_data = data.pop("stamp_event_data") + + # save stock data in database + self._save_stock_data(user_id, stock_data) + + # save tickets in database + for ticket in ticket_data: + self.data.item.put_ticket(user_id, ticket) + + # save mode rank data in database + rank_data.update(reward_dist_data) + self.data.profile.put_profile_rank(user_id, self.version, rank_data) + + # save the current story progress in database + max_loop = data.get("chapter_loop_max") + chapter_id = data.get("chapter") + + episode_id = data.get("episode") + difficulty = data.get("difficulty") + play_status = data.get("play_status") + + # get the current loop from the database + story_data = self.data.item.get_story(user_id, chapter_id) + # 1 = active, 2+ = cleared? + loop_count = 1 + if story_data: + loop_count = story_data["loop_count"] + + # if the played difficulty is smaller than loop_count you cannot clear + # (play_status = 2) the episode otherwise the following difficulties + # won't earn any EXP? + if difficulty < loop_count: + play_status = 1 + + # if the episode has already been cleared, set the play_status to 2 + # so it won't be set to unplayed (play_status = 1) + episode_data = self.data.item.get_story_episode(user_id, episode_id) + if episode_data: + if play_status < episode_data["play_status"]: + play_status = 2 + + # save the current episode progress in database + self.data.item.put_story_episode( + user_id, + chapter_id, + { + "episode": episode_id, + "play_status": play_status, + }, + ) + + if loop_count < max_loop and data.get("chapter_clear") == 1: + # increase the loop count + loop_count += 1 + + # for the current chapter set all episode play_status back to 1 + self.data.item.put_story_episode_play_status(user_id, chapter_id, 1) + + self.data.item.put_story( + user_id, + { + "story_type": data.get("story_type"), + "chapter": chapter_id, + "loop_count": loop_count, + }, + ) + + # save the current episode difficulty progress in database + self.data.item.put_story_episode_difficulty( + user_id, + episode_id, + { + "difficulty": difficulty, + "play_count": 1, # no idea where this comes from + "clear_count": 1, # no idea where this comes from + "play_status": data.get("play_status"), + "play_score": data.get("play_score"), + }, + ) + + # get the use_count and story_use_count of the used car + style_car_id = data.get("style_car_id") + car_mileage = data.get("car_mileage") + used_car = self.data.item.get_car(user_id, self.version, style_car_id)._asdict() + + # increase the use_count and story_use_count of the used car + used_car["use_count"] += 1 + used_car["story_use_count"] += 1 + used_car["car_mileage"] = car_mileage + + # save the used car in database + self.data.item.put_car(user_id, self.version, used_car) + + # get the profile data, update total_play and daily_play, and save it + profile = self.data.profile.get_profile(user_id, self.version) + total_play = profile["total_play"] + 1 + + # save user profile in database + self.data.profile.put_profile( + user_id, + self.version, + { + "total_play": total_play, + "last_play_date": datetime.now(), + "mileage": data.pop("mileage"), + "cash": data.pop("cash"), + "total_cash": data.pop("total_cash"), + "dressup_point": data.pop("dressup_point"), + "avatar_point": data.pop("avatar_point"), + "aura_id": data.pop("aura_id"), + "aura_color_id": data.pop("aura_color_id"), + "aura_line_id": data.pop("aura_line_id"), + }, + ) + + return { + "status_code": "0", + "story_data": self._generate_story_data(user_id), + "car_use_count": [], + "maker_use_count": [], + } + + def handle_user_updatespecialmoderesult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + stock_data: Dict = data.pop("stock_obj") + ticket_data: List = data.pop("ticket_data") + reward_dist_data: Dict = data.pop("reward_dist_obj") + driver_debut_data = data.pop("driver_debut_obj") + rank_data: Dict = data.pop("mode_rank_obj") + + # unused + hint_display_flag: int = data.pop("hint_display_flag") + + # get the vs use count from database and update it + style_car_id = data.pop("style_car_id") + car_data = self.data.item.get_car(user_id, self.version, style_car_id) + story_use_count = car_data["story_use_count"] + 1 + + # save car data in database + self.data.item.put_car( + user_id, + self.version, + { + "style_car_id": style_car_id, + "car_mileage": data.pop("car_mileage"), + "story_use_count": story_use_count, + }, + ) + + # get the profile data, update total_play and daily_play, and save it + profile = self.data.profile.get_profile(user_id, self.version) + total_play = profile["total_play"] + 1 + + # save user profile in database + self.data.profile.put_profile( + user_id, + self.version, + { + "total_play": total_play, + "last_play_date": datetime.now(), + "mileage": data.pop("mileage"), + "cash": data.pop("cash"), + "total_cash": data.pop("total_cash"), + "dressup_point": data.pop("dressup_point"), + "avatar_point": data.pop("avatar_point"), + "aura_id": data.pop("aura_id"), + "aura_color_id": data.pop("aura_color_id"), + "aura_line_id": data.pop("aura_line_id"), + }, + ) + + # save stock data in database + self._save_stock_data(user_id, stock_data) + + # save ticket data in database + for ticket in ticket_data: + self.data.item.put_ticket(user_id, ticket) + + # save mode_rank and reward_dist data in database + rank_data.update(reward_dist_data) + self.data.profile.put_profile_rank(user_id, self.version, rank_data) + + # finally save the special mode with story_type=4 in database + self.data.item.put_challenge(user_id, data) + + return { + "status_code": "0", + "special_mode_data": self._generate_special_data(user_id), + "car_use_count": [], + "maker_use_count": [], + } + + def handle_user_updatechallengemoderesult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + stock_data: Dict = data.pop("stock_obj") + ticket_data: List = data.pop("ticket_data") + reward_dist_data: Dict = data.pop("reward_dist_obj") + driver_debut_data = data.pop("driver_debut_obj") + rank_data: Dict = data.pop("mode_rank_obj") + + # get the vs use count from database and update it + style_car_id = data.get("style_car_id") + car_data = self.data.item.get_car(user_id, self.version, style_car_id) + story_use_count = car_data["story_use_count"] + 1 + + # save car data in database + self.data.item.put_car( + user_id, + self.version, + { + "style_car_id": style_car_id, + "car_mileage": data.pop("car_mileage"), + "story_use_count": story_use_count, + }, + ) + + # get the profile data, update total_play and daily_play, and save it + profile = self.data.profile.get_profile(user_id, self.version) + total_play = profile["total_play"] + 1 + + # save user profile in database + self.data.profile.put_profile( + user_id, + self.version, + { + "total_play": total_play, + "last_play_date": datetime.now(), + "mileage": data.pop("mileage"), + "cash": data.pop("cash"), + "total_cash": data.pop("total_cash"), + "dressup_point": data.pop("dressup_point"), + "avatar_point": data.pop("avatar_point"), + "aura_id": data.pop("aura_id"), + "aura_color_id": data.pop("aura_color_id"), + "aura_line_id": data.pop("aura_line_id"), + }, + ) + + # save stock data in database + self._save_stock_data(user_id, stock_data) + + # save ticket data in database + for ticket in ticket_data: + self.data.item.put_ticket(user_id, ticket) + + # save mode_rank and reward_dist data in database + rank_data.update(reward_dist_data) + self.data.profile.put_profile_rank(user_id, self.version, rank_data) + + # get the challenge mode data from database + challenge_data = self.data.item.get_challenge( + user_id, data.get("vs_type"), data.get("play_difficulty") + ) + + if challenge_data: + # update play count + play_count = challenge_data["play_count"] + 1 + data["play_count"] = play_count + + # finally save the challenge mode with story_type=3 in database + self.data.item.put_challenge(user_id, data) + + return { + "status_code": "0", + "challenge_mode_data": self._generate_challenge_data(user_id), + "car_use_count": [], + "maker_use_count": [], + } + + def _generate_time_trial_data(self, season_id: int, user_id: int) -> List[Dict]: + # get the season time trial data from database + timetrial_data = [] + + courses = self.data.item.get_courses(user_id) + if courses is None or len(courses) == 0: + return {"status_code": "0", "timetrial_data": timetrial_data} + + for course in courses: + # grab the course id and course proeficiency + course_id = course["course_id"] + skill_level_exp = course["skill_level_exp"] + + # get the best time for the current course for the current user + best_trial = self.data.item.get_time_trial_best_ranking_by_course( + season_id, user_id, course_id + ) + if not best_trial: + continue + + goal_time = best_trial["goal_time"] + # get the rank for the current course + course_rank = self.data.item.get_time_trial_ranking_by_course( + season_id, course_id, limit=None + ) + course_rank = ( + len([r for r in course_rank if r["goal_time"] < goal_time]) + 1 + ) + + timetrial_data.append( + { + "style_car_id": best_trial["style_car_id"], + "course_id": course_id, + "skill_level_exp": skill_level_exp, + "goal_time": goal_time, + "rank": course_rank, + "rank_dt": int(best_trial["play_dt"].timestamp()), + } + ) + + return timetrial_data + + def handle_user_getpastseasontadata_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + season_id = data.get("season_id") + + # so to get the season 1 data just subtract 1 from the season id + past_timetrial_data = self._generate_time_trial_data(season_id - 1, user_id) + + # TODO: get the current season timetrial data somehow, because after requesting + # GetPastSeasonTAData the game will NOT request GetTAData?! + return { + "status_code": "0", + "season_id": season_id, + "past_season_timetrial_data": past_timetrial_data, + } + + def handle_user_gettadata_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + timetrial_data = self._generate_time_trial_data(self.version, user_id) + + # TODO: get the past season timetrial data somehow, because after requesting + # GetTAData the game will NOT request GetPastSeasonTAData?! + return { + "status_code": "0", + "timetrial_data": timetrial_data, + # "past_season_timetrial_data": timetrial_data, + } + + def handle_user_updatecartune_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + # full tune ticket + use_ticket = data.pop("use_ticket") + if use_ticket: + # get the user's tickets, full tune ticket id is 25 + ticket = self.data.item.get_ticket(user_id, ticket_id=25) + + # update the ticket in the database + self.data.item.put_ticket( + user_id, + { + "ticket_id": ticket["ticket_id"], + "ticket_cnt": ticket["ticket_cnt"] - 1, + }, + ) + + # also set the tune_level to 16 (fully tuned) + data["tune_level"] = 16 + + self.data.item.put_car(user_id, self.version, data) + + return { + "status_code": "0", + "story_data": self._generate_story_data(user_id), + "car_use_count": [], + "maker_use_count": [], + } + + def handle_log_saveplaylog_request(self, data: Dict, headers: Dict): + pass + + def handle_log_saveendlog_request(self, data: Dict, headers: Dict): + pass + + def handle_user_updatemoderesult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + config_data: Dict = data.pop("config_obj") + stock_data: Dict = data.pop("stock_obj") + ticket_data: List = data.pop("ticket_data") + reward_dist_data: Dict = data.pop("reward_dist_obj") + + # not required? + mode_id = data.pop("mode_id") + standby_play_flag = data.pop("standby_play_flag") + tips_list = data.pop("tips_list") + + # save stock data in database + self._save_stock_data(user_id, stock_data) + + # save tickets in database + for ticket in ticket_data: + self.data.item.put_ticket(user_id, ticket) + + # save rank dist data in database + self.data.profile.put_profile_rank(user_id, self.version, reward_dist_data) + + # update profile data and config in database + self.data.profile.put_profile(user_id, self.version, data) + config_data["config_id"] = config_data.pop("id") + self.data.profile.put_profile_config(user_id, config_data) + + return {"status_code": "0", "server_status": 1} + + def _generate_theory_rival_data( + self, user_list: list, course_id: int, req_user_id: int + ) -> list: + rival_data = [] + for user_id in user_list: + # if not enough players are available just use the data from the req_user + if user_id == -1: + profile = self.data.profile.get_profile(req_user_id, self.version) + profile = profile._asdict() + # set the name to CPU + profile["username"] = f"CPU" + # also reset stamps to default + profile["country"] = 9 + profile["store"] = 0 + profile["stamp_key_assign_0"] = 0 + profile["stamp_key_assign_1"] = 1 + profile["stamp_key_assign_2"] = 2 + profile["stamp_key_assign_3"] = 3 + profile["mytitle_id"] = 0 + else: + profile = self.data.profile.get_profile(user_id, self.version) + + rank = self.data.profile.get_profile_rank(profile["user"], self.version) + + avatars = [ + { + "sex": 0, + "face": 1, + "eye": 1, + "mouth": 1, + "hair": 1, + "glasses": 0, + "face_accessory": 0, + "body": 1, + "body_accessory": 0, + "behind": 0, + "bg": 1, + "effect": 0, + "special": 0, + }, + { + "sex": 0, + "face": 1, + "eye": 1, + "mouth": 1, + "hair": 19, + "glasses": 0, + "face_accessory": 0, + "body": 2, + "body_accessory": 0, + "behind": 0, + "bg": 1, + "effect": 0, + "special": 0, + }, + { + "sex": 1, + "face": 91, + "eye": 265, + "mouth": 13, + "hair": 369, + "glasses": 0, + "face_accessory": 0, + "body": 113, + "body_accessory": 0, + "behind": 0, + "bg": 1, + "effect": 0, + "special": 0, + }, + { + "sex": 1, + "face": 91, + "eye": 265, + "mouth": 13, + "hair": 387, + "glasses": 0, + "face_accessory": 0, + "body": 114, + "body_accessory": 0, + "behind": 0, + "bg": 1, + "effect": 0, + "special": 0, + }, + ] + + if user_id == -1: + # get a random avatar from the list and some random car from all users + avatar = choice(avatars) + car = self.data.item.get_random_car(self.version) + else: + avatar = self.data.profile.get_profile_avatar(profile["user"]) + car = self.data.item.get_random_user_car(profile["user"], self.version) + + parts_list = [] + for part in car["parts_list"]: + parts_list.append(part["parts"]) + + course = self.data.item.get_theory_course(profile["user"], course_id) + powerhose_lv = 0 + if course: + powerhose_lv = course["powerhouse_lv"] + + theory_running = self.data.item.get_theory_running_by_course( + profile["user"], course_id + ) + + # normally it's 127 after the first play so we set it to 128 + attack = 128 + defense = 128 + safety = 128 + runaway = 128 + trick_flag = 0 + if theory_running and user_id != -1: + attack = theory_running["attack"] + defense = theory_running["defense"] + safety = theory_running["safety"] + runaway = theory_running["runaway"] + trick_flag = theory_running["trick_flag"] + + # get the time trial ranking medal + eval_id = 0 + time_trial = self.data.item.get_time_trial_best_ranking_by_course( + self.version, profile["user"], course_id + ) + if time_trial: + eval_id = time_trial["eval_id"] + + arcade = self.data.arcade.get_arcade(profile["store"]) + if arcade is None: + arcade = {} + arcade["name"] = self.core_cfg.server.name + + rival_data.append( + { + "id": profile["user"], + "name": profile["username"], + "grade": rank["grade"], + # only needed for power match + "powerhouseLv": powerhose_lv, + "mytitleId": profile["mytitle_id"], + "country": profile["country"], + "auraId": profile["aura_id"], + "auraColor": profile["aura_color_id"], + "auraLine": profile["aura_line_id"], + # not sure? + "roundRanking": 0, + "storeName": arcade["name"], + "sex": avatar["sex"], + "face": avatar["face"], + "eye": avatar["eye"], + "mouth": avatar["mouth"], + "hair": avatar["hair"], + "glasses": avatar["glasses"], + "faceAccessory": avatar["face_accessory"], + "body": avatar["body"], + "bodyAccessory": avatar["body_accessory"], + "behind": avatar["behind"], + "bg": avatar["bg"], + "effect": avatar["effect"], + "special": avatar["special"], + "styleCarId": car["style_car_id"], + "color": car["color"], + "bureau": car["bureau"], + "kana": car["kana"], + "sNo": car["s_no"], + "lNo": car["l_no"], + "tuneLv": car["tune_level"], + "carFlag": car["car_flag"], + "tunePoint": car["tune_point"], + "infinityTune": car["infinity_tune"], + "tuneParts": car["tune_parts"], + "partsList": parts_list, + "partsCount": car["equip_parts_count"], + "stamp0": profile["stamp_key_assign_0"], + "stamp1": profile["stamp_key_assign_1"], + "stamp2": profile["stamp_key_assign_2"], + "stamp3": profile["stamp_key_assign_3"], + "attack": attack, + "defense": defense, + "safety": safety, + "runaway": runaway, + "trickFlg": trick_flag, + # time trial ranking medal + "taEval": eval_id, + } + ) + + return rival_data + + def handle_theory_matching_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + course_id = data.pop("course_id") + # no idea why thats needed? + grade = data.pop("grade") + + # number of auto_matches and power_matches, official values are: + count_auto_match = 9 + count_power_match = 3 + + # required for the power_match list? + powerhose_lv = data.pop("powerhouse_lv") + + # get random profiles for auto match + profiles = self.data.profile.get_different_random_profiles( + user_id, self.version, count=count_auto_match + ) + + user_list = [profile["user"] for profile in profiles] + # if user_list is not count_auto_match long, fill it up with -1 + while len(user_list) < count_auto_match: + user_list.append(-1) + + auto_match = self._generate_theory_rival_data(user_list, course_id, user_id) + + # get profiles with the same powerhouse_lv for power match + theory_courses = self.data.item.get_theory_course_by_powerhouse_lv( + user_id, course_id, powerhose_lv, count=count_power_match + ) + user_list = [course["user"] for course in theory_courses] + + # if user_list is not count_power_match long, fill it up with -1 + while len(user_list) < count_power_match: + user_list.append(-1) + + power_match = self._generate_theory_rival_data(user_list, course_id, user_id) + + return { + "status_code": "0", + "server_status": 1, + "rival_data": { + "auto_match": auto_match, + "power_match": power_match, + }, + } + + def handle_user_updatetheoryresult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + stock_data: Dict = data.pop("stock_obj") + ticket_data: List = data.pop("ticket_data") + reward_dist_data: Dict = data.pop("reward_dist_obj") + rank_data: Dict = data.pop("mode_rank_obj") + driver_debut_data: Dict = data.pop("driver_debut_obj") + + # save stock data in database + self._save_stock_data(user_id, stock_data) + + # save tickets in database + for ticket in ticket_data: + self.data.item.put_ticket(user_id, ticket) + + # save rank dist data in database + rank_data.update(reward_dist_data) + self.data.profile.put_profile_rank(user_id, self.version, rank_data) + + # save the profile theory data in database + play_count = 1 + play_count_multi = 1 + win_count = 0 + win_count_multi = 0 + + theory_data = self.data.profile.get_profile_theory(user_id, self.version) + if theory_data: + play_count = theory_data["play_count"] + 1 + play_count_multi = theory_data["play_count_multi"] + 1 + win_count = theory_data["win_count"] + win_count_multi = theory_data["win_count_multi"] + + # check all advantages and see if one of them is larger than 0 + # if so, we won + if ( + data.get("advantage_1") > 0 + or data.get("advantage_2") > 0 + or data.get("advantage_3") > 0 + or data.get("advantage_4") > 0 + ): + win_count += 1 + win_count_multi += 1 + + self.data.profile.put_profile_theory( + user_id, + self.version, + { + "play_count": play_count, + "play_count_multi": play_count_multi, + "partner_id": data.get("partner_id"), + "partner_progress": data.get("partner_progress"), + "partner_progress_score": data.get("partner_progress_score"), + "practice_start_rank": data.get("practice_start_rank"), + "general_flag": data.get("general_flag"), + "vs_history": data.get("vs_history"), + # no idea? + "vs_history_multi": data.get("vs_history"), + "win_count": win_count, + "win_count_multi": win_count_multi, + }, + ) + + # save theory course in database + self.data.item.put_theory_course( + user_id, + { + "course_id": data.get("course_id"), + "max_victory_grade": data.get("max_victory_grade"), + # always add 1? + "run_count": 1, + "powerhouse_lv": data.get("powerhouse_lv"), + "powerhouse_exp": data.get("powerhouse_exp"), + # not sure if the played_powerhouse_lv is the same as powerhouse_lv + "played_powerhouse_lv": data.get("powerhouse_lv"), + }, + ) + + # save the theory partner in database + self.data.item.put_theory_partner( + user_id, + { + "partner_id": data.get("partner_id"), + "fellowship_lv": data.get("fellowship_lv"), + "fellowship_exp": data.get("fellowship_exp"), + }, + ) + + # save the theory running in database? + self.data.item.put_theory_running( + user_id, + { + "course_id": data.get("course_id"), + "attack": data.get("attack"), + "defense": data.get("defense"), + "safety": data.get("safety"), + "runaway": data.get("runaway"), + "trick_flag": data.get("trick_flag"), + }, + ) + + # get the use_count and theory_use_count of the used car + style_car_id = data.get("style_car_id") + car_mileage = data.get("car_mileage") + used_car = self.data.item.get_car(user_id, self.version, style_car_id)._asdict() + + # increase the use_count and theory_use_count of the used car + used_car["use_count"] += 1 + used_car["theory_use_count"] += 1 + used_car["car_mileage"] = car_mileage + + # save the used car in database + self.data.item.put_car(user_id, self.version, used_car) + + # get the profile data, update total_play and daily_play, and save it + profile = self.data.profile.get_profile(user_id, self.version) + total_play = profile["total_play"] + 1 + + # save the profile in database + self.data.profile.put_profile( + user_id, + self.version, + { + "total_play": total_play, + "last_play_date": datetime.now(), + "mileage": data.get("mileage"), + "aura_id": data.get("aura_id"), + "aura_color_id": data.get("aura_color_id"), + "aura_line_id": data.get("aura_line_id"), + "cash": data.get("cash"), + "total_cash": data.get("total_cash"), + "dressup_point": data.get("dressup_point"), + "avatar_point": data.get("avatar_point"), + }, + ) + + return { + "status_code": "0", + "played_powerhouse_lv": data.get("powerhouse_lv"), + "car_use_count": [], + "maker_use_count": [], + "play_count": play_count, + "play_count_multi": play_count_multi, + "win_count": win_count, + "win_count_multi": win_count_multi, + } + + def handle_timetrial_getbestrecordprebattle_request( + self, data: Dict, headers: Dict + ): + user_id = headers["session"] + + course_pickup_car_best_data = [] + courses = self.data.item.get_time_trial_courses(self.version) + for course in courses: + car_list = [] + best_cars = self.data.item.get_time_trial_best_cars_by_course( + self.version, course["course_id"], user_id + ) + + for i, car in enumerate(best_cars): + car_list.append( + { + "rank": i + 1, + # no clue + "member": user_id, + "value": car["goal_time"], + "store": self.core_cfg.server.name, + # use car_id from request? + "car_id": 0, + "style_car_id": car["style_car_id"], + "play_dt": car["play_dt"].timestamp(), + "section_time_1": car["section_time_1"], + "section_time_2": car["section_time_2"], + "section_time_3": car["section_time_3"], + "section_time_4": car["section_time_4"], + "mission": car["mission"], + } + ) + + course_pickup_car_best_data.append( + { + "course_id": course["course_id"], + "car_list": car_list, + } + ) + + return { + "status_code": "0", + "course_pickup_car_best_data": course_pickup_car_best_data, + } + + def handle_user_updateonlinebattle_request(self, data: Dict, headers: Dict): + return { + "status_code": "0", + "bothwin_penalty": 1, + } + + def handle_user_updateonlinebattleresult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + stock_data: Dict = data.pop("stock_obj") + # save stock data in database + self._save_stock_data(user_id, stock_data) + + ticket_data: List = data.pop("ticket_data") + for ticket in ticket_data: + self.data.item.put_ticket(user_id, ticket) + + reward_dist_data: Dict = data.pop("reward_dist_obj") + rank_data: Dict = data.pop("mode_rank_obj") + + # save rank dist data in database + rank_data.update(reward_dist_data) + self.data.profile.put_profile_rank(user_id, self.version, rank_data) + + driver_debut_data = data.pop("driver_debut_obj") + + # get the use_count and net_vs_use_count of the used car + style_car_id = data.get("style_car_id") + car_mileage = data.pop("car_mileage") + used_car = self.data.item.get_car(user_id, self.version, style_car_id)._asdict() + + # increase the use_count and net_vs_use_count of the used car + used_car["use_count"] += 1 + used_car["net_vs_use_count"] += 1 + used_car["car_mileage"] = car_mileage + + # save the used car in database + self.data.item.put_car(user_id, self.version, used_car) + + # get the profile data, update total_play and daily_play, and save it + profile = self.data.profile.get_profile(user_id, self.version) + total_play = profile["total_play"] + 1 + + # save the profile in database + self.data.profile.put_profile( + user_id, + self.version, + { + "total_play": total_play, + "last_play_date": datetime.now(), + "mileage": data.pop("mileage"), + "aura_id": data.pop("aura_id"), + "aura_color_id": data.pop("aura_color_id"), + "aura_line_id": data.pop("aura_line_id"), + "cash": data.pop("cash"), + "total_cash": data.pop("total_cash"), + "dressup_point": data.pop("dressup_point"), + "avatar_point": data.pop("avatar_point"), + }, + ) + + self.data.item.put_vs_info(user_id, data) + + vs_info = { + "battle_mode": 0, + "vs_cnt": 1, + "vs_win": data.get("win_flg"), + "invalid": 0, + "str": 0, + "str_now": 0, + "lose_now": 0, + "vs_history": data.get("vs_history"), + "course_select_priority": data.get("course_select_priority"), + "vsinfo_course_data": [ + { + "course_id": data.get("course_id"), + "vs_cnt": 1, + "vs_win": data.get("win_flg"), + } + ], + } + + return { + "status_code": "0", + "vsinfo_data": vs_info, + "round_event": [ + { + "count": 1, + "win": 1, + "rank": 1, + "point": 1, + "total_round_point": 1, + } + ], + "car_use_count": [], + "maker_use_count": [], + } + + def handle_user_updatestorebattleresult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + stock_data: Dict = data.pop("stock_obj") + ticket_data: List = data.pop("ticket_data") + reward_dist_data: Dict = data.pop("reward_dist_obj") + rank_data: Dict = data.pop("mode_rank_obj") + driver_debut_data: Dict = data.pop("driver_debut_obj") + + # no idea? + result = data.pop("result") + battle_gift_event_id = data.pop("battle_gift_event_id") + gift_id = data.pop("gift_id") + + # save stock data in database + self._save_stock_data(user_id, stock_data) + + # save tickets in database + for ticket in ticket_data: + self.data.item.put_ticket(user_id, ticket) + + # save rank dist data in database + rank_data.update(reward_dist_data) + self.data.profile.put_profile_rank(user_id, self.version, rank_data) + + # get the use_count and net_vs_use_count of the used car + style_car_id = data.get("style_car_id") + car_mileage = data.pop("car_mileage") + used_car = self.data.item.get_car(user_id, self.version, style_car_id)._asdict() + + # increase the use_count and net_vs_use_count of the used car + used_car["use_count"] += 1 + used_car["vs_use_count"] += 1 + used_car["car_mileage"] = car_mileage + + # save the used car in database + self.data.item.put_car(user_id, self.version, used_car) + + # get the profile data, update total_play and daily_play, and save it + profile = self.data.profile.get_profile(user_id, self.version) + total_play = profile["total_play"] + 1 + + # save the profile in database + self.data.profile.put_profile( + user_id, + self.version, + { + "total_play": total_play, + "last_play_date": datetime.now(), + "mileage": data.pop("mileage"), + "aura_id": data.pop("aura_id"), + "aura_color_id": data.pop("aura_color_id"), + "aura_line_id": data.pop("aura_line_id"), + "cash": data.pop("cash"), + "total_cash": data.pop("total_cash"), + "dressup_point": data.pop("dressup_point"), + "avatar_point": data.pop("avatar_point"), + }, + ) + + # save vs_info in database + self.data.item.put_vs_info(user_id, data) + + vs_info = { + "battle_mode": 0, + "vs_cnt": 1, + "vs_win": data.get("win_flg"), + "invalid": 0, + "str": 0, + "str_now": 0, + "lose_now": 0, + "vs_history": data.get("vs_history"), + "course_select_priority": 0, + "vsinfo_course_data": [ + { + "course_id": data.get("course_id"), + "vs_cnt": 1, + "vs_win": data.get("win_flg"), + } + ], + } + + return { + "status_code": "0", + "vsinfo_data": vs_info, + "car_use_count": [], + "maker_use_count": [], + } diff --git a/titles/idz/__init__.py b/titles/idz/__init__.py new file mode 100644 index 0000000..958d08a --- /dev/null +++ b/titles/idz/__init__.py @@ -0,0 +1,8 @@ +from titles.idz.index import IDZServlet +from titles.idz.const import IDZConstants +from titles.idz.database import IDZData + +index = IDZServlet +database = IDZData +game_codes = [IDZConstants.GAME_CODE] +current_schema_version = 1 diff --git a/titles/idz/config.py b/titles/idz/config.py new file mode 100644 index 0000000..f7af4fd --- /dev/null +++ b/titles/idz/config.py @@ -0,0 +1,73 @@ +from typing import List, Dict + +from core.config import CoreConfig + + +class IDZServerConfig: + def __init__(self, parent_config: "IDZConfig") -> None: + self.__config = parent_config + + @property + def enable(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "idz", "server", "enable", default=True + ) + + @property + def loglevel(self) -> int: + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "idz", "server", "loglevel", default="info" + ) + ) + + @property + def hostname(self) -> str: + return CoreConfig.get_config_field( + self.__config, "idz", "server", "hostname", default="" + ) + + @property + def news(self) -> str: + return CoreConfig.get_config_field( + self.__config, "idz", "server", "news", default="" + ) + + @property + def aes_key(self) -> str: + return CoreConfig.get_config_field( + self.__config, "idz", "server", "aes_key", default="" + ) + + +class IDZPortsConfig: + def __init__(self, parent_config: "IDZConfig") -> None: + self.__config = parent_config + + @property + def userdb(self) -> int: + return CoreConfig.get_config_field( + self.__config, "idz", "ports", "userdb", default=10000 + ) + + @property + def match(self) -> int: + return CoreConfig.get_config_field( + self.__config, "idz", "ports", "match", default=10010 + ) + + @property + def echo(self) -> int: + return CoreConfig.get_config_field( + self.__config, "idz", "ports", "echo", default=10020 + ) + + +class IDZConfig(dict): + def __init__(self) -> None: + self.server = IDZServerConfig(self) + self.ports = IDZPortsConfig(self) + + @property + def rsa_keys(self) -> List[Dict]: + return CoreConfig.get_config_field(self, "idz", "rsa_keys", default=[]) diff --git a/titles/idz/const.py b/titles/idz/const.py new file mode 100644 index 0000000..93cbb4c --- /dev/null +++ b/titles/idz/const.py @@ -0,0 +1,53 @@ +from enum import Enum + + +class IDZConstants: + GAME_CODE = "SDDF" + + CONFIG_NAME = "idz.yaml" + + VER_IDZ_110 = 0 + VER_IDZ_130 = 1 + VER_IDZ_210 = 2 + VER_IDZ_230 = 3 + NUM_VERS = 4 + + VERSION_NAMES = ( + "Initial D Arcade Stage Zero v1.10", + "Initial D Arcade Stage Zero v1.30", + "Initial D Arcade Stage Zero v2.10", + "Initial D Arcade Stage Zero v2.30", + ) + + class PROFILE_STATUS(Enum): + LOCKED = 0 + UNLOCKED = 1 + OLD = 2 + + HASH_LUT = [ + # No clue + 0x9C82E674, + 0x5A4738D9, + 0x8B8D7AE0, + 0x29EC9D81, + # These three are from AES TE0 + 0x1209091B, + 0x1D83839E, + 0x582C2C74, + 0x341A1A2E, + 0x361B1B2D, + 0xDC6E6EB2, + 0xB45A5AEE, + 0x5BA0A0FB, + 0xA45252F6, + 0x763B3B4D, + 0xB7D6D661, + 0x7DB3B3CE, + ] + HASH_NUM = 0 + HASH_MUL = [5, 7, 11, 12][HASH_NUM] + HASH_XOR = [0xB3, 0x8C, 0x14, 0x50][HASH_NUM] + + @classmethod + def game_ver_to_string(cls, ver: int): + return cls.VERSION_NAMES[ver] diff --git a/titles/idz/database.py b/titles/idz/database.py new file mode 100644 index 0000000..525b1c1 --- /dev/null +++ b/titles/idz/database.py @@ -0,0 +1,7 @@ +from core.data import Data +from core.config import CoreConfig + + +class IDZData(Data): + def __init__(self, cfg: CoreConfig) -> None: + super().__init__(cfg) diff --git a/titles/idz/echo.py b/titles/idz/echo.py new file mode 100644 index 0000000..979fd19 --- /dev/null +++ b/titles/idz/echo.py @@ -0,0 +1,19 @@ +from twisted.internet.protocol import DatagramProtocol +import logging + +from core.config import CoreConfig +from .config import IDZConfig + + +class IDZEcho(DatagramProtocol): + def __init__(self, cfg: CoreConfig, game_cfg: IDZConfig) -> None: + super().__init__() + self.core_config = cfg + self.game_config = game_cfg + self.logger = logging.getLogger("idz") + + def datagramReceived(self, data, addr): + self.logger.debug( + f"Echo from from {addr[0]}:{addr[1]} -> {self.transport.getHost().port} - {data.hex()}" + ) + self.transport.write(data, addr) diff --git a/titles/idz/handlers/__init__.py b/titles/idz/handlers/__init__.py new file mode 100644 index 0000000..17213ba --- /dev/null +++ b/titles/idz/handlers/__init__.py @@ -0,0 +1,47 @@ +from .base import IDZHandlerBase + +from .load_server_info import IDZHandlerLoadServerInfo + +from .load_ghost import IDZHandlerLoadGhost + +from .load_config import IDZHandlerLoadConfigA, IDZHandlerLoadConfigB + +from .load_top_ten import IDZHandlerLoadTopTen + +from .update_story_clear_num import IDZHandlerUpdateStoryClearNum + +from .save_expedition import IDZHandlerSaveExpedition + +from .load_2on2 import IDZHandlerLoad2on2A, IDZHandlerLoad2on2B + +from .load_team_ranking import IDZHandlerLoadTeamRankingA, IDZHandlerLoadTeamRankingB + +from .discover_profile import IDZHandlerDiscoverProfile + +from .lock_profile import IDZHandlerLockProfile + +from .check_team_names import IDZHandlerCheckTeamName + +from .unknown import IDZHandlerUnknown + +from .create_profile import IDZHandlerCreateProfile + +from .create_auto_team import IDZHandlerCreateAutoTeam + +from .load_profile import IDZHandlerLoadProfile + +from .save_profile import IDZHandlerSaveProfile + +from .update_provisional_store_rank import IDZHandlerUpdateProvisionalStoreRank + +from .load_reward_table import IDZHandlerLoadRewardTable + +from .save_topic import IDZHandlerSaveTopic + +from .save_time_attack import IDZHandlerSaveTimeAttack + +from .unlock_profile import IDZHandlerUnlockProfile + +from .update_team_points import IDZHandleUpdateTeamPoints + +from .update_ui_report import IDZHandleUpdateUIReport diff --git a/titles/idz/handlers/base.py b/titles/idz/handlers/base.py new file mode 100644 index 0000000..6b1e5d5 --- /dev/null +++ b/titles/idz/handlers/base.py @@ -0,0 +1,26 @@ +import logging +import struct +from core.data import Data +from core.config import CoreConfig +from ..config import IDZConfig +from ..const import IDZConstants + + +class IDZHandlerBase: + name = "generic" + cmd_codes = [0x0000] * IDZConstants.NUM_VERS + rsp_codes = [0x0001] * IDZConstants.NUM_VERS + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + self.core_config = core_cfg + self.game_cfg = game_cfg + self.data = Data(core_cfg) + self.logger = logging.getLogger("idz") + self.game = IDZConstants.GAME_CODE + self.version = version + self.size = 0x30 + + def handle(self, data: bytes) -> bytearray: + ret = bytearray([0] * self.size) + struct.pack_into(" None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x0010 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + struct.pack_into(" None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x0CA0 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + aime_id = struct.unpack_from(" None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x0020 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + + aime_id = struct.unpack_from(" 2: + struct.pack_into(" None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x0010 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + user_id = struct.unpack_from(" None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x04C0 + + if version >= IDZConstants.VER_IDZ_210: + self.size = 0x1290 + + def handle(self, data: bytes) -> bytearray: + return super().handle(data) + + +class IDZHandlerLoad2on2B(IDZHandlerBase): + cmd_codes = [0x0132] * 4 + rsp_codes = [0x0133] * 4 + name = "load_2on2B" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x04C0 + + if version >= IDZConstants.VER_IDZ_210: + self.size = 0x0540 + + def handle(self, data: bytes) -> bytearray: + return super().handle(data) diff --git a/titles/idz/handlers/load_config.py b/titles/idz/handlers/load_config.py new file mode 100644 index 0000000..b3ceb0d --- /dev/null +++ b/titles/idz/handlers/load_config.py @@ -0,0 +1,43 @@ +import struct + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig +from ..const import IDZConstants + + +class IDZHandlerLoadConfigA(IDZHandlerBase): + cmd_codes = [0x0004] * IDZConstants.NUM_VERS + rsp_codes = [0x0005] * IDZConstants.NUM_VERS + name = "load_config_a" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x01A0 + + if self.version > 1: + self.size = 0x05E0 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + struct.pack_into(" None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x0230 + + if self.version > 1: + self.size = 0x0240 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + struct.pack_into(" None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x0070 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + struct.pack_into(" None: + super().__init__(core_cfg, game_cfg, version) + + if self.version == IDZConstants.VER_IDZ_110: + self.size = 0x0D30 + elif self.version == IDZConstants.VER_IDZ_130: + self.size = 0x0EA0 + elif self.version == IDZConstants.VER_IDZ_210: + self.size = 0x1360 + elif self.version == IDZConstants.VER_IDZ_230: + self.size = 0x1640 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + aime_id = struct.unpack_from(" None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x01C0 + + def handle(self, data: bytes) -> bytearray: + return super().handle(data) diff --git a/titles/idz/handlers/load_server_info.py b/titles/idz/handlers/load_server_info.py new file mode 100644 index 0000000..4c60dd7 --- /dev/null +++ b/titles/idz/handlers/load_server_info.py @@ -0,0 +1,99 @@ +import struct + +from core.utils import Utils +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig +from ..const import IDZConstants + + +class IDZHandlerLoadServerInfo(IDZHandlerBase): + cmd_codes = [0x0006] * IDZConstants.NUM_VERS + rsp_codes = [0x0007] * IDZConstants.NUM_VERS + name = "load_server_info1" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x04B0 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + offset = 0 + if self.version >= IDZConstants.VER_IDZ_210: + offset = 2 + + t_port = Utils.get_title_port(self.core_config) + + news_str = f"http://{self.core_config.title.hostname}:{t_port}/idz/news/news80**.txt" + err_str = f"http://{self.core_config.title.hostname}:{t_port}/idz/error" + + len_hostname = len(self.core_config.title.hostname) + len_news = len(news_str) + len_error = len(err_str) + + struct.pack_into(" None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x0BA0 + + def handle(self, data: bytes) -> bytearray: + return super().handle(data) + + +class IDZHandlerLoadTeamRankingB(IDZHandlerBase): + cmd_codes = [0x00BB, 0x00BB, 0x00A9, 0x00A9] + rsp_codes = [0x00A8] * 4 + name = "load_team_ranking_b" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x0BA0 + + def handle(self, data: bytes) -> bytearray: + return super().handle(data) diff --git a/titles/idz/handlers/load_top_ten.py b/titles/idz/handlers/load_top_ten.py new file mode 100644 index 0000000..09a9f5f --- /dev/null +++ b/titles/idz/handlers/load_top_ten.py @@ -0,0 +1,35 @@ +import struct +from typing import Tuple, List, Dict + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig + + +class IDZHandlerLoadTopTen(IDZHandlerBase): + cmd_codes = [0x012C] * 4 + rsp_codes = [0x00CE] * 4 + name = "load_top_ten" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x1720 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + tracks_dates: List[Tuple[int, int]] = [] + for i in range(32): + tracks_dates.append( + ( + struct.unpack_from(" None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x0020 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + profile_data = { + "status": IDZConstants.PROFILE_STATUS.UNLOCKED.value, + "expire_time": int( + (datetime.now() + timedelta(hours=1)).timestamp() / 1000 + ), + } + user_id = struct.unpack_from(" 0: + old_profile = None + if old_profile is not None: + profile_data["status"] = IDZConstants.PROFILE_STATUS.OLD.value + + return self.handle_common(profile_data, ret) + + def handle_common(cls, data: Dict, ret: bytearray) -> bytearray: + struct.pack_into(" None: + super().__init__(core_cfg, game_cfg, version) + + def handle(self, data: bytes) -> bytearray: + return super().handle(data) diff --git a/titles/idz/handlers/save_profile.py b/titles/idz/handlers/save_profile.py new file mode 100644 index 0000000..3f5311d --- /dev/null +++ b/titles/idz/handlers/save_profile.py @@ -0,0 +1,16 @@ +import struct + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig + + +class IDZHandlerSaveProfile(IDZHandlerBase): + cmd_codes = [0x0068, 0x0138, 0x0138, 0x0143] + name = "save_profile" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + + def handle(self, data: bytes) -> bytearray: + return super().handle(data) diff --git a/titles/idz/handlers/save_time_attack.py b/titles/idz/handlers/save_time_attack.py new file mode 100644 index 0000000..bea83af --- /dev/null +++ b/titles/idz/handlers/save_time_attack.py @@ -0,0 +1,23 @@ +import struct + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig +from ..const import IDZConstants + + +class IDZHandlerSaveTimeAttack(IDZHandlerBase): + cmd_codes = [0x00CD, 0x0136, 0x0136, 0x0136] + rsp_codes = [0x00CE, 0x00CE, 0x00CD, 0x00CD] + name = "save_time_attack" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x00B0 + + if self.version > IDZConstants.VER_IDZ_130: + self.size = 0x00F0 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + return ret diff --git a/titles/idz/handlers/save_topic.py b/titles/idz/handlers/save_topic.py new file mode 100644 index 0000000..090ce52 --- /dev/null +++ b/titles/idz/handlers/save_topic.py @@ -0,0 +1,18 @@ +import struct + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig + + +class IDZHandlerSaveTopic(IDZHandlerBase): + cmd_codes = [0x009A, 0x009A, 0x0091, 0x0091] + rsp_codes = [0x009B, 0x009B, 0x0092, 0x0092] + name = "save_topic" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x05D0 + + def handle(self, data: bytes) -> bytearray: + return super().handle(data) diff --git a/titles/idz/handlers/unknown.py b/titles/idz/handlers/unknown.py new file mode 100644 index 0000000..8998d81 --- /dev/null +++ b/titles/idz/handlers/unknown.py @@ -0,0 +1,13 @@ +import struct + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig + + +class IDZHandlerUnknown(IDZHandlerBase): + cmd_codes = [0x00AD, 0x00AD, 0x00A2, 0x00A2] + name = "unknown" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) diff --git a/titles/idz/handlers/unlock_profile.py b/titles/idz/handlers/unlock_profile.py new file mode 100644 index 0000000..1be50f5 --- /dev/null +++ b/titles/idz/handlers/unlock_profile.py @@ -0,0 +1,21 @@ +import struct + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig +from ..const import IDZConstants + + +class IDZHandlerUnlockProfile(IDZHandlerBase): + cmd_codes = [0x006F, 0x006F, 0x006B, 0x006B] + rsp_codes = [0x0070, 0x0070, 0x006C, 0x006C] + name = "unlock_profile" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x0010 + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + struct.pack_into(" None: + super().__init__(core_cfg, game_cfg, version) + self.size = 0x02B0 + + def handle(self, data: bytes) -> bytearray: + return super().handle(data) + + def handle_common(cls, aime_id: int, ret: bytearray) -> bytearray: + pass diff --git a/titles/idz/handlers/update_story_clear_num.py b/titles/idz/handlers/update_story_clear_num.py new file mode 100644 index 0000000..bcf44a5 --- /dev/null +++ b/titles/idz/handlers/update_story_clear_num.py @@ -0,0 +1,27 @@ +import struct + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig +from ..const import IDZConstants + + +class IDZHandlerUpdateStoryClearNum(IDZHandlerBase): + cmd_codes = [0x007F, 0x097F, 0x013D, 0x0144] + rsp_codes = [0x0080, 0x013E, 0x013E, 0x0145] + name = "update_story_clear_num" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + + if self.version == IDZConstants.VER_IDZ_110: + self.size = 0x0220 + elif self.version == IDZConstants.VER_IDZ_130: + self.size = 0x04F0 + elif self.version == IDZConstants.VER_IDZ_210: + self.size = 0x0510 + elif self.version == IDZConstants.VER_IDZ_230: + self.size = 0x0800 + + def handle(self, data: bytes) -> bytearray: + return super().handle(data) diff --git a/titles/idz/handlers/update_team_points.py b/titles/idz/handlers/update_team_points.py new file mode 100644 index 0000000..a23d843 --- /dev/null +++ b/titles/idz/handlers/update_team_points.py @@ -0,0 +1,18 @@ +import struct + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig +from ..const import IDZConstants + + +class IDZHandleUpdateTeamPoints(IDZHandlerBase): + cmd_codes = [0x0081, 0x0081, 0x007B, 0x007B] + name = "unlock_profile" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + return ret diff --git a/titles/idz/handlers/update_ui_report.py b/titles/idz/handlers/update_ui_report.py new file mode 100644 index 0000000..7e99b40 --- /dev/null +++ b/titles/idz/handlers/update_ui_report.py @@ -0,0 +1,18 @@ +import struct + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig +from ..const import IDZConstants + + +class IDZHandleUpdateUIReport(IDZHandlerBase): + cmd_codes = [0x0084, 0x0084, 0x007E, 0x007E] + name = "update_ui_report" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + return ret diff --git a/titles/idz/handlers/update_user_log.py b/titles/idz/handlers/update_user_log.py new file mode 100644 index 0000000..c862f52 --- /dev/null +++ b/titles/idz/handlers/update_user_log.py @@ -0,0 +1,18 @@ +import struct + +from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig +from ..const import IDZConstants + + +class IDZHandleUpdateUserLog(IDZHandlerBase): + cmd_codes = [0x00BD, 0x00BD, 0x00AB, 0x00B3] + name = "update_user_log" + + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None: + super().__init__(core_cfg, game_cfg, version) + + def handle(self, data: bytes) -> bytearray: + ret = super().handle(data) + return ret diff --git a/titles/idz/index.py b/titles/idz/index.py new file mode 100644 index 0000000..62473f9 --- /dev/null +++ b/titles/idz/index.py @@ -0,0 +1,179 @@ +from twisted.web.http import Request +import yaml +import logging +import coloredlogs +from logging.handlers import TimedRotatingFileHandler +from os import path +from typing import Tuple, List, Dict +from twisted.internet import reactor, endpoints +from twisted.web import server, resource +import importlib + +from core.config import CoreConfig +from core.title import BaseServlet +from .config import IDZConfig +from .const import IDZConstants +from .userdb import IDZUserDBFactory, IDZKey +from .echo import IDZEcho + + +class IDZServlet(BaseServlet): + def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: + super().__init__(core_cfg, cfg_dir) + self.game_cfg = IDZConfig() + if path.exists(f"{cfg_dir}/{IDZConstants.CONFIG_NAME}"): + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{IDZConstants.CONFIG_NAME}")) + ) + + self.logger = logging.getLogger("idz") + if not hasattr(self.logger, "inited"): + log_fmt_str = "[%(asctime)s] IDZ | %(levelname)s | %(message)s" + log_fmt = logging.Formatter(log_fmt_str) + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.core_cfg.server.log_dir, "idz"), + encoding="utf8", + when="d", + backupCount=10, + ) + + self.rsa_keys: List[IDZKey] = [] + + fileHandler.setFormatter(log_fmt) + + consoleHandler = logging.StreamHandler() + consoleHandler.setFormatter(log_fmt) + + self.logger.addHandler(fileHandler) + self.logger.addHandler(consoleHandler) + + self.logger.setLevel(self.game_cfg.server.loglevel) + coloredlogs.install( + level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str + ) + self.logger.inited = True + + @classmethod + def rsaHashKeyN(cls, data): + hash_ = 0 + for i in data: + hash_ = ( + hash_ * IDZConstants.HASH_MUL + (i ^ IDZConstants.HASH_XOR) + ^ IDZConstants.HASH_LUT[i & 0xF] + ) + hash_ &= 0xFFFFFFFF + return hash_ + + @classmethod + def is_game_enabled( + cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str + ) -> bool: + game_cfg = IDZConfig() + if path.exists(f"{cfg_dir}/{IDZConstants.CONFIG_NAME}"): + game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{IDZConstants.CONFIG_NAME}")) + ) + + if not game_cfg.server.enable: + return False + + if len(game_cfg.rsa_keys) <= 0 or not game_cfg.server.aes_key: + logging.getLogger("idz").error("IDZ: No RSA/AES keys! IDZ cannot start") + return False + + return True + + def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: + return[ + [("render_GET", "/idz/news/{endpoint:.*?}", {}), + ("render_GET", "/idz/error", {})], + [] + ] + + def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: + hostname = ( + self.core_cfg.title.hostname + if not self.game_cfg.server.hostname + else self.game_cfg.server.hostname + ) + return ( + f"", + f"{hostname}:{self.game_cfg.ports.userdb}", + ) + + def setup(self): + for key in self.game_cfg.rsa_keys: + if "N" not in key or "d" not in key or "e" not in key: + self.logger.error(f"Invalid IDZ key {key}") + continue + + hashN = self.rsaHashKeyN(str(key["N"]).encode()) + self.rsa_keys.append(IDZKey(key["N"], key["d"], key["e"], hashN)) + + if len(self.rsa_keys) <= 0: + self.logger.error("No valid RSA keys provided! IDZ cannot start!") + return + + handler_map = [{} for _ in range(IDZConstants.NUM_VERS)] + handler_mod = mod = importlib.import_module(f"titles.idz.handlers") + + for cls_name in dir(handler_mod): + if cls_name.startswith("__"): + continue + + try: + mod = getattr(handler_mod, cls_name) + mod_cmds: List = getattr(mod, "cmd_codes") + while len(mod_cmds) < IDZConstants.NUM_VERS: + mod_cmds.append(None) + + for i in range(len(mod_cmds)): + if mod_cmds[i] is None: + mod_cmds[i] = mod_cmds[i - 1] + + handler_map[i][mod_cmds[i]] = mod + + except AttributeError as e: + continue + + endpoints.serverFromString( + reactor, + f"tcp:{self.game_cfg.ports.userdb}:interface={self.core_cfg.server.listen_address}", + ).listen( + IDZUserDBFactory(self.core_cfg, self.game_cfg, self.rsa_keys, handler_map) + ) + + reactor.listenUDP( + self.game_cfg.ports.echo, IDZEcho(self.core_cfg, self.game_cfg) + ) + reactor.listenUDP( + self.game_cfg.ports.echo + 1, IDZEcho(self.core_cfg, self.game_cfg) + ) + reactor.listenUDP( + self.game_cfg.ports.match, IDZEcho(self.core_cfg, self.game_cfg) + ) + reactor.listenUDP( + self.game_cfg.ports.userdb + 1, IDZEcho(self.core_cfg, self.game_cfg) + ) + + self.logger.info(f"UserDB Listening on port {self.game_cfg.ports.userdb}") + + def render_GET(self, request: Request, game_code: str, matchers: Dict) -> bytes: + url_path = matchers['endpoint'] + self.logger.info(f"IDZ GET request: {url_path}") + request.responseHeaders.setRawHeaders( + "Content-Type", [b"text/plain; charset=utf-8"] + ) + request.responseHeaders.setRawHeaders( + "Last-Modified", [b"Sun, 23 Apr 2023 05:33:20 GMT"] + ) + + news = ( + self.game_cfg.server.news + if self.game_cfg.server.news + else f"Welcome to Initial D Arcade Stage Zero on {self.core_cfg.server.name}!" + ) + news += "\r\n" + news = "1979/01/01 00:00:00 2099/12/31 23:59:59 " + news + + return news.encode() diff --git a/titles/idz/match.py b/titles/idz/match.py new file mode 100644 index 0000000..e69de29 diff --git a/titles/idz/userdb.py b/titles/idz/userdb.py new file mode 100644 index 0000000..fd555a1 --- /dev/null +++ b/titles/idz/userdb.py @@ -0,0 +1,201 @@ +from twisted.internet.protocol import Factory, Protocol +import logging, coloredlogs +from Crypto.Cipher import AES +import struct +from typing import Dict, Optional, List, Type +from twisted.web import server, resource +from twisted.internet import reactor, endpoints +from twisted.web.http import Request +from routes import Mapper +import random +from os import walk +import importlib + +from core.config import CoreConfig +from .database import IDZData +from .config import IDZConfig +from .const import IDZConstants +from .handlers import IDZHandlerBase + +HANDLER_MAP: List[Dict] + + +class IDZKey: + def __init__(self, n, d, e, hashN: int) -> None: + self.N = n + self.d = d + self.e = e + self.hashN = hashN + + +class IDZUserDBProtocol(Protocol): + def __init__( + self, + core_cfg: CoreConfig, + game_cfg: IDZConfig, + keys: List[IDZKey], + handlers: List[Dict], + ) -> None: + self.logger = logging.getLogger("idz") + self.core_config = core_cfg + self.game_config = game_cfg + self.rsa_keys = keys + self.handlers = handlers + self.static_key = bytes.fromhex(self.game_config.server.aes_key) + self.version = None + self.version_internal = None + self.skip_next = False + + def append_padding(self, data: bytes): + """Appends 0s to the end of the data until it's at the correct size""" + length = struct.unpack_from(" None: + self.logger.debug(f"{self.transport.getPeer().host} Connected") + base = 0 + + for i in range(len(self.static_key) - 1): + shift = 8 * i + byte = self.static_key[i] + + base |= byte << shift + + rsa_key = random.choice(self.rsa_keys) + key_enc: int = pow(base, rsa_key.e, rsa_key.N) + result = ( + key_enc.to_bytes(0x40, "little") + + struct.pack(" None: + self.logger.debug( + f"{self.transport.getPeer().host} Disconnected - {reason.value}" + ) + + def dataReceived(self, data: bytes) -> None: + self.logger.debug(f"Receive data {data.hex()}") + crypt = AES.new(self.static_key, AES.MODE_ECB) + + try: + data_dec = crypt.decrypt(data) + + except Exception as e: + self.logger.error(f"Failed to decrypt UserDB request from {self.transport.getPeer().host} because {e} - {data.hex()}") + + self.logger.debug(f"Decrypt data {data_dec.hex()}") + + magic = struct.unpack_from(" None: + self.core_config = cfg + self.game_config = game_cfg + self.keys = keys + self.handlers = handlers + + def buildProtocol(self, addr): + return IDZUserDBProtocol( + self.core_config, self.game_config, self.keys, self.handlers + ) + + +class IDZUserDBWeb(resource.Resource): + def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig): + super().__init__() + self.isLeaf = True + self.core_config = core_cfg + self.game_config = game_cfg + self.logger = logging.getLogger("idz") + + def render_POST(self, request: Request) -> bytes: + self.logger.info( + f"IDZUserDBWeb POST from {request.getClientAddress().host} to {request.uri} with data {request.content.getvalue()}" + ) + return b"" + + def render_GET(self, request: Request) -> bytes: + self.logger.info( + f"IDZUserDBWeb GET from {request.getClientAddress().host} to {request.uri}" + ) + return b"" diff --git a/titles/mai2/__init__.py b/titles/mai2/__init__.py index 1c51244..4857644 100644 --- a/titles/mai2/__init__.py +++ b/titles/mai2/__init__.py @@ -6,13 +6,14 @@ from titles.mai2.read import Mai2Reader index = Mai2Servlet database = Mai2Data reader = Mai2Reader - -use_default_title = True -include_protocol = True -title_secure = False -game_codes = [Mai2Constants.GAME_CODE] -trailing_slash = True -use_default_host = False -host = "" - -current_schema_version = 1 \ No newline at end of file +game_codes = [ + Mai2Constants.GAME_CODE_DX, + Mai2Constants.GAME_CODE_FINALE, + Mai2Constants.GAME_CODE_MILK, + Mai2Constants.GAME_CODE_MURASAKI, + Mai2Constants.GAME_CODE_PINK, + Mai2Constants.GAME_CODE_ORANGE, + Mai2Constants.GAME_CODE_GREEN, + Mai2Constants.GAME_CODE, +] +current_schema_version = 8 diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 4b0b81c..2f40176 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -1,39 +1,77 @@ -from datetime import datetime, date, timedelta -from typing import Dict +from datetime import datetime, timedelta +from typing import Any, Dict, List import logging +from base64 import b64decode +from os import path, stat, remove +from PIL import ImageFile +import pytz from core.config import CoreConfig -from titles.mai2.const import Mai2Constants -from titles.mai2.config import Mai2Config -from titles.mai2.database import Mai2Data +from core.utils import Utils +from .const import Mai2Constants +from .config import Mai2Config +from .database import Mai2Data -class Mai2Base(): + +class Mai2Base: def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: self.core_config = cfg self.game_config = game_cfg - self.game = Mai2Constants.GAME_CODE - self.version = Mai2Constants.VER_MAIMAI_DX + self.version = Mai2Constants.VER_MAIMAI self.data = Mai2Data(cfg) self.logger = logging.getLogger("mai2") + self.can_deliver = False + self.can_usbdl = False + self.old_server = "" + self.date_time_format = "%Y-%m-%d %H:%M:%S" + + if not self.core_config.server.is_using_proxy and Utils.get_title_port(self.core_config) != 80: + self.old_server = f"http://{self.core_config.title.hostname}:{Utils.get_title_port(cfg)}/197/MaimaiServlet/" + + else: + self.old_server = f"http://{self.core_config.title.hostname}/197/MaimaiServlet/" def handle_get_game_setting_api_request(self, data: Dict): - reboot_start = date.strftime(datetime.now() + timedelta(hours=3), Mai2Constants.DATE_TIME_FORMAT) - reboot_end = date.strftime(datetime.now() + timedelta(hours=4), Mai2Constants.DATE_TIME_FORMAT) + # if reboot start/end time is not defined use the default behavior of being a few hours ago + if self.core_config.title.reboot_start_time == "" or self.core_config.title.reboot_end_time == "": + reboot_start = datetime.strftime( + datetime.utcnow() + timedelta(hours=6), self.date_time_format + ) + reboot_end = datetime.strftime( + datetime.utcnow() + timedelta(hours=7), self.date_time_format + ) + else: + # get current datetime in JST + current_jst = datetime.now(pytz.timezone('Asia/Tokyo')).date() + + # parse config start/end times into datetime + reboot_start_time = datetime.strptime(self.core_config.title.reboot_start_time, "%H:%M") + reboot_end_time = datetime.strptime(self.core_config.title.reboot_end_time, "%H:%M") + + # offset datetimes with current date/time + reboot_start_time = reboot_start_time.replace(year=current_jst.year, month=current_jst.month, day=current_jst.day, tzinfo=pytz.timezone('Asia/Tokyo')) + reboot_end_time = reboot_end_time.replace(year=current_jst.year, month=current_jst.month, day=current_jst.day, tzinfo=pytz.timezone('Asia/Tokyo')) + + # create strings for use in gameSetting + reboot_start = reboot_start_time.strftime(self.date_time_format) + reboot_end = reboot_end_time.strftime(self.date_time_format) + + return { - "gameSetting": { - "isMaintenance": "false", - "requestInterval": 10, - "rebootStartTime": reboot_start, - "rebootEndTime": reboot_end, - "movieUploadLimit": 10000, - "movieStatus": 0, - "movieServerUri": "", - "deliverServerUri": "", - "oldServerUri": "", - "usbDlServerUri": "", - "rebootInterval": 0 + "isDevelop": False, + "isAouAccession": False, + "gameSetting": { + "isMaintenance": False, + "requestInterval": 1800, + "rebootStartTime": reboot_start, + "rebootEndTime": reboot_end, + "movieUploadLimit": 100, + "movieStatus": 1, + "movieServerUri": self.old_server + "api/movie" if self.game_config.uploads.movies else "movie", + "deliverServerUri": self.old_server + "deliver/" if self.can_deliver and self.game_config.deliver.enable else "", + "oldServerUri": self.old_server + "old", + "usbDlServerUri": self.old_server + "usbdl/" if self.can_deliver and self.game_config.deliver.udbdl_enable else "", }, - "isAouAccession": "true", } def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: @@ -46,82 +84,94 @@ class Mai2Base(): def handle_get_game_event_api_request(self, data: Dict) -> Dict: events = self.data.static.get_enabled_events(self.version) events_lst = [] - if events is None: return {"type": data["type"], "length": 0, "gameEventList": []} + if events is None or not events: + self.logger.warning("No enabled events, did you run the reader?") + return {"type": data["type"], "length": 0, "gameEventList": []} for event in events: - events_lst.append({ - "type": event["type"], - "id": event["eventId"], - "startDate": "2017-12-05 07:00:00.0", - "endDate": "2099-12-31 00:00:00.0" - }) + events_lst.append( + { + "type": event["type"], + "id": event["eventId"], + # actually use the startDate from the import so it + # properly shows all the events when new ones are imported + "startDate": datetime.strftime( + event["startDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0" + ), + "endDate": "2099-12-31 00:00:00.0", + } + ) - return {"type": data["type"], "length": len(events_lst), "gameEventList": events_lst} + return { + "type": data["type"], + "length": len(events_lst), + "gameEventList": events_lst, + } def handle_get_game_ng_music_id_api_request(self, data: Dict) -> Dict: return {"length": 0, "musicIdList": []} def handle_get_game_charge_api_request(self, data: Dict) -> Dict: game_charge_list = self.data.static.get_enabled_tickets(self.version, 1) - if game_charge_list is None: return {"length": 0, "gameChargeList": []} + if game_charge_list is None: + return {"length": 0, "gameChargeList": []} charge_list = [] - for x in range(len(game_charge_list)): - charge_list.append({ - "orderId": x, - "chargeId": game_charge_list[x]["ticketId"], - "price": game_charge_list[x]["price"], - "startDate": "2017-12-05 07:00:00.0", - "endDate": "2099-12-31 00:00:00.0" - }) + for i, charge in enumerate(game_charge_list): + charge_list.append( + { + "orderId": i + 1, + "chargeId": charge["ticketId"], + "price": charge["price"], + "startDate": "2017-12-05 07:00:00.0", + "endDate": "2099-12-31 00:00:00.0", + } + ) return {"length": len(charge_list), "gameChargeList": charge_list} def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: - pass + return {"returnCode": 1, "apiName": "UpsertClientSettingApi"} def handle_upsert_client_upload_api_request(self, data: Dict) -> Dict: - pass + return {"returnCode": 1, "apiName": "UpsertClientUploadApi"} def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: - pass + return {"returnCode": 1, "apiName": "UpsertClientBookkeepingApi"} def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict: - pass + return {"returnCode": 1, "apiName": "UpsertClientTestmodeApi"} def handle_get_user_preview_api_request(self, data: Dict) -> Dict: - p = self.data.profile.get_profile_detail(data["userId"], self.version) - o = self.data.profile.get_profile_option(data["userId"], self.version) - if p is None or o is None: return {} # Register + p = self.data.profile.get_profile_detail(data["userId"], self.version, False) + w = self.data.profile.get_web_option(data["userId"], self.version) + if p is None or w is None: + return {} # Register profile = p._asdict() - option = o._asdict() + web_opt = w._asdict() return { "userId": data["userId"], "userName": profile["userName"], "isLogin": False, - "lastGameId": profile["lastGameId"], "lastDataVersion": profile["lastDataVersion"], - "lastRomVersion": profile["lastRomVersion"], - "lastLoginDate": profile["lastLoginDate"], + "lastLoginDate": profile["lastPlayDate"], "lastPlayDate": profile["lastPlayDate"], "playerRating": profile["playerRating"], - "nameplateId": 0, # Unused - "iconId": profile["iconId"], - "trophyId": 0, # Unused - "partnerId": profile["partnerId"], + "nameplateId": profile["nameplateId"], "frameId": profile["frameId"], - "dispRate": option["dispRate"], # 0: all/begin, 1: disprate, 2: dispDan, 3: hide, 4: end - "totalAwake": profile["totalAwake"], - "isNetMember": profile["isNetMember"], - "dailyBonusDate": profile["dailyBonusDate"], - "headPhoneVolume": option["headPhoneVolume"], - "isInherit": False, # Not sure what this is or does?? - "banState": profile["banState"] if profile["banState"] is not None else 0 # New with uni+ + "iconId": profile["iconId"], + "trophyId": profile["trophyId"], + "dispRate": web_opt["dispRate"], # 0: all, 1: dispRate, 2: dispDan, 3: hide + "dispRank": web_opt["dispRank"], + "dispHomeRanker": web_opt["dispHomeRanker"], + "dispTotalLv": web_opt["dispTotalLv"], + "totalLv": profile["totalLv"], } - + def handle_user_login_api_request(self, data: Dict) -> Dict: profile = self.data.profile.get_profile_detail(data["userId"], self.version) + consec = self.data.profile.get_consec_login(data["userId"], self.version) if profile is not None: lastLoginDate = profile["lastLoginDate"] @@ -132,207 +182,402 @@ class Mai2Base(): else: loginCt = 0 lastLoginDate = "2017-12-05 07:00:00.0" + + if consec is None or not consec: + consec_ct = 1 + + else: + lastlogindate_ = datetime.strptime(profile["lastLoginDate"], "%Y-%m-%d %H:%M:%S.%f").timestamp() + today_midnight = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0).timestamp() + yesterday_midnight = today_midnight - 86400 + + if lastlogindate_ < today_midnight: + consec_ct = consec['logins'] + 1 + self.data.profile.add_consec_login(data["userId"], self.version) + + elif lastlogindate_ < yesterday_midnight: + consec_ct = 1 + self.data.profile.reset_consec_login(data["userId"], self.version) + + else: + consec_ct = consec['logins'] + return { "returnCode": 1, "lastLoginDate": lastLoginDate, "loginCount": loginCt, - "consecutiveLoginCount": 0, # We don't really have a way to track this... - "loginId": loginCt # Used with the playlog! + "consecutiveLoginCount": consec_ct, # Number of consecutive days we've logged in. } - + def handle_upload_user_playlog_api_request(self, data: Dict) -> Dict: user_id = data["userId"] playlog = data["userPlaylog"] self.data.score.put_playlog(user_id, playlog) + return {"returnCode": 1, "apiName": "UploadUserPlaylogApi"} + + def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: + user_id = data["userId"] + charge = data["userCharge"] + + # remove the ".0" from the date string, festival only? + charge["purchaseDate"] = charge["purchaseDate"].replace(".0", "") + self.data.item.put_charge( + user_id, + charge["chargeId"], + charge["stock"], + charge["purchaseDate"], # Ideally these should be datetimes, but db was + charge["validDate"] # set up with them being str, so str it is for now + ) + + return {"returnCode": 1, "apiName": "UpsertUserChargelogApi"} + def handle_upsert_user_all_api_request(self, data: Dict) -> Dict: user_id = data["userId"] upsert = data["upsertUserAll"] + + if int(user_id) & 1000000000001 == 1000000000001: + self.logger.info("Guest play, ignoring.") + return {"returnCode": 1, "apiName": "UpsertUserAllApi"} if "userData" in upsert and len(upsert["userData"]) > 0: - upsert["userData"][0]["isNetMember"] = 1 upsert["userData"][0].pop("accessCode") - self.data.profile.put_profile_detail(user_id, self.version, upsert["userData"][0]) + upsert["userData"][0].pop("userId") + + self.data.profile.put_profile_detail( + user_id, self.version, upsert["userData"][0], False + ) + if "userWebOption" in upsert and len(upsert["userWebOption"]) > 0: + upsert["userWebOption"][0]["isNetMember"] = True + self.data.profile.put_web_option( + user_id, self.version, upsert["userWebOption"][0] + ) + + if "userGradeStatusList" in upsert and len(upsert["userGradeStatusList"]) > 0: + self.data.profile.put_grade_status( + user_id, upsert["userGradeStatusList"][0] + ) + + if "userBossList" in upsert and len(upsert["userBossList"]) > 0: + self.data.profile.put_boss_list( + user_id, upsert["userBossList"][0] + ) + + if "userPlaylogList" in upsert and len(upsert["userPlaylogList"]) > 0: + for playlog in upsert["userPlaylogList"]: + self.data.score.put_playlog( + user_id, playlog, False + ) + if "userExtend" in upsert and len(upsert["userExtend"]) > 0: - self.data.profile.put_profile_extend(user_id, self.version, upsert["userExtend"][0]) + self.data.profile.put_profile_extend( + user_id, self.version, upsert["userExtend"][0] + ) if "userGhost" in upsert: for ghost in upsert["userGhost"]: - self.data.profile.put_profile_extend(user_id, self.version, ghost) - + self.data.profile.put_profile_ghost(user_id, self.version, ghost) + + if "userRecentRatingList" in upsert: + self.data.profile.put_recent_rating(user_id, upsert["userRecentRatingList"]) + if "userOption" in upsert and len(upsert["userOption"]) > 0: - self.data.profile.put_profile_option(user_id, self.version, upsert["userOption"][0]) + upsert["userOption"][0].pop("userId") + self.data.profile.put_profile_option( + user_id, self.version, upsert["userOption"][0], False + ) if "userRatingList" in upsert and len(upsert["userRatingList"]) > 0: - self.data.profile.put_profile_rating(user_id, self.version, upsert["userRatingList"][0]) + self.data.profile.put_profile_rating( + user_id, self.version, upsert["userRatingList"][0] + ) if "userActivityList" in upsert and len(upsert["userActivityList"]) > 0: - for k,v in upsert["userActivityList"][0].items(): - for act in v: - self.data.profile.put_profile_activity(user_id, act) + for act in upsert["userActivityList"]: + self.data.profile.put_profile_activity(user_id, act) - if upsert["isNewCharacterList"] and int(upsert["isNewCharacterList"]) > 0: + if "userChargeList" in upsert and len(upsert["userChargeList"]) > 0: + for charge in upsert["userChargeList"]: + # remove the ".0" from the date string, festival only? + charge["purchaseDate"] = charge["purchaseDate"].replace(".0", "") + self.data.item.put_charge( + user_id, + charge["chargeId"], + charge["stock"], + charge["purchaseDate"], + charge["validDate"] + ) + + if "userCharacterList" in upsert and len(upsert["userCharacterList"]) > 0: for char in upsert["userCharacterList"]: - self.data.item.put_character(user_id, char["characterId"], char["level"], char["awakening"], char["useCount"]) + self.data.item.put_character_( + user_id, + char + ) - if upsert["isNewItemList"] and int(upsert["isNewItemList"]) > 0: + if "userItemList" in upsert and len(upsert["userItemList"]) > 0: for item in upsert["userItemList"]: - self.data.item.put_item(user_id, int(item["itemKind"]), item["itemId"], item["stock"], item["isValid"]) + self.data.item.put_item( + user_id, + int(item["itemKind"]), + item["itemId"], + item["stock"], + True + ) - if upsert["isNewLoginBonusList"] and int(upsert["isNewLoginBonusList"]) > 0: + if "userLoginBonusList" in upsert and len(upsert["userLoginBonusList"]) > 0: for login_bonus in upsert["userLoginBonusList"]: - self.data.item.put_login_bonus(user_id, login_bonus["bonusId"], login_bonus["point"], login_bonus["isCurrent"], login_bonus["isComplete"]) + self.data.item.put_login_bonus( + user_id, + login_bonus["bonusId"], + login_bonus["point"], + login_bonus["isCurrent"], + login_bonus["isComplete"], + ) - if upsert["isNewMapList"] and int(upsert["isNewMapList"]) > 0: + if "userMapList" in upsert and len(upsert["userMapList"]) > 0: for map in upsert["userMapList"]: - self.data.item.put_map(user_id, map["mapId"], map["distance"], map["isLock"], map["isClear"], map["isComplete"]) - - if upsert["isNewMusicDetailList"] and int(upsert["isNewMusicDetailList"]) > 0: + self.data.item.put_map( + user_id, + map["mapId"], + map["distance"], + map["isLock"], + map["isClear"], + map["isComplete"], + ) + + if "userMusicDetailList" in upsert and len(upsert["userMusicDetailList"]) > 0: for music in upsert["userMusicDetailList"]: - self.data.score.put_best_score(user_id, music) - - if upsert["isNewCourseList"] and int(upsert["isNewCourseList"]) > 0: + self.data.score.put_best_score(user_id, music, False) + + if "userCourseList" in upsert and len(upsert["userCourseList"]) > 0: for course in upsert["userCourseList"]: self.data.score.put_course(user_id, course) - - if upsert["isNewFavoriteList"] and int(upsert["isNewFavoriteList"]) > 0: + + if "userFavoriteList" in upsert and len(upsert["userFavoriteList"]) > 0: for fav in upsert["userFavoriteList"]: self.data.item.put_favorite(user_id, fav["kind"], fav["itemIdList"]) - if "isNewFriendSeasonRankingList" in upsert and int(upsert["isNewFriendSeasonRankingList"]) > 0: + if ( + "userFriendSeasonRankingList" in upsert + and len(upsert["userFriendSeasonRankingList"]) > 0 + ): for fsr in upsert["userFriendSeasonRankingList"]: - pass + fsr["recordDate"] = ( + datetime.strptime( + fsr["recordDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0" + ), + ) + self.data.item.put_friend_season_ranking(user_id, fsr) + + return {"returnCode": 1, "apiName": "UpsertUserAllApi"} def handle_user_logout_api_request(self, data: Dict) -> Dict: - pass + return {"returnCode": 1} def handle_get_user_data_api_request(self, data: Dict) -> Dict: - profile = self.data.profile.get_profile_detail(data["userId"], self.version) - if profile is None: return + profile = self.data.profile.get_profile_detail(data["userId"], self.version, False) + if profile is None: + return profile_dict = profile._asdict() profile_dict.pop("id") profile_dict.pop("user") profile_dict.pop("version") - return { - "userId": data["userId"], - "userData": profile_dict - } + return {"userId": data["userId"], "userData": profile_dict} def handle_get_user_extend_api_request(self, data: Dict) -> Dict: extend = self.data.profile.get_profile_extend(data["userId"], self.version) - if extend is None: return + if extend is None: + return extend_dict = extend._asdict() extend_dict.pop("id") extend_dict.pop("user") extend_dict.pop("version") - return { - "userId": data["userId"], - "userExtend": extend_dict - } + return {"userId": data["userId"], "userExtend": extend_dict} def handle_get_user_option_api_request(self, data: Dict) -> Dict: - options = self.data.profile.get_profile_option(data["userId"], self.version) - if options is None: return + options = self.data.profile.get_profile_option(data["userId"], self.version, False) + if options is None: + return options_dict = options._asdict() options_dict.pop("id") options_dict.pop("user") options_dict.pop("version") - return { - "userId": data["userId"], - "userOption": options_dict - } + return {"userId": data["userId"], "userOption": options_dict} def handle_get_user_card_api_request(self, data: Dict) -> Dict: - return {"userId": data["userId"], "nextIndex": 0, "userCardList": []} + user_cards = self.data.item.get_cards(data["userId"]) + if user_cards is None: + return {"userId": data["userId"], "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 { + "userId": data["userId"], + "nextIndex": next_idx, + "userCardList": card_list[start_idx:end_idx], + } def handle_get_user_charge_api_request(self, data: Dict) -> Dict: - return {"userId": data["userId"], "length": 0, "userChargeList": []} + user_charges = self.data.item.get_charges(data["userId"]) + if user_charges is None: + return {"userId": data["userId"], "length": 0, "userChargeList": []} + + user_charge_list = [] + for charge in user_charges: + tmp = charge._asdict() + tmp.pop("id") + tmp.pop("user") + + user_charge_list.append(tmp) + + return { + "userId": data["userId"], + "length": len(user_charge_list), + "userChargeList": user_charge_list, + } + + def handle_get_user_present_api_request(self, data: Dict) -> Dict: + return { "userId": data.get("userId", 0), "length": 0, "userPresentList": []} + + def handle_get_transfer_friend_api_request(self, data: Dict) -> Dict: + return {} + + def handle_get_user_present_event_api_request(self, data: Dict) -> Dict: + return { "userId": data.get("userId", 0), "length": 0, "userPresentEventList": []} + + def handle_get_user_boss_api_request(self, data: Dict) -> Dict: + b = self.data.profile.get_boss_list(data["userId"]) + if b is None: + return { "userId": data.get("userId", 0), "userBossData": {}} + boss_lst = b._asdict() + boss_lst.pop("id") + boss_lst.pop("user") + + return { "userId": data.get("userId", 0), "userBossData": boss_lst} def handle_get_user_item_api_request(self, data: Dict) -> Dict: kind = int(data["nextIndex"] / 10000000000) next_idx = int(data["nextIndex"] % 10000000000) - user_items = self.data.item.get_items(data["userId"], kind) - user_item_list = [] - next_idx = 0 + user_item_list = self.data.item.get_items(data["userId"], kind) - for x in range(next_idx, data["maxCount"]): - try: - user_item_list.append({"item_kind": user_items[x]["item_kind"], "item_id": user_items[x]["item_id"], - "stock": user_items[x]["stock"], "isValid": user_items[x]["is_valid"]}) - except: break - - if len(user_item_list) == data["maxCount"]: - next_idx = data["nextIndex"] + data["maxCount"] + 1 + 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 - return {"userId": data["userId"], "nextIndex": next_idx, "itemKind": kind, "userItemList": user_item_list} + xout = kind * 10000000000 + next_idx + len(items) - def handle_get_user_character_api_request(self, data: Dict) -> Dict: - characters = self.data.item.get_characters(data["userId"]) - chara_list = [] - for chara in characters: - chara_list.append({ - "characterId": chara["character_id"], - "level": chara["level"], - "awakening": chara["awakening"], - "useCount": chara["use_count"], - }) - - return {"userId": data["userId"], "userCharacterList": chara_list} - - def handle_get_user_favorite_api_request(self, data: Dict) -> Dict: - favorites = self.data.item.get_favorites(data["userId"], data["itemKind"]) - if favorites is None: return - - userFavs = [] - for fav in favorites: - userFavs.append({ - "userId": data["userId"], - "itemKind": fav["itemKind"], - "itemIdList": fav["itemIdList"] - }) + if len(items) < int(data["maxCount"]): + next_idx = 0 + else: + next_idx = xout return { "userId": data["userId"], - "userFavoriteData": userFavs + "nextIndex": next_idx, + "itemKind": kind, + "userItemList": items, } + def handle_get_user_character_api_request(self, data: Dict) -> Dict: + characters = self.data.item.get_characters(data["userId"]) + + chara_list = [] + for chara in characters: + tmp = chara._asdict() + tmp.pop("id") + tmp.pop("user") + tmp.pop("awakening") + tmp.pop("useCount") + chara_list.append(tmp) + + return {"userId": data["userId"], "userCharacterList": chara_list} + + def handle_get_user_favorite_api_request(self, data: Dict) -> Dict: + favorites = self.data.item.get_favorites(data["userId"], data["itemKind"]) + if favorites is None: + return + + userFavs = [] + for fav in favorites: + userFavs.append( + { + "userId": data["userId"], + "itemKind": fav["itemKind"], + "itemIdList": fav["itemIdList"], + } + ) + + return {"userId": data["userId"], "userFavoriteData": userFavs} + def handle_get_user_ghost_api_request(self, data: Dict) -> Dict: ghost = self.data.profile.get_profile_ghost(data["userId"], self.version) - if ghost is None: return + if ghost is None: + return ghost_dict = ghost._asdict() ghost_dict.pop("user") ghost_dict.pop("id") ghost_dict.pop("version_int") - return { - "userId": data["userId"], - "userGhost": ghost_dict - } + return {"userId": data["userId"], "userGhost": ghost_dict} + + def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict: + rating = self.data.profile.get_recent_rating(data["userId"]) + if rating is None: + return + + r = rating._asdict() + lst = r.get("userRecentRatingList", []) + + return {"userId": data["userId"], "length": len(lst), "userRecentRatingList": lst} def handle_get_user_rating_api_request(self, data: Dict) -> Dict: rating = self.data.profile.get_profile_rating(data["userId"], self.version) - if rating is None: return + if rating is None: + return rating_dict = rating._asdict() rating_dict.pop("user") rating_dict.pop("id") rating_dict.pop("version") - return { - "userId": data["userId"], - "userRating": rating_dict - } + return {"userId": data["userId"], "userRating": rating_dict} def handle_get_user_activity_api_request(self, data: Dict) -> Dict: """ @@ -340,34 +585,32 @@ class Mai2Base(): """ playlist = self.data.profile.get_profile_activity(data["userId"], 1) musiclist = self.data.profile.get_profile_activity(data["userId"], 2) - if playlist is None or musiclist is None: return + if playlist is None or musiclist is None: + return plst = [] mlst = [] for play in playlist: - tmp = play._asdict() + tmp = play._asdict() tmp["id"] = tmp["activityId"] tmp.pop("activityId") tmp.pop("user") plst.append(tmp) for music in musiclist: - tmp = music._asdict() + tmp = music._asdict() tmp["id"] = tmp["activityId"] tmp.pop("activityId") tmp.pop("user") mlst.append(tmp) - return { - "userActivity": { - "playList": plst, - "musicList": mlst - } - } + return {"userActivity": {"playList": plst, "musicList": mlst}} def handle_get_user_course_api_request(self, data: Dict) -> Dict: user_courses = self.data.score.get_courses(data["userId"]) + if user_courses is None: + return {"userId": data["userId"], "nextIndex": 0, "userCourseList": []} course_list = [] for course in user_courses: @@ -384,94 +627,260 @@ class Mai2Base(): def handle_get_user_friend_season_ranking_api_request(self, data: Dict) -> Dict: friend_season_ranking = self.data.item.get_friend_season_ranking(data["userId"]) - friend_season_ranking_list = [] - next_index = 0 + if friend_season_ranking is None: + return { + "userId": data["userId"], + "nextIndex": 0, + "userFriendSeasonRankingList": [], + } - for x in range(data["nextIndex"], data["maxCount"] + data["nextIndex"]): - try: - friend_season_ranking_list.append({ - "mapId": friend_season_ranking_list[x]["map_id"], - "distance": friend_season_ranking_list[x]["distance"], - "isLock": friend_season_ranking_list[x]["is_lock"], - "isClear": friend_season_ranking_list[x]["is_clear"], - "isComplete": friend_season_ranking_list[x]["is_complete"], - }) - except: + friend_season_ranking_list = [] + next_idx = int(data["nextIndex"]) + max_ct = int(data["maxCount"]) + + for x in range(next_idx, len(friend_season_ranking)): + tmp = friend_season_ranking[x]._asdict() + tmp.pop("user") + tmp.pop("id") + tmp["recordDate"] = datetime.strftime( + tmp["recordDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0" + ) + friend_season_ranking_list.append(tmp) + + if len(friend_season_ranking_list) >= max_ct: break - # We're capped and still have some left to go - if len(friend_season_ranking_list) == data["maxCount"] and len(friend_season_ranking) > data["maxCount"] + data["nextIndex"]: - next_index = data["maxCount"] + data["nextIndex"] + if len(friend_season_ranking) >= next_idx + max_ct: + next_idx += max_ct + else: + next_idx = 0 - return {"userId": data["userId"], "nextIndex": next_index, "userFriendSeasonRankingList": friend_season_ranking_list} + return { + "userId": data["userId"], + "nextIndex": next_idx, + "userFriendSeasonRankingList": friend_season_ranking_list, + } def handle_get_user_map_api_request(self, data: Dict) -> Dict: maps = self.data.item.get_maps(data["userId"]) - map_list = [] - next_index = 0 + if maps is None: + return { + "userId": data["userId"], + "nextIndex": 0, + "userMapList": [], + } - for x in range(data["nextIndex"], data["maxCount"] + data["nextIndex"]): - try: - map_list.append({ - "mapId": maps[x]["map_id"], - "distance": maps[x]["distance"], - "isLock": maps[x]["is_lock"], - "isClear": maps[x]["is_clear"], - "isComplete": maps[x]["is_complete"], - }) - except: + map_list = [] + next_idx = int(data["nextIndex"]) + max_ct = int(data["maxCount"]) + + for x in range(next_idx, len(maps)): + tmp = maps[x]._asdict() + tmp.pop("user") + tmp.pop("id") + map_list.append(tmp) + + if len(map_list) >= max_ct: break - # We're capped and still have some left to go - if len(map_list) == data["maxCount"] and len(maps) > data["maxCount"] + data["nextIndex"]: - next_index = data["maxCount"] + data["nextIndex"] + if len(maps) >= next_idx + max_ct: + next_idx += max_ct + else: + next_idx = 0 - return {"userId": data["userId"], "nextIndex": next_index, "userMapList": map_list} + return { + "userId": data["userId"], + "nextIndex": next_idx, + "userMapList": map_list, + } def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: login_bonuses = self.data.item.get_login_bonuses(data["userId"]) - login_bonus_list = [] - next_index = 0 + if login_bonuses is None: + return { + "userId": data["userId"], + "nextIndex": 0, + "userLoginBonusList": [], + } - for x in range(data["nextIndex"], data["maxCount"] + data["nextIndex"]): - try: - login_bonus_list.append({ - "bonusId": login_bonuses[x]["bonus_id"], - "point": login_bonuses[x]["point"], - "isCurrent": login_bonuses[x]["is_current"], - "isComplete": login_bonuses[x]["is_complete"], - }) - except: + login_bonus_list = [] + next_idx = int(data["nextIndex"]) + max_ct = int(data["maxCount"]) + + for x in range(next_idx, len(login_bonuses)): + tmp = login_bonuses[x]._asdict() + tmp.pop("user") + tmp.pop("id") + login_bonus_list.append(tmp) + + if len(login_bonus_list) >= max_ct: break - # We're capped and still have some left to go - if len(login_bonus_list) == data["maxCount"] and len(login_bonuses) > data["maxCount"] + data["nextIndex"]: - next_index = data["maxCount"] + data["nextIndex"] + if len(login_bonuses) >= next_idx + max_ct: + next_idx += max_ct + else: + next_idx = 0 - return {"userId": data["userId"], "nextIndex": next_index, "userLoginBonusList": login_bonus_list} + return { + "userId": data["userId"], + "nextIndex": next_idx, + "userLoginBonusList": login_bonus_list, + } def handle_get_user_region_api_request(self, data: Dict) -> Dict: return {"userId": data["userId"], "length": 0, "userRegionList": []} + + def handle_get_user_web_option_api_request(self, data: Dict) -> Dict: + w = self.data.profile.get_web_option(data["userId"], self.version) + if w is None: + return {"userId": data["userId"], "userWebOption": {}} + + web_opt = w._asdict() + web_opt.pop("id") + web_opt.pop("user") + web_opt.pop("version") + + return {"userId": data["userId"], "userWebOption": web_opt} + + def handle_get_user_survival_api_request(self, data: Dict) -> Dict: + return {"userId": data["userId"], "length": 0, "userSurvivalList": []} + + def handle_get_user_grade_api_request(self, data: Dict) -> Dict: + g = self.data.profile.get_grade_status(data["userId"]) + if g is None: + return {"userId": data["userId"], "userGradeStatus": {}, "length": 0, "userGradeList": []} + grade_stat = g._asdict() + grade_stat.pop("id") + grade_stat.pop("user") + + return {"userId": data["userId"], "userGradeStatus": grade_stat, "length": 0, "userGradeList": []} def handle_get_user_music_api_request(self, data: Dict) -> Dict: - songs = self.data.score.get_best_scores(data["userId"]) + user_id = data.get("userId", 0) + next_index = data.get("nextIndex", 0) + max_ct = data.get("maxCount", 50) + upper_lim = next_index + max_ct music_detail_list = [] - next_index = 0 - if songs is not None: - for song in songs: - music_detail_list.append({ - "musicId": song["song_id"], - "level": song["chart_id"], - "playCount": song["play_count"], - "achievement": song["achievement"], - "comboStatus": song["combo_status"], - "syncStatus": song["sync_status"], - "deluxscoreMax": song["dx_score"], - "scoreRank": song["score_rank"], - }) - if len(music_detail_list) == data["maxCount"]: - next_index = data["maxCount"] + data["nextIndex"] - break + if user_id <= 0: + self.logger.warning("handle_get_user_music_api_request: Could not find userid in data, or userId is 0") + return {} + + songs = self.data.score.get_best_scores(user_id, is_dx=False) + if songs is None: + self.logger.debug("handle_get_user_music_api_request: get_best_scores returned None!") + return { + "userId": data["userId"], + "nextIndex": 0, + "userMusicList": [], + } - return {"userId": data["userId"], "nextIndex": next_index, "userMusicList": [{"userMusicDetailList": music_detail_list}]} + num_user_songs = len(songs) + + for x in range(next_index, upper_lim): + if num_user_songs <= x: + break + + tmp = songs[x]._asdict() + tmp.pop("id") + tmp.pop("user") + music_detail_list.append(tmp) + + next_index = 0 if len(music_detail_list) < max_ct or num_user_songs == upper_lim else upper_lim + self.logger.info(f"Send songs {next_index}-{upper_lim} ({len(music_detail_list)}) out of {num_user_songs} for user {user_id} (next idx {next_index})") + return { + "userId": data["userId"], + "nextIndex": next_index, + "userMusicList": [{"userMusicDetailList": music_detail_list}], + } + + def handle_upload_user_portrait_api_request(self, data: Dict) -> Dict: + self.logger.debug(data) + + def handle_upload_user_photo_api_request(self, data: Dict) -> Dict: + if not self.game_config.uploads.photos or not self.game_config.uploads.photos_dir: + return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} + + photo = data.get("userPhoto", {}) + + if photo is None or not photo: + return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} + + order_id = int(photo.get("orderId", -1)) + user_id = int(photo.get("userId", -1)) + div_num = int(photo.get("divNumber", -1)) + div_len = int(photo.get("divLength", -1)) + div_data = photo.get("divData", "") + playlog_id = int(photo.get("playlogId", -1)) + track_num = int(photo.get("trackNo", -1)) + upload_date = photo.get("uploadDate", "") + + if order_id < 0 or user_id <= 0 or div_num < 0 or div_len <= 0 or not div_data or playlog_id < 0 or track_num <= 0 or not upload_date: + self.logger.warning(f"Malformed photo upload request") + return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} + + if order_id == 0 and div_num > 0: + self.logger.warning(f"Failed to set orderId properly (still 0 after first chunk)") + return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} + + if div_num == 0 and order_id > 0: + self.logger.warning(f"First chuck re-send, Ignore") + return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} + + if div_num >= div_len: + self.logger.warning(f"Sent extra chunks ({div_num} >= {div_len})") + return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} + + if div_len >= 100: + self.logger.warning(f"Photo too large ({div_len} * 10240 = {div_len * 10240} bytes)") + return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} + + ret_code = order_id + 1 + photo_chunk = b64decode(div_data) + + if len(photo_chunk) > 10240 or (len(photo_chunk) < 10240 and div_num + 1 != div_len): + self.logger.warning(f"Incorrect data size after decoding (Expected 10240, got {len(photo_chunk)})") + return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} + + out_name = f"{self.game_config.uploads.photos_dir}/{user_id}_{playlog_id}_{track_num}" + + if not path.exists(f"{out_name}.bin") and div_num != 0: + self.logger.warning(f"Out of order photo upload (div_num {div_num})") + return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} + + if path.exists(f"{out_name}.bin") and div_num == 0: + self.logger.warning(f"Duplicate file upload") + return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} + + elif path.exists(f"{out_name}.bin"): + fstats = stat(f"{out_name}.bin") + if fstats.st_size != 10240 * div_num: + self.logger.warning(f"Out of order photo upload (trying to upload div {div_num}, expected div {fstats.st_size / 10240} for file sized {fstats.st_size} bytes)") + return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} + + try: + with open(f"{out_name}.bin", "ab") as f: + f.write(photo_chunk) + + except Exception: + self.logger.error(f"Failed writing to {out_name}.bin") + return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} + + if div_num + 1 == div_len and path.exists(f"{out_name}.bin"): + try: + p = ImageFile.Parser() + with open(f"{out_name}.bin", "rb") as f: + p.feed(f.read()) + + im = p.close() + im.save(f"{out_name}.jpeg") + except Exception: + self.logger.error(f"File {out_name}.bin failed image validation") + + try: + remove(f"{out_name}.bin") + + except Exception: + self.logger.error(f"Failed to delete {out_name}.bin, please remove it manually") + + return {'returnCode': ret_code, 'apiName': 'UploadUserPhotoApi'} diff --git a/titles/mai2/config.py b/titles/mai2/config.py index 7fe9a33..850e0c1 100644 --- a/titles/mai2/config.py +++ b/titles/mai2/config.py @@ -1,17 +1,77 @@ from core.config import CoreConfig -class Mai2ServerConfig(): + +class Mai2ServerConfig: def __init__(self, parent: "Mai2Config") -> None: self.__config = parent @property def enable(self) -> bool: - return CoreConfig.get_config_field(self.__config, 'mai2', 'server', 'enable', default=True) - + return CoreConfig.get_config_field( + self.__config, "mai2", "server", "enable", default=True + ) + @property def loglevel(self) -> int: - return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'mai2', 'server', 'loglevel', default="info")) + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "mai2", "server", "loglevel", default="info" + ) + ) + +class Mai2DeliverConfig: + def __init__(self, parent: "Mai2Config") -> None: + self.__config = parent + + @property + def enable(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "mai2", "deliver", "enable", default=False + ) + + @property + def udbdl_enable(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "mai2", "deliver", "udbdl_enable", default=False + ) + + @property + def content_folder(self) -> int: + return CoreConfig.get_config_field( + self.__config, "mai2", "deliver", "content_folder", default="" + ) + +class Mai2UploadsConfig: + def __init__(self, parent: "Mai2Config") -> None: + self.__config = parent + + @property + def photos(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "mai2", "uploads", "photos", default=False + ) + + @property + def photos_dir(self) -> str: + return CoreConfig.get_config_field( + self.__config, "mai2", "uploads", "photos_dir", default="" + ) + + @property + def movies(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "mai2", "uploads", "movies", default=False + ) + + @property + def movies_dir(self) -> str: + return CoreConfig.get_config_field( + self.__config, "mai2", "uploads", "movies_dir", default="" + ) + class Mai2Config(dict): def __init__(self) -> None: - self.server = Mai2ServerConfig(self) \ No newline at end of file + self.server = Mai2ServerConfig(self) + self.deliver = Mai2DeliverConfig(self) + self.uploads = Mai2UploadsConfig(self) diff --git a/titles/mai2/const.py b/titles/mai2/const.py index c6ae129..a4c29db 100644 --- a/titles/mai2/const.py +++ b/titles/mai2/const.py @@ -1,4 +1,4 @@ -class Mai2Constants(): +class Mai2Constants: GRADE = { "D": 0, "C": 1, @@ -13,39 +13,71 @@ class Mai2Constants(): "SS": 10, "SS+": 11, "SSS": 12, - "SSS+": 13 - } - FC = { - "None": 0, - "FC": 1, - "FC+": 2, - "AP": 3, - "AP+": 4 - } - SYNC = { - "None": 0, - "FS": 1, - "FS+": 2, - "FDX": 3, - "FDX+": 4 + "SSS+": 13, } + FC = {"None": 0, "FC": 1, "FC+": 2, "AP": 3, "AP+": 4} + SYNC = {"None": 0, "FS": 1, "FS+": 2, "FDX": 3, "FDX+": 4} DATE_TIME_FORMAT = "%Y-%m-%d %H:%M:%S" - GAME_CODE = "SDEZ" + GAME_CODE = "SBXL" + GAME_CODE_GREEN = "SBZF" + GAME_CODE_ORANGE = "SDBM" + GAME_CODE_PINK = "SDCQ" + GAME_CODE_MURASAKI = "SDDK" + GAME_CODE_MILK = "SDDZ" + GAME_CODE_FINALE = "SDEY" + GAME_CODE_DX = "SDEZ" CONFIG_NAME = "mai2.yaml" - VER_MAIMAI_DX = 0 - VER_MAIMAI_DX_PLUS = 1 - VER_MAIMAI_DX_SPLASH = 2 - VER_MAIMAI_DX_SPLASH_PLUS = 3 - VER_MAIMAI_DX_UNIVERSE = 4 - VER_MAIMAI_DX_UNIVERSE_PLUS = 5 + VER_MAIMAI = 0 + VER_MAIMAI_PLUS = 1 + VER_MAIMAI_GREEN = 2 + VER_MAIMAI_GREEN_PLUS = 3 + VER_MAIMAI_ORANGE = 4 + VER_MAIMAI_ORANGE_PLUS = 5 + VER_MAIMAI_PINK = 6 + VER_MAIMAI_PINK_PLUS = 7 + VER_MAIMAI_MURASAKI = 8 + VER_MAIMAI_MURASAKI_PLUS = 9 + VER_MAIMAI_MILK = 10 + VER_MAIMAI_MILK_PLUS = 11 + VER_MAIMAI_FINALE = 12 - VERSION_STRING = ("maimai Delux", "maimai Delux PLUS", "maimai Delux Splash", "maimai Delux Splash PLUS", "maimai Delux Universe", - "maimai Delux Universe PLUS") + VER_MAIMAI_DX = 13 + VER_MAIMAI_DX_PLUS = 14 + VER_MAIMAI_DX_SPLASH = 15 + VER_MAIMAI_DX_SPLASH_PLUS = 16 + VER_MAIMAI_DX_UNIVERSE = 17 + VER_MAIMAI_DX_UNIVERSE_PLUS = 18 + VER_MAIMAI_DX_FESTIVAL = 19 + VER_MAIMAI_DX_FESTIVAL_PLUS = 20 + + VERSION_STRING = ( + "maimai", + "maimai PLUS", + "maimai GreeN", + "maimai GreeN PLUS", + "maimai ORANGE", + "maimai ORANGE PLUS", + "maimai PiNK", + "maimai PiNK PLUS", + "maimai MURASAKi", + "maimai MURASAKi PLUS", + "maimai MiLK", + "maimai MiLK PLUS", + "maimai FiNALE", + "maimai DX", + "maimai DX PLUS", + "maimai DX Splash", + "maimai DX Splash PLUS", + "maimai DX UNiVERSE", + "maimai DX UNiVERSE PLUS", + "maimai DX FESTiVAL", + "maimai DX FESTiVAL PLUS", + ) @classmethod def game_ver_to_string(cls, ver: int): - return cls.VERSION_STRING[ver] \ No newline at end of file + return cls.VERSION_STRING[ver] diff --git a/titles/mai2/database.py b/titles/mai2/database.py index 7a19e75..be9e518 100644 --- a/titles/mai2/database.py +++ b/titles/mai2/database.py @@ -1,6 +1,12 @@ from core.data import Data from core.config import CoreConfig -from titles.mai2.schema import Mai2ItemData, Mai2ProfileData, Mai2StaticData, Mai2ScoreData +from titles.mai2.schema import ( + Mai2ItemData, + Mai2ProfileData, + Mai2StaticData, + Mai2ScoreData, +) + class Mai2Data(Data): def __init__(self, cfg: CoreConfig) -> None: @@ -9,4 +15,4 @@ class Mai2Data(Data): self.profile = Mai2ProfileData(self.config, self.session) self.item = Mai2ItemData(self.config, self.session) self.static = Mai2StaticData(self.config, self.session) - self.score = Mai2ScoreData(self.config, self.session) \ No newline at end of file + self.score = Mai2ScoreData(self.config, self.session) diff --git a/titles/mai2/dx.py b/titles/mai2/dx.py new file mode 100644 index 0000000..508cebb --- /dev/null +++ b/titles/mai2/dx.py @@ -0,0 +1,624 @@ +from typing import Any, List, Dict +from datetime import datetime, timedelta +import pytz +import json +from random import randint + +from core.config import CoreConfig +from titles.mai2.base import Mai2Base +from titles.mai2.config import Mai2Config +from titles.mai2.const import Mai2Constants + + +class Mai2DX(Mai2Base): + def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: + super().__init__(cfg, game_cfg) + self.version = Mai2Constants.VER_MAIMAI_DX + + def handle_get_game_setting_api_request(self, data: Dict): + return { + "gameSetting": { + "isMaintenance": False, + "requestInterval": 1800, + "rebootStartTime": "2020-01-01 07:00:00.0", + "rebootEndTime": "2020-01-01 07:59:59.0", + "movieUploadLimit": 100, + "movieStatus": 1, + "movieServerUri": self.old_server + "movie/", + "deliverServerUri": self.old_server + "deliver/" if self.can_deliver and self.game_config.deliver.enable else "", + "oldServerUri": self.old_server + "old", + "usbDlServerUri": self.old_server + "usbdl/" if self.can_deliver and self.game_config.deliver.udbdl_enable else "", + "rebootInterval": 0, + }, + "isAouAccession": False, + } + + def handle_get_user_preview_api_request(self, data: Dict) -> Dict: + p = self.data.profile.get_profile_detail(data["userId"], self.version) + o = self.data.profile.get_profile_option(data["userId"], self.version) + if p is None or o is None: + return {} # Register + profile = p._asdict() + option = o._asdict() + + return { + "userId": data["userId"], + "userName": profile["userName"], + "isLogin": False, + "lastGameId": profile["lastGameId"], + "lastDataVersion": profile["lastDataVersion"], + "lastRomVersion": profile["lastRomVersion"], + "lastLoginDate": profile["lastLoginDate"], + "lastPlayDate": profile["lastPlayDate"], + "playerRating": profile["playerRating"], + "nameplateId": 0, # Unused + "iconId": profile["iconId"], + "trophyId": 0, # Unused + "partnerId": profile["partnerId"], + "frameId": profile["frameId"], + "dispRate": option[ + "dispRate" + ], # 0: all/begin, 1: disprate, 2: dispDan, 3: hide, 4: end + "totalAwake": profile["totalAwake"], + "isNetMember": profile["isNetMember"], + "dailyBonusDate": profile["dailyBonusDate"], + "headPhoneVolume": option["headPhoneVolume"], + "isInherit": False, # Not sure what this is or does?? + "banState": profile["banState"] + if profile["banState"] is not None + else 0, # New with uni+ + } + + def handle_upload_user_playlog_api_request(self, data: Dict) -> Dict: + user_id = data["userId"] + playlog = data["userPlaylog"] + + self.data.score.put_playlog(user_id, playlog) + + return {"returnCode": 1, "apiName": "UploadUserPlaylogApi"} + + def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: + user_id = data["userId"] + charge = data["userCharge"] + + # remove the ".0" from the date string, festival only? + charge["purchaseDate"] = charge["purchaseDate"].replace(".0", "") + self.data.item.put_charge( + user_id, + charge["chargeId"], + charge["stock"], + datetime.strptime(charge["purchaseDate"], Mai2Constants.DATE_TIME_FORMAT), + datetime.strptime(charge["validDate"], Mai2Constants.DATE_TIME_FORMAT), + ) + + return {"returnCode": 1, "apiName": "UpsertUserChargelogApi"} + + def handle_upsert_user_all_api_request(self, data: Dict) -> Dict: + user_id = data["userId"] + upsert = data["upsertUserAll"] + + if int(user_id) & 1000000000001 == 1000000000001: + self.logger.info("Guest play, ignoring.") + return {"returnCode": 1, "apiName": "UpsertUserAllApi"} + + if "userData" in upsert and len(upsert["userData"]) > 0: + upsert["userData"][0]["isNetMember"] = 1 + upsert["userData"][0].pop("accessCode") + self.data.profile.put_profile_detail( + user_id, self.version, upsert["userData"][0] + ) + + if "userExtend" in upsert and len(upsert["userExtend"]) > 0: + self.data.profile.put_profile_extend( + user_id, self.version, upsert["userExtend"][0] + ) + + if "userGhost" in upsert: + for ghost in upsert["userGhost"]: + self.data.profile.put_profile_ghost(user_id, self.version, ghost) + + if "userOption" in upsert and len(upsert["userOption"]) > 0: + self.data.profile.put_profile_option( + user_id, self.version, upsert["userOption"][0] + ) + + if "userRatingList" in upsert and len(upsert["userRatingList"]) > 0: + self.data.profile.put_profile_rating( + user_id, self.version, upsert["userRatingList"][0] + ) + + if "userActivityList" in upsert and len(upsert["userActivityList"]) > 0: + for k, v in upsert["userActivityList"][0].items(): + for act in v: + self.data.profile.put_profile_activity(user_id, act) + + if "userChargeList" in upsert and len(upsert["userChargeList"]) > 0: + for charge in upsert["userChargeList"]: + # remove the ".0" from the date string, festival only? + charge["purchaseDate"] = charge["purchaseDate"].replace(".0", "") + self.data.item.put_charge( + user_id, + charge["chargeId"], + charge["stock"], + datetime.strptime( + charge["purchaseDate"], Mai2Constants.DATE_TIME_FORMAT + ), + datetime.strptime( + charge["validDate"], Mai2Constants.DATE_TIME_FORMAT + ), + ) + + if "userCharacterList" in upsert and len(upsert["userCharacterList"]) > 0: + for char in upsert["userCharacterList"]: + self.data.item.put_character( + user_id, + char["characterId"], + char["level"], + char["awakening"], + char["useCount"], + ) + + if "userItemList" in upsert and len(upsert["userItemList"]) > 0: + for item in upsert["userItemList"]: + self.data.item.put_item( + user_id, + int(item["itemKind"]), + item["itemId"], + item["stock"], + item["isValid"], + ) + + if "userLoginBonusList" in upsert and len(upsert["userLoginBonusList"]) > 0: + for login_bonus in upsert["userLoginBonusList"]: + self.data.item.put_login_bonus( + user_id, + login_bonus["bonusId"], + login_bonus["point"], + login_bonus["isCurrent"], + login_bonus["isComplete"], + ) + + if "userMapList" in upsert and len(upsert["userMapList"]) > 0: + for map in upsert["userMapList"]: + self.data.item.put_map( + user_id, + map["mapId"], + map["distance"], + map["isLock"], + map["isClear"], + map["isComplete"], + ) + + if "userMusicDetailList" in upsert and len(upsert["userMusicDetailList"]) > 0: + for music in upsert["userMusicDetailList"]: + self.data.score.put_best_score(user_id, music) + + if "userCourseList" in upsert and len(upsert["userCourseList"]) > 0: + for course in upsert["userCourseList"]: + self.data.score.put_course(user_id, course) + + if "userFavoriteList" in upsert and len(upsert["userFavoriteList"]) > 0: + for fav in upsert["userFavoriteList"]: + self.data.item.put_favorite(user_id, fav["kind"], fav["itemIdList"]) + + if ( + "userFriendSeasonRankingList" in upsert + and len(upsert["userFriendSeasonRankingList"]) > 0 + ): + for fsr in upsert["userFriendSeasonRankingList"]: + fsr["recordDate"] = ( + datetime.strptime( + fsr["recordDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0" + ), + ) + self.data.item.put_friend_season_ranking(user_id, fsr) + + return {"returnCode": 1, "apiName": "UpsertUserAllApi"} + + def handle_get_user_data_api_request(self, data: Dict) -> Dict: + profile = self.data.profile.get_profile_detail(data["userId"], self.version) + if profile is None: + return + + profile_dict = profile._asdict() + profile_dict.pop("id") + profile_dict.pop("user") + profile_dict.pop("version") + + return {"userId": data["userId"], "userData": profile_dict} + + def handle_get_user_extend_api_request(self, data: Dict) -> Dict: + extend = self.data.profile.get_profile_extend(data["userId"], self.version) + if extend is None: + return + + extend_dict = extend._asdict() + extend_dict.pop("id") + extend_dict.pop("user") + extend_dict.pop("version") + + return {"userId": data["userId"], "userExtend": extend_dict} + + def handle_get_user_option_api_request(self, data: Dict) -> Dict: + options = self.data.profile.get_profile_option(data["userId"], self.version) + if options is None: + return + + options_dict = options._asdict() + options_dict.pop("id") + options_dict.pop("user") + options_dict.pop("version") + + return {"userId": data["userId"], "userOption": options_dict} + + def handle_get_user_card_api_request(self, data: Dict) -> Dict: + user_cards = self.data.item.get_cards(data["userId"]) + if user_cards is None: + return {"userId": data["userId"], "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 { + "userId": data["userId"], + "nextIndex": next_idx, + "userCardList": card_list[start_idx:end_idx], + } + + def handle_get_user_charge_api_request(self, data: Dict) -> Dict: + user_charges = self.data.item.get_charges(data["userId"]) + if user_charges is None: + return {"userId": data["userId"], "length": 0, "userChargeList": []} + + user_charge_list = [] + for charge in user_charges: + tmp = charge._asdict() + tmp.pop("id") + tmp.pop("user") + tmp["purchaseDate"] = datetime.strftime( + tmp["purchaseDate"], Mai2Constants.DATE_TIME_FORMAT + ) + tmp["validDate"] = datetime.strftime( + tmp["validDate"], Mai2Constants.DATE_TIME_FORMAT + ) + + user_charge_list.append(tmp) + + return { + "userId": data["userId"], + "length": len(user_charge_list), + "userChargeList": user_charge_list, + } + + 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 = self.data.item.get_items(data["userId"], kind) + + 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 + + xout = kind * 10000000000 + next_idx + len(items) + + if len(items) < int(data["maxCount"]): + next_idx = 0 + else: + next_idx = xout + + return { + "userId": data["userId"], + "nextIndex": next_idx, + "itemKind": kind, + "userItemList": items, + } + + def handle_get_user_character_api_request(self, data: Dict) -> Dict: + characters = self.data.item.get_characters(data["userId"]) + + chara_list = [] + for chara in characters: + tmp = chara._asdict() + tmp.pop("id") + tmp.pop("user") + chara_list.append(tmp) + + return {"userId": data["userId"], "userCharacterList": chara_list} + + def handle_get_user_favorite_api_request(self, data: Dict) -> Dict: + favorites = self.data.item.get_favorites(data["userId"], data["itemKind"]) + if favorites is None: + return + + userFavs = [] + for fav in favorites: + userFavs.append( + { + "userId": data["userId"], + "itemKind": fav["itemKind"], + "itemIdList": fav["itemIdList"], + } + ) + + return {"userId": data["userId"], "userFavoriteData": userFavs} + + def handle_get_user_ghost_api_request(self, data: Dict) -> Dict: + ghost = self.data.profile.get_profile_ghost(data["userId"], self.version) + if ghost is None: + return + + ghost_dict = ghost._asdict() + ghost_dict.pop("user") + ghost_dict.pop("id") + ghost_dict.pop("version_int") + + return {"userId": data["userId"], "userGhost": ghost_dict} + + def handle_get_user_rating_api_request(self, data: Dict) -> Dict: + rating = self.data.profile.get_profile_rating(data["userId"], self.version) + if rating is None: + return + + rating_dict = rating._asdict() + rating_dict.pop("user") + rating_dict.pop("id") + rating_dict.pop("version") + + return {"userId": data["userId"], "userRating": rating_dict} + + def handle_get_user_activity_api_request(self, data: Dict) -> Dict: + """ + kind 1 is playlist, kind 2 is music list + """ + playlist = self.data.profile.get_profile_activity(data["userId"], 1) + musiclist = self.data.profile.get_profile_activity(data["userId"], 2) + if playlist is None or musiclist is None: + return + + plst = [] + mlst = [] + + for play in playlist: + tmp = play._asdict() + tmp["id"] = tmp["activityId"] + tmp.pop("activityId") + tmp.pop("user") + plst.append(tmp) + + for music in musiclist: + tmp = music._asdict() + tmp["id"] = tmp["activityId"] + tmp.pop("activityId") + tmp.pop("user") + mlst.append(tmp) + + return {"userActivity": {"playList": plst, "musicList": mlst}} + + def handle_get_user_course_api_request(self, data: Dict) -> Dict: + user_courses = self.data.score.get_courses(data["userId"]) + if user_courses is None: + return {"userId": data["userId"], "nextIndex": 0, "userCourseList": []} + + course_list = [] + for course in user_courses: + tmp = course._asdict() + tmp.pop("user") + tmp.pop("id") + course_list.append(tmp) + + return {"userId": data["userId"], "nextIndex": 0, "userCourseList": course_list} + + def handle_get_user_portrait_api_request(self, data: Dict) -> Dict: + # No support for custom pfps + return {"length": 0, "userPortraitList": []} + + def handle_get_user_friend_season_ranking_api_request(self, data: Dict) -> Dict: + friend_season_ranking = self.data.item.get_friend_season_ranking(data["userId"]) + if friend_season_ranking is None: + return { + "userId": data["userId"], + "nextIndex": 0, + "userFriendSeasonRankingList": [], + } + + friend_season_ranking_list = [] + next_idx = int(data["nextIndex"]) + max_ct = int(data["maxCount"]) + + for x in range(next_idx, len(friend_season_ranking)): + tmp = friend_season_ranking[x]._asdict() + tmp.pop("user") + tmp.pop("id") + tmp["recordDate"] = datetime.strftime( + tmp["recordDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0" + ) + friend_season_ranking_list.append(tmp) + + if len(friend_season_ranking_list) >= max_ct: + break + + if len(friend_season_ranking) >= next_idx + max_ct: + next_idx += max_ct + else: + next_idx = 0 + + return { + "userId": data["userId"], + "nextIndex": next_idx, + "userFriendSeasonRankingList": friend_season_ranking_list, + } + + def handle_get_user_map_api_request(self, data: Dict) -> Dict: + maps = self.data.item.get_maps(data["userId"]) + if maps is None: + return { + "userId": data["userId"], + "nextIndex": 0, + "userMapList": [], + } + + map_list = [] + next_idx = int(data["nextIndex"]) + max_ct = int(data["maxCount"]) + + for x in range(next_idx, len(maps)): + tmp = maps[x]._asdict() + tmp.pop("user") + tmp.pop("id") + map_list.append(tmp) + + if len(map_list) >= max_ct: + break + + if len(maps) >= next_idx + max_ct: + next_idx += max_ct + else: + next_idx = 0 + + return { + "userId": data["userId"], + "nextIndex": next_idx, + "userMapList": map_list, + } + + def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: + login_bonuses = self.data.item.get_login_bonuses(data["userId"]) + if login_bonuses is None: + return { + "userId": data["userId"], + "nextIndex": 0, + "userLoginBonusList": [], + } + + login_bonus_list = [] + next_idx = int(data["nextIndex"]) + max_ct = int(data["maxCount"]) + + for x in range(next_idx, len(login_bonuses)): + tmp = login_bonuses[x]._asdict() + tmp.pop("user") + tmp.pop("id") + login_bonus_list.append(tmp) + + if len(login_bonus_list) >= max_ct: + break + + if len(login_bonuses) >= next_idx + max_ct: + next_idx += max_ct + else: + next_idx = 0 + + return { + "userId": data["userId"], + "nextIndex": next_idx, + "userLoginBonusList": login_bonus_list, + } + + def handle_get_user_region_api_request(self, data: Dict) -> Dict: + """ + class UserRegionList: + regionId: int + playCount: int + created: str + """ + return {"userId": data["userId"], "length": 0, "userRegionList": []} + + def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict: + user_id = data["userId"] + rival_id = data["rivalId"] + + """ + class UserRivalData: + rivalId: int + rivalName: str + """ + return {"userId": user_id, "userRivalData": {}} + + def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict: + 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": []} + + def handle_get_user_music_api_request(self, data: Dict) -> Dict: + user_id = data.get("userId", 0) + next_index = data.get("nextIndex", 0) + max_ct = data.get("maxCount", 50) + upper_lim = next_index + max_ct + music_detail_list = [] + + if user_id <= 0: + self.logger.warning("handle_get_user_music_api_request: Could not find userid in data, or userId is 0") + return {} + + songs = self.data.score.get_best_scores(user_id) + if songs is None: + self.logger.debug("handle_get_user_music_api_request: get_best_scores returned None!") + return { + "userId": data["userId"], + "nextIndex": 0, + "userMusicList": [], + } + + num_user_songs = len(songs) + + for x in range(next_index, upper_lim): + if num_user_songs <= x: + break + + tmp = songs[x]._asdict() + tmp.pop("id") + tmp.pop("user") + music_detail_list.append(tmp) + + next_index = 0 if len(music_detail_list) < max_ct or num_user_songs == upper_lim else upper_lim + self.logger.info(f"Send songs {next_index}-{upper_lim} ({len(music_detail_list)}) out of {num_user_songs} for user {user_id} (next idx {next_index})") + return { + "userId": data["userId"], + "nextIndex": next_index, + "userMusicList": [{"userMusicDetailList": music_detail_list}], + } + + def handle_user_login_api_request(self, data: Dict) -> Dict: + ret = super().handle_user_login_api_request(data) + if ret is None or not ret: + return ret + ret['loginId'] = ret.get('loginCount', 0) + return ret diff --git a/titles/mai2/plus.py b/titles/mai2/dxplus.py similarity index 73% rename from titles/mai2/plus.py rename to titles/mai2/dxplus.py index 2af7bf6..9062ff5 100644 --- a/titles/mai2/plus.py +++ b/titles/mai2/dxplus.py @@ -4,11 +4,12 @@ import pytz import json from core.config import CoreConfig -from titles.mai2.base import Mai2Base +from titles.mai2.dx import Mai2DX from titles.mai2.config import Mai2Config from titles.mai2.const import Mai2Constants -class Mai2Plus(Mai2Base): + +class Mai2DXPlus(Mai2DX): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) - self.version = Mai2Constants.VER_MAIMAI_DX_PLUS \ No newline at end of file + self.version = Mai2Constants.VER_MAIMAI_DX_PLUS diff --git a/titles/mai2/festival.py b/titles/mai2/festival.py new file mode 100644 index 0000000..145fa71 --- /dev/null +++ b/titles/mai2/festival.py @@ -0,0 +1,37 @@ +from typing import Dict + +from core.config import CoreConfig +from titles.mai2.universeplus import Mai2UniversePlus +from titles.mai2.const import Mai2Constants +from titles.mai2.config import Mai2Config + + +class Mai2Festival(Mai2UniversePlus): + def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: + super().__init__(cfg, game_cfg) + self.version = Mai2Constants.VER_MAIMAI_DX_FESTIVAL + + def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = super().handle_cm_get_user_preview_api_request(data) + + # hardcode lastDataVersion for CardMaker + user_data["lastDataVersion"] = "1.30.00" + return user_data + + def handle_user_login_api_request(self, data: Dict) -> Dict: + user_login = super().handle_user_login_api_request(data) + # useless? + user_login["Bearer"] = "ARTEMiSTOKEN" + return user_login + + def handle_get_user_recommend_rate_music_api_request(self, data: Dict) -> Dict: + """ + userRecommendRateMusicIdList: list[int] + """ + return {"userId": data["userId"], "userRecommendRateMusicIdList": []} + + 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 new file mode 100644 index 0000000..7deeb98 --- /dev/null +++ b/titles/mai2/festivalplus.py @@ -0,0 +1,38 @@ +from typing import Dict + +from core.config import CoreConfig +from titles.mai2.festival import Mai2Festival +from titles.mai2.const import Mai2Constants +from titles.mai2.config import Mai2Config + + +class Mai2FestivalPlus(Mai2Festival): + def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: + super().__init__(cfg, game_cfg) + self.version = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS + + def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = super().handle_cm_get_user_preview_api_request(data) + + # hardcode lastDataVersion for CardMaker + user_data["lastDataVersion"] = "1.35.00" + return user_data + + 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/finale.py b/titles/mai2/finale.py new file mode 100644 index 0000000..f9f1d88 --- /dev/null +++ b/titles/mai2/finale.py @@ -0,0 +1,17 @@ +from typing import Any, List, Dict +from datetime import datetime, timedelta +import pytz +import json + +from core.config import CoreConfig +from titles.mai2.base import Mai2Base +from titles.mai2.config import Mai2Config +from titles.mai2.const import Mai2Constants + + +class Mai2Finale(Mai2Base): + def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: + super().__init__(cfg, game_cfg) + self.version = Mai2Constants.VER_MAIMAI_FINALE + self.can_deliver = True + self.can_usbdl = True diff --git a/titles/mai2/index.py b/titles/mai2/index.py index a4a8f3e..793aaef 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -1,4 +1,5 @@ from twisted.web.http import Request +from twisted.web.server import NOT_DONE_YET import json import inflection import yaml @@ -6,104 +7,366 @@ import string import logging, coloredlogs import zlib from logging.handlers import TimedRotatingFileHandler +from os import path, mkdir +from typing import Tuple, List, Dict from core.config import CoreConfig -from titles.mai2.config import Mai2Config -from titles.mai2.const import Mai2Constants -from titles.mai2.base import Mai2Base -from titles.mai2.plus import Mai2Plus -from titles.mai2.splash import Mai2Splash -from titles.mai2.splashplus import Mai2SplashPlus -from titles.mai2.universe import Mai2Universe -from titles.mai2.universeplus import Mai2UniversePlus +from core.utils import Utils +from core.title import BaseServlet +from .config import Mai2Config +from .const import Mai2Constants +from .base import Mai2Base +from .finale import Mai2Finale +from .dx import Mai2DX +from .dxplus import Mai2DXPlus +from .splash import Mai2Splash +from .splashplus import Mai2SplashPlus +from .universe import Mai2Universe +from .universeplus import Mai2UniversePlus +from .festival import Mai2Festival +from .festivalplus import Mai2FestivalPlus -class Mai2Servlet(): +class Mai2Servlet(BaseServlet): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: - self.core_cfg = core_cfg + super().__init__(core_cfg, cfg_dir) self.game_cfg = Mai2Config() - self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"))) + if path.exists(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"): + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}")) + ) self.versions = [ - Mai2Base(core_cfg, self.game_cfg), - Mai2Plus(core_cfg, self.game_cfg), - Mai2Splash(core_cfg, self.game_cfg), - Mai2SplashPlus(core_cfg, self.game_cfg), - Mai2Universe(core_cfg, self.game_cfg), - Mai2UniversePlus(core_cfg, self.game_cfg), + Mai2Base, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + Mai2Finale, + Mai2DX, + Mai2DXPlus, + Mai2Splash, + Mai2SplashPlus, + Mai2Universe, + Mai2UniversePlus, + Mai2Festival, + Mai2FestivalPlus, ] self.logger = logging.getLogger("mai2") - log_fmt_str = "[%(asctime)s] Mai2 | %(levelname)s | %(message)s" - log_fmt = logging.Formatter(log_fmt_str) - fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.core_cfg.server.log_dir, "mai2"), encoding='utf8', - when="d", backupCount=10) + if not hasattr(self.logger, "initted"): + log_fmt_str = "[%(asctime)s] Mai2 | %(levelname)s | %(message)s" + log_fmt = logging.Formatter(log_fmt_str) + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.core_cfg.server.log_dir, "mai2"), + encoding="utf8", + when="d", + backupCount=10, + ) - fileHandler.setFormatter(log_fmt) + fileHandler.setFormatter(log_fmt) + + consoleHandler = logging.StreamHandler() + consoleHandler.setFormatter(log_fmt) + + self.logger.addHandler(fileHandler) + self.logger.addHandler(consoleHandler) + + self.logger.setLevel(self.game_cfg.server.loglevel) + coloredlogs.install( + level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str + ) + self.logger.initted = True + + @classmethod + def is_game_enabled( + cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str + ) -> bool: + game_cfg = Mai2Config() + + if path.exists(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"): + game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}")) + ) + + if not game_cfg.server.enable: + return False - consoleHandler = logging.StreamHandler() - consoleHandler.setFormatter(log_fmt) + return True + + def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: + return ( + [ + ("handle_movie", "/{version}/MaimaiServlet/api/movie/{endpoint}", {}), + ("handle_old_srv", "/{version}/MaimaiServlet/old/{endpoint}", {}), + ("handle_old_srv_userdata", "/{version}/MaimaiServlet/old/{endpoint}/{placeid}/{keychip}/{userid}", {}), + ("handle_old_srv_userdata", "/{version}/MaimaiServlet/old/{endpoint}/{userid}", {}), + ("handle_usbdl", "/{version}/MaimaiServlet/usbdl/{endpoint}", {}), + ("handle_deliver", "/{version}/MaimaiServlet/deliver/{endpoint}", {}), + ], + [ + ("handle_movie", "/{version}/MaimaiServlet/api/movie/{endpoint}", {}), + ("handle_mai", "/{version}/MaimaiServlet/{endpoint}", {}), + ("handle_mai2", "/{version}/Maimai2Servlet/{endpoint}", {}), + ] + ) + + def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: + if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80: + return ( + f"http://{self.core_cfg.title.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_ver}/", + f"{self.core_cfg.title.hostname}", + ) - self.logger.addHandler(fileHandler) - self.logger.addHandler(consoleHandler) + return ( + f"http://{self.core_cfg.title.hostname}/{game_ver}/", + f"{self.core_cfg.title.hostname}", + ) + + def setup(self): + if ( + self.game_cfg.uploads.photos + and self.game_cfg.uploads.photos_dir + and not path.exists(self.game_cfg.uploads.photos_dir) + ): + try: + mkdir(self.game_cfg.uploads.photos_dir) + except Exception: + self.logger.error( + f"Failed to make photo upload directory at {self.game_cfg.uploads.photos_dir}" + ) + + if ( + self.game_cfg.uploads.movies + and self.game_cfg.uploads.movies_dir + and not path.exists(self.game_cfg.uploads.movies_dir) + ): + try: + mkdir(self.game_cfg.uploads.movies_dir) + except Exception: + self.logger.error( + f"Failed to make movie upload directory at {self.game_cfg.uploads.movies_dir}" + ) + + def handle_mai(self, request: Request, game_code: str, matchers: Dict) -> bytes: + endpoint = matchers['endpoint'] + version = int(matchers['version']) + if endpoint.lower() == "ping": + return zlib.compress(b'{"returnCode": "1"}') - self.logger.setLevel(self.game_cfg.server.loglevel) - coloredlogs.install(level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str) - - def render_POST(self, request: Request, version: int, url_path: str) -> bytes: req_raw = request.content.getvalue() - url = request.uri.decode() - url_split = url_path.split("/") internal_ver = 0 - endpoint = url_split[len(url_split) - 1] - - if version < 105: # 1.0 - internal_ver = Mai2Constants.VER_MAIMAI_DX - elif version >= 105 and version < 110: # Plus - internal_ver = Mai2Constants.VER_MAIMAI_DX_PLUS - elif version >= 110 and version < 115: # Splash - internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH - elif version >= 115 and version < 120: # Splash Plus - internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS - elif version >= 120 and version < 125: # Universe - internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE - elif version >= 125: # Universe Plus - internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS - - 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 - # technically not 0 - self.logger.error("Encryption not supported at this time") - - try: - unzip = zlib.decompress(req_raw) - - except zlib.error as e: - self.logger.error(f"Failed to decompress v{version} {endpoint} request -> {e}") - return zlib.compress("{\"stat\": \"0\"}".encode("utf-8")) + client_ip = Utils.get_ip_addr(request) - req_data = json.loads(unzip) - - self.logger.info(f"v{version} {endpoint} request - {req_data}") - - func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" + if version < 110: # 1.0 + internal_ver = Mai2Constants.VER_MAIMAI + elif version >= 110 and version < 120: # Plus + internal_ver = Mai2Constants.VER_MAIMAI_PLUS + elif version >= 120 and version < 130: # Green + internal_ver = Mai2Constants.VER_MAIMAI_GREEN + elif version >= 130 and version < 140: # Green Plus + internal_ver = Mai2Constants.VER_MAIMAI_GREEN_PLUS + elif version >= 140 and version < 150: # Orange + internal_ver = Mai2Constants.VER_MAIMAI_ORANGE + elif version >= 150 and version < 160: # Orange Plus + internal_ver = Mai2Constants.VER_MAIMAI_ORANGE_PLUS + elif version >= 160 and version < 170: # Pink + internal_ver = Mai2Constants.VER_MAIMAI_PINK + elif version >= 170 and version < 180: # Pink Plus + internal_ver = Mai2Constants.VER_MAIMAI_PINK_PLUS + elif version >= 180 and version < 185: # Murasaki + internal_ver = Mai2Constants.VER_MAIMAI_MURASAKI + elif version >= 185 and version < 190: # Murasaki Plus + internal_ver = Mai2Constants.VER_MAIMAI_MURASAKI_PLUS + elif version >= 190 and version < 195: # Milk + internal_ver = Mai2Constants.VER_MAIMAI_MILK + elif version >= 195 and version < 197: # Milk Plus + internal_ver = Mai2Constants.VER_MAIMAI_MILK_PLUS + elif version >= 197: # Finale + internal_ver = Mai2Constants.VER_MAIMAI_FINALE try: - handler = getattr(self.versions[internal_ver], func_to_find) - resp = handler(req_data) - - except AttributeError as e: - self.logger.warning(f"Unhandled v{version} request {endpoint} - {e}") - return zlib.compress("{\"stat\": \"0\"}".encode("utf-8")) + unzip = zlib.decompress(req_raw) + + except zlib.error as e: + self.logger.error( + f"Failed to decompress v{version} {endpoint} request -> {e}" + ) + return zlib.compress(b'{"stat": "0"}') + + req_data = json.loads(unzip) + + self.logger.info(f"v{version} {endpoint} request from {client_ip}") + self.logger.debug(req_data) + + func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" + handler_cls = self.versions[internal_ver](self.core_cfg, self.game_cfg) + + if not hasattr(handler_cls, func_to_find): + self.logger.warning(f"Unhandled v{version} request {endpoint}") + resp = {"returnCode": 1} + + else: + try: + handler = getattr(handler_cls, func_to_find) + resp = handler(req_data) + + except Exception as e: + self.logger.error(f"Error handling v{version} method {endpoint} - {e}") + return zlib.compress(b'{"returnCode": "0"}') - except Exception as e: - self.logger.error(f"Error handling v{version} method {endpoint} - {e}") - return zlib.compress("{\"stat\": \"0\"}".encode("utf-8")) - if resp == None: - resp = {'returnCode': 1} - - self.logger.info(f"Response {resp}") - + resp = {"returnCode": 1} + + self.logger.debug(f"Response {resp}") + return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) + + def handle_mai2(self, request: Request, game_code: str, matchers: Dict) -> bytes: + endpoint = matchers['endpoint'] + version = int(matchers['version']) + if endpoint.lower() == "ping": + return zlib.compress(b'{"returnCode": "1"}') + + req_raw = request.content.getvalue() + internal_ver = 0 + client_ip = Utils.get_ip_addr(request) + if version < 105: # 1.0 + internal_ver = Mai2Constants.VER_MAIMAI_DX + elif version >= 105 and version < 110: # PLUS + internal_ver = Mai2Constants.VER_MAIMAI_DX_PLUS + elif version >= 110 and version < 115: # Splash + internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH + elif version >= 115 and version < 120: # Splash PLUS + internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS + elif version >= 120 and version < 125: # UNiVERSE + internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE + elif version >= 125 and version < 130: # UNiVERSE PLUS + internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS + elif version >= 130 and version < 135: # FESTiVAL + internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL + elif version >= 135: # FESTiVAL PLUS + internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS + + if ( + request.getHeader("Mai-Encoding") is not None + or request.getHeader("X-Mai-Encoding") is not None + ): + # The has is some flavor of MD5 of the endpoint with a constant bolted onto the end of it. + # See cake.dll's Obfuscator function for details. Hopefully most DLL edits will remove + # these two(?) headers to not cause issues, but given the general quality of SEGA data... + enc_ver = request.getHeader("Mai-Encoding") + if enc_ver is None: + enc_ver = request.getHeader("X-Mai-Encoding") + self.logger.debug( + f"Encryption v{enc_ver} - User-Agent: {request.getHeader('User-Agent')}" + ) + + try: + unzip = zlib.decompress(req_raw) + + except zlib.error as e: + self.logger.error( + f"Failed to decompress v{version} {endpoint} request -> {e}" + ) + return zlib.compress(b'{"stat": "0"}') + + req_data = json.loads(unzip) + + self.logger.info(f"v{version} {endpoint} request from {client_ip}") + self.logger.debug(req_data) + + func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" + handler_cls = self.versions[internal_ver](self.core_cfg, self.game_cfg) + + if not hasattr(handler_cls, func_to_find): + self.logger.warning(f"Unhandled v{version} request {endpoint}") + resp = {"returnCode": 1} + + else: + try: + handler = getattr(handler_cls, func_to_find) + resp = handler(req_data) + + except Exception as e: + self.logger.error(f"Error handling v{version} method {endpoint} - {e}") + return zlib.compress(b'{"stat": "0"}') + + if resp == None: + resp = {"returnCode": 1} + + self.logger.debug(f"Response {resp}") + + return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) + + def handle_old_srv(self, request: Request, game_code: str, matchers: Dict) -> bytes: + endpoint = matchers['endpoint'] + version = matchers['version'] + self.logger.info(f"v{version} old server {endpoint}") + return zlib.compress(b"ok") + + def handle_old_srv_userdata(self, request: Request, game_code: str, matchers: Dict) -> bytes: + endpoint = matchers['endpoint'] + version = matchers['version'] + self.logger.info(f"v{version} old server {endpoint}") + return zlib.compress(b"{}") + + def render_GET(self, request: Request, version: int, url_path: str) -> bytes: + self.logger.debug(f"v{version} GET {url_path}") + url_split = url_path.split("/") + + if (url_split[0] == "api" and url_split[1] == "movie") or url_split[ + 0 + ] == "movie": + if url_split[2] == "moviestart": + return json.dumps({"moviestart": {"status": "OK"}}).encode() + + else: + request.setResponseCode(404) + return b"" + + elif url_split[0] == "usbdl": + if url_split[1] == "CONNECTIONTEST": + self.logger.info(f"v{version} usbdl server test") + return b"" + + elif self.game_cfg.deliver.udbdl_enable and path.exists( + f"{self.game_cfg.deliver.content_folder}/usb/{url_split[-1]}" + ): + with open( + f"{self.game_cfg.deliver.content_folder}/usb/{url_split[-1]}", "rb" + ) as f: + return f.read() + + else: + request.setResponseCode(404) + return b"" + + elif url_split[0] == "deliver": + file = url_split[len(url_split) - 1] + self.logger.info(f"v{version} {file} deliver inquire") + self.logger.debug( + f"{self.game_cfg.deliver.content_folder}/net_deliver/{file}" + ) + + if self.game_cfg.deliver.enable and path.exists( + f"{self.game_cfg.deliver.content_folder}/net_deliver/{file}" + ): + with open( + f"{self.game_cfg.deliver.content_folder}/net_deliver/{file}", "rb" + ) as f: + return f.read() + + else: + request.setResponseCode(404) + return b"" + + else: + return zlib.compress(b"{}") diff --git a/titles/mai2/read.py b/titles/mai2/read.py index 1652292..cc4f678 100644 --- a/titles/mai2/read.py +++ b/titles/mai2/read.py @@ -4,6 +4,9 @@ import os import re import xml.etree.ElementTree as ET from typing import Any, Dict, List, Optional +from Crypto.Cipher import AES +import zlib +import codecs from core.config import CoreConfig from core.data import Data @@ -11,30 +14,170 @@ from read import BaseReader from titles.mai2.const import Mai2Constants from titles.mai2.database import Mai2Data + class Mai2Reader(BaseReader): - def __init__(self, config: CoreConfig, version: int, bin_dir: Optional[str], opt_dir: Optional[str], extra: Optional[str]) -> None: + 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 = Mai2Data(config) try: - self.logger.info(f"Start importer for {Mai2Constants.game_ver_to_string(version)}") + self.logger.info( + f"Start importer for {Mai2Constants.game_ver_to_string(version)}" + ) except IndexError: - self.logger.error(f"Invalid maidx version {version}") + self.logger.error(f"Invalid maimai DX version {version}") exit(1) - + def read(self) -> None: data_dirs = [] - if self.bin_dir is not None: - data_dirs += self.get_data_directories(self.bin_dir) + if self.version >= Mai2Constants.VER_MAIMAI_DX: + if self.bin_dir is not None: + data_dirs += self.get_data_directories(self.bin_dir) + + if self.opt_dir is not None: + data_dirs += self.get_data_directories(self.opt_dir) + + for dir in data_dirs: + self.logger.info(f"Read from {dir}") + self.get_events(f"{dir}/event") + self.disable_events(f"{dir}/information", f"{dir}/scoreRanking") + self.read_music(f"{dir}/music") + self.read_tickets(f"{dir}/ticket") - if self.opt_dir is not None: - data_dirs += self.get_data_directories(self.opt_dir) + else: + if not os.path.exists(f"{self.bin_dir}/tables"): + self.logger.error(f"tables directory not found in {self.bin_dir}") + return + + if self.version >= Mai2Constants.VER_MAIMAI_MILK: + if self.extra is None: + self.logger.error("Milk - Finale requre an AES key via a hex string send as the --extra flag") + return + + key = bytes.fromhex(self.extra) + + else: + key = None + + evt_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmEvent.bin", key) + txt_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmtextout_jp.bin", key) + score_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmScore.bin", key) + + self.read_old_events(evt_table) + self.read_old_music(score_table, txt_table) + + if self.opt_dir is not None: + evt_table = self.load_table_raw(f"{self.opt_dir}/tables", "mmEvent.bin", key) + txt_table = self.load_table_raw(f"{self.opt_dir}/tables", "mmtextout_jp.bin", key) + score_table = self.load_table_raw(f"{self.opt_dir}/tables", "mmScore.bin", key) + + self.read_old_events(evt_table) + self.read_old_music(score_table, txt_table) + + return + + def load_table_raw(self, dir: str, file: str, key: Optional[bytes]) -> Optional[List[Dict[str, str]]]: + if not os.path.exists(f"{dir}/{file}"): + self.logger.warning(f"file {file} does not exist in directory {dir}, skipping") + return - for dir in data_dirs: - self.logger.info(f"Read from {dir}") - self.get_events(f"{dir}/event") - self.read_music(f"{dir}/music") - self.read_tickets(f"{dir}/ticket") + self.logger.info(f"Load table {file} from {dir}") + if key is not None: + cipher = AES.new(key, AES.MODE_CBC) + with open(f"{dir}/{file}", "rb") as f: + f_encrypted = f.read() + f_data = cipher.decrypt(f_encrypted)[0x10:] + + else: + with open(f"{dir}/{file}", "rb") as f: + f_data = f.read()[0x10:] + + if f_data is None or not f_data: + self.logger.warning(f"file {dir} could not be read, skipping") + return + + f_data_deflate = zlib.decompress(f_data, wbits = zlib.MAX_WBITS | 16)[0x12:] # lop off the junk at the beginning + f_decoded = codecs.utf_16_le_decode(f_data_deflate)[0] + f_split = f_decoded.splitlines() + + has_struct_def = "struct " in f_decoded + is_struct = False + struct_def = [] + tbl_content = [] + + if has_struct_def: + for x in f_split: + if x.startswith("struct "): + is_struct = True + struct_name = x[7:-1] + continue + + if x.startswith("};"): + is_struct = False + break + + if is_struct: + try: + struct_def.append(x[x.rindex(" ") + 2: -1]) + except ValueError: + self.logger.warning(f"rindex failed on line {x}") + + if is_struct: + self.logger.warning("Struct not formatted properly") + + if not struct_def: + self.logger.warning("Struct def not found") + + name = file[:file.index(".")] + if "_" in name: + name = name[:file.index("_")] + + for x in f_split: + if not x.startswith(name.upper()): + continue + + line_match = re.match(r"(\w+)\((.*?)\)([ ]+\/{3}<[ ]+(.*))?", x) + if line_match is None: + continue + + if not line_match.group(1) == name.upper(): + self.logger.warning(f"Strange regex match for line {x} -> {line_match}") + continue + + vals = line_match.group(2) + comment = line_match.group(4) + line_dict = {} + + vals_split = vals.split(",") + for y in range(len(vals_split)): + stripped = vals_split[y].strip().lstrip("L\"").lstrip("\"").rstrip("\"") + if not stripped or stripped is None: + continue + + if has_struct_def and len(struct_def) > y: + line_dict[struct_def[y]] = stripped + + else: + line_dict[f'item_{y}'] = stripped + + if comment: + line_dict['comment'] = comment + + tbl_content.append(line_dict) + + if tbl_content: + return tbl_content + + else: + self.logger.warning("Failed load table content, skipping") + return def get_events(self, base_dir: str) -> None: self.logger.info(f"Reading events from {base_dir}...") @@ -43,47 +186,122 @@ class Mai2Reader(BaseReader): for dir in dirs: if os.path.exists(f"{root}/{dir}/Event.xml"): with open(f"{root}/{dir}/Event.xml", encoding="utf-8") as f: - troot = ET.fromstring(f.read()) - name = troot.find('name').find('str').text - id = int(troot.find('name').find('id').text) - event_type = int(troot.find('infoType').text) + name = troot.find("name").find("str").text + id = int(troot.find("name").find("id").text) + event_type = int(troot.find("infoType").text) - self.data.static.put_game_event(self.version, event_type, id, name) + self.data.static.put_game_event( + self.version, event_type, id, name + ) self.logger.info(f"Added event {id}...") - + + def disable_events( + self, base_information_dir: str, base_score_ranking_dir: str + ) -> None: + self.logger.info(f"Reading disabled events from {base_information_dir}...") + + for root, dirs, files in os.walk(base_information_dir): + for dir in dirs: + if os.path.exists(f"{root}/{dir}/Information.xml"): + with open(f"{root}/{dir}/Information.xml", encoding="utf-8") as f: + troot = ET.fromstring(f.read()) + + event_id = int(troot.find("name").find("id").text) + + self.data.static.toggle_game_event( + self.version, event_id, toggle=False + ) + self.logger.info(f"Disabled event {event_id}...") + + for root, dirs, files in os.walk(base_score_ranking_dir): + for dir in dirs: + if os.path.exists(f"{root}/{dir}/ScoreRanking.xml"): + with open(f"{root}/{dir}/ScoreRanking.xml", encoding="utf-8") as f: + troot = ET.fromstring(f.read()) + + event_id = int(troot.find("eventName").find("id").text) + + self.data.static.toggle_game_event( + self.version, event_id, toggle=False + ) + self.logger.info(f"Disabled event {event_id}...") + + # manually disable events wich are known to be problematic + for event_id in [ + 1, + 10, + 220311, + 220312, + 220313, + 220314, + 220315, + 220316, + 220317, + 220318, + 20121821, + 21121651, + 22091511, + 22091512, + 22091513, + 22091514, + 22091515, + 22091516, + 22091517, + 22091518, + 22091519, + ]: + self.data.static.toggle_game_event(self.version, event_id, toggle=False) + self.logger.info(f"Disabled event {event_id}...") + def read_music(self, base_dir: str) -> None: self.logger.info(f"Reading music from {base_dir}...") for root, dirs, files in os.walk(base_dir): - for dir in dirs: + for dir in dirs: if os.path.exists(f"{root}/{dir}/Music.xml"): with open(f"{root}/{dir}/Music.xml", encoding="utf-8") as f: troot = ET.fromstring(f.read()) - song_id = int(troot.find('name').find('id').text) - title = troot.find('name').find('str').text - artist = troot.find('artistName').find('str').text - genre = troot.find('genreName').find('str').text - bpm = int(troot.find('bpm').text) - added_ver = troot.find('AddVersion').find('str').text + song_id = int(troot.find("name").find("id").text) + title = troot.find("name").find("str").text + artist = troot.find("artistName").find("str").text + genre = troot.find("genreName").find("str").text + bpm = int(troot.find("bpm").text) + added_ver = troot.find("AddVersion").find("str").text - note_data = troot.find('notesData').findall('Notes') + note_data = troot.find("notesData").findall("Notes") for dif in note_data: - path = dif.find('file').find('path').text + path = dif.find("file").find("path").text if path is not None: if os.path.exists(f"{root}/{dir}/{path}"): - chart_id = int(path.split(".")[0].split('_')[1]) - diff_num = float(f"{dif.find('level').text}.{dif.find('levelDecimal').text}") - note_designer = dif.find('notesDesigner').find('str').text + chart_id = int(path.split(".")[0].split("_")[1]) + diff_num = float( + f"{dif.find('level').text}.{dif.find('levelDecimal').text}" + ) + note_designer = ( + dif.find("notesDesigner").find("str").text + ) + + self.data.static.put_game_music( + self.version, + song_id, + chart_id, + title, + artist, + genre, + bpm, + added_ver, + diff_num, + note_designer, + ) + + self.logger.info( + f"Added music id {song_id} chart {chart_id}" + ) - self.data.static.put_game_music(self.version, song_id, chart_id, title, artist, - genre, bpm, added_ver, diff_num, note_designer) - - self.logger.info(f"Added music id {song_id} chart {chart_id}") - def read_tickets(self, base_dir: str) -> None: self.logger.info(f"Reading tickets from {base_dir}...") @@ -91,13 +309,35 @@ class Mai2Reader(BaseReader): for dir in dirs: if os.path.exists(f"{root}/{dir}/Ticket.xml"): with open(f"{root}/{dir}/Ticket.xml", encoding="utf-8") as f: - troot = ET.fromstring(f.read()) - name = troot.find('name').find('str').text - id = int(troot.find('name').find('id').text) - ticket_type = int(troot.find('ticketKind').find('id').text) - price = int(troot.find('creditNum').text) + name = troot.find("name").find("str").text + id = int(troot.find("name").find("id").text) + ticket_type = int(troot.find("ticketKind").find("id").text) + price = int(troot.find("creditNum").text) - self.data.static.put_game_ticket(self.version, id, ticket_type, price, name) + self.data.static.put_game_ticket( + self.version, id, ticket_type, price, name + ) self.logger.info(f"Added ticket {id}...") + + def read_old_events(self, events: Optional[List[Dict[str, str]]]) -> None: + if events is None: + return + + for event in events: + evt_id = int(event.get('イベントID', '0')) + evt_expire_time = float(event.get('オフ時強制時期', '0.0')) + is_exp = bool(int(event.get('海外許可', '0'))) + is_aou = bool(int(event.get('AOU許可', '0'))) + name = event.get('comment', f'evt_{evt_id}') + + self.data.static.put_game_event(self.version, 0, evt_id, name) + + if not (is_exp or is_aou): + self.data.static.toggle_game_event(self.version, evt_id, False) + + def read_old_music(self, scores: Optional[List[Dict[str, str]]], text: Optional[List[Dict[str, str]]]) -> None: + if scores is None or text is None: + return + # TODO diff --git a/titles/mai2/schema/__init__.py b/titles/mai2/schema/__init__.py index c2be969..7a8c060 100644 --- a/titles/mai2/schema/__init__.py +++ b/titles/mai2/schema/__init__.py @@ -3,4 +3,4 @@ from titles.mai2.schema.item import Mai2ItemData from titles.mai2.schema.static import Mai2StaticData from titles.mai2.schema.score import Mai2ScoreData -__all__ = [Mai2ProfileData, Mai2ItemData, Mai2StaticData, Mai2ScoreData] \ No newline at end of file +__all__ = [Mai2ProfileData, Mai2ItemData, Mai2StaticData, Mai2ScoreData] diff --git a/titles/mai2/schema/item.py b/titles/mai2/schema/item.py index 4f283d3..a6ed876 100644 --- a/titles/mai2/schema/item.py +++ b/titles/mai2/schema/item.py @@ -1,5 +1,6 @@ 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 @@ -12,287 +13,538 @@ character = Table( "mai2_item_character", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column("character_id", Integer, nullable=False), - Column("level", Integer, nullable=False, server_default="1"), - Column("awakening", Integer, nullable=False, server_default="0"), - Column("use_count", Integer, nullable=False, server_default="0"), - UniqueConstraint("user", "character_id", name="mai2_item_character_uk"), - mysql_charset='utf8mb4' + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("characterId", Integer), + Column("level", Integer), + Column("awakening", Integer), + Column("useCount", Integer), + Column("point", Integer), + UniqueConstraint("user", "characterId", name="mai2_item_character_uk"), + mysql_charset="utf8mb4", ) card = Table( "mai2_item_card", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column("card_kind", Integer, nullable=False), - Column("card_id", Integer, nullable=False), - Column("chara_id", Integer, nullable=False), - Column("map_id", Integer, nullable=False), - Column("start_date", String(255), nullable=False), - Column("end_date", String(255), nullable=False), - UniqueConstraint("user", "card_kind", "card_id", name="mai2_item_card_uk"), - mysql_charset='utf8mb4' + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("cardId", Integer), + Column("cardTypeId", Integer), + Column("charaId", Integer), + Column("mapId", Integer), + Column("startDate", TIMESTAMP, server_default=func.now()), + Column("endDate", TIMESTAMP), + UniqueConstraint("user", "cardId", "cardTypeId", name="mai2_item_card_uk"), + mysql_charset="utf8mb4", ) item = Table( "mai2_item_item", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column("item_kind", Integer, nullable=False), - Column("item_id", Integer, nullable=False), - Column("stock", Integer, nullable=False, server_default="1"), - Column("is_valid", Boolean, nullable=False, server_default="1"), - UniqueConstraint("user", "item_kind", "item_id", name="mai2_item_item_uk"), - mysql_charset='utf8mb4' + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("itemId", Integer), + Column("itemKind", Integer), + Column("stock", Integer), + Column("isValid", Boolean), + UniqueConstraint("user", "itemId", "itemKind", name="mai2_item_item_uk"), + mysql_charset="utf8mb4", ) map = Table( "mai2_item_map", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column("map_id", Integer, nullable=False), - Column("distance", Integer, nullable=False), - Column("is_lock", Boolean, nullable=False, server_default="0"), - Column("is_clear", Boolean, nullable=False, server_default="0"), - Column("is_complete", Boolean, nullable=False, server_default="0"), - UniqueConstraint("user", "map_id", name="mai2_item_map_uk"), - mysql_charset='utf8mb4' + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("mapId", Integer), + Column("distance", Integer), + Column("isLock", Boolean), + Column("isClear", Boolean), + Column("isComplete", Boolean), + UniqueConstraint("user", "mapId", name="mai2_item_map_uk"), + mysql_charset="utf8mb4", ) login_bonus = Table( "mai2_item_login_bonus", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column("bonus_id", Integer, nullable=False), - Column("point", Integer, nullable=False), - Column("is_current", Boolean, nullable=False, server_default="0"), - Column("is_complete", Boolean, nullable=False, server_default="0"), - UniqueConstraint("user", "bonus_id", name="mai2_item_login_bonus_uk"), - mysql_charset='utf8mb4' + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("bonusId", Integer), + Column("point", Integer), + Column("isCurrent", Boolean), + Column("isComplete", Boolean), + UniqueConstraint("user", "bonusId", name="mai2_item_login_bonus_uk"), + mysql_charset="utf8mb4", ) friend_season_ranking = Table( "mai2_item_friend_season_ranking", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column("season_id", Integer, nullable=False), - Column("point", Integer, nullable=False), - Column("rank", Integer, nullable=False), - Column("reward_get", Boolean, nullable=False), - Column("user_name", String(8), nullable=False), - Column("record_date", String(255), nullable=False), - UniqueConstraint("user", "season_id", "user_name", name="mai2_item_login_bonus_uk"), - mysql_charset='utf8mb4' + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("seasonId", Integer), + Column("point", Integer), + Column("rank", Integer), + Column("rewardGet", Boolean), + Column("userName", String(8)), + Column("recordDate", TIMESTAMP), + UniqueConstraint( + "user", "seasonId", "userName", name="mai2_item_friend_season_ranking_uk" + ), + mysql_charset="utf8mb4", ) favorite = Table( "mai2_item_favorite", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column("itemKind", Integer, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("itemKind", Integer), Column("itemIdList", JSON), UniqueConstraint("user", "itemKind", name="mai2_item_favorite_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) charge = Table( "mai2_item_charge", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column("charge_id", Integer, nullable=False), - Column("stock", Integer, nullable=False), - Column("purchase_date", String(255), nullable=False), - Column("valid_date", String(255), nullable=False), - UniqueConstraint("user", "charge_id", name="mai2_item_charge_uk"), - mysql_charset='utf8mb4' + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("chargeId", Integer), + Column("stock", Integer), + Column("purchaseDate", String(255)), + Column("validDate", String(255)), + UniqueConstraint("user", "chargeId", name="mai2_item_charge_uk"), + mysql_charset="utf8mb4", ) +print_detail = Table( + "mai2_item_print_detail", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("orderId", Integer), + Column("printNumber", Integer), + Column("printDate", TIMESTAMP, server_default=func.now()), + Column("serialId", String(20)), + Column("placeId", Integer), + Column("clientId", String(11)), + Column("printerSerialId", String(20)), + Column("cardRomVersion", Integer), + Column("isHolograph", Boolean, server_default="1"), + Column("printOption1", Boolean, server_default="0"), + Column("printOption2", Boolean, server_default="0"), + Column("printOption3", Boolean, server_default="0"), + Column("printOption4", Boolean, server_default="0"), + Column("printOption5", Boolean, server_default="0"), + Column("printOption6", Boolean, server_default="0"), + Column("printOption7", Boolean, server_default="0"), + Column("printOption8", Boolean, server_default="0"), + Column("printOption9", Boolean, server_default="0"), + Column("printOption10", Boolean, server_default="0"), + Column("created", String(255), server_default=""), + UniqueConstraint("user", "serialId", name="mai2_item_print_detail_uk"), + mysql_charset="utf8mb4", +) + + class Mai2ItemData(BaseData): - def put_item(self, user_id: int, item_kind: int, item_id: int, stock: int, is_valid: bool) -> None: + def put_item( + self, user_id: int, item_kind: int, item_id: int, stock: int, is_valid: bool + ) -> None: sql = insert(item).values( user=user_id, - item_kind=item_kind, - item_id=item_id, + itemKind=item_kind, + itemId=item_id, stock=stock, - is_valid=is_valid, + isValid=is_valid, ) conflict = sql.on_duplicate_key_update( stock=stock, - is_valid=is_valid, + isValid=is_valid, ) result = self.execute(conflict) if result is None: - self.logger.warn(f"put_item: failed to insert item! user_id: {user_id}, item_kind: {item_kind}, item_id: {item_id}") + self.logger.warning( + f"put_item: failed to insert item! user_id: {user_id}, item_kind: {item_kind}, item_id: {item_id}" + ) return None return result.lastrowid - + def get_items(self, user_id: int, item_kind: int = None) -> Optional[List[Row]]: if item_kind is None: sql = item.select(item.c.user == user_id) else: - sql = item.select(and_(item.c.user == user_id, item.c.item_kind == item_kind)) + sql = item.select( + and_(item.c.user == user_id, item.c.itemKind == item_kind) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - + def get_item(self, user_id: int, item_kind: int, item_id: int) -> Optional[Row]: - sql = item.select(and_(item.c.user == user_id, item.c.item_kind == item_kind, item.c.item_id == item_id)) + sql = item.select( + and_( + item.c.user == user_id, + item.c.itemKind == item_kind, + item.c.itemId == item_id, + ) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() - - def put_login_bonus(self, user_id: int, bonus_id: int, point: int, is_current: bool, is_complete: bool) -> None: + + def put_login_bonus( + self, + user_id: int, + bonus_id: int, + point: int, + is_current: bool, + is_complete: bool, + ) -> None: sql = insert(login_bonus).values( user=user_id, - bonus_id=bonus_id, + bonusId=bonus_id, point=point, - is_current=is_current, - is_complete=is_complete, + isCurrent=is_current, + isComplete=is_complete, ) conflict = sql.on_duplicate_key_update( point=point, - is_current=is_current, - is_complete=is_complete, + isCurrent=is_current, + isComplete=is_complete, ) result = self.execute(conflict) if result is None: - self.logger.warn(f"put_login_bonus: failed to insert item! user_id: {user_id}, bonus_id: {bonus_id}, point: {point}") + self.logger.warning( + f"put_login_bonus: failed to insert item! user_id: {user_id}, bonus_id: {bonus_id}, point: {point}" + ) return None return result.lastrowid - + def get_login_bonuses(self, user_id: int) -> Optional[List[Row]]: sql = login_bonus.select(login_bonus.c.user == user_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - + def get_login_bonus(self, user_id: int, bonus_id: int) -> Optional[Row]: - sql = login_bonus.select(and_(login_bonus.c.user == user_id, login_bonus.c.bonus_id == bonus_id)) + sql = login_bonus.select( + and_(login_bonus.c.user == user_id, login_bonus.c.bonus_id == bonus_id) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() - def put_map(self, user_id: int, map_id: int, distance: int, is_lock: bool, is_clear: bool, is_complete: bool) -> None: + def put_map( + self, + user_id: int, + map_id: int, + distance: int, + is_lock: bool, + is_clear: bool, + is_complete: bool, + ) -> None: sql = insert(map).values( user=user_id, - map_id=map_id, + mapId=map_id, distance=distance, - is_lock=is_lock, - is_clear=is_clear, - is_complete=is_complete, + isLock=is_lock, + isClear=is_clear, + isComplete=is_complete, ) conflict = sql.on_duplicate_key_update( distance=distance, - is_lock=is_lock, - is_clear=is_clear, - is_complete=is_complete, + isLock=is_lock, + isClear=is_clear, + isComplete=is_complete, ) result = self.execute(conflict) if result is None: - self.logger.warn(f"put_map: failed to insert item! user_id: {user_id}, map_id: {map_id}, distance: {distance}") + self.logger.warning( + f"put_map: failed to insert item! user_id: {user_id}, map_id: {map_id}, distance: {distance}" + ) return None return result.lastrowid - + def get_maps(self, user_id: int) -> Optional[List[Row]]: sql = map.select(map.c.user == user_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - + def get_map(self, user_id: int, map_id: int) -> Optional[Row]: - sql = map.select(and_(map.c.user == user_id, map.c.map_id == map_id)) + sql = map.select(and_(map.c.user == user_id, map.c.mapId == map_id)) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() - def put_character(self, user_id: int, character_id: int, level: int, awakening: int, use_count: int) -> None: + def put_character_(self, user_id: int, char_data: Dict) -> Optional[int]: + char_data["user"] = user_id + sql = insert(character).values(**char_data) + + conflict = sql.on_duplicate_key_update(**char_data) + result = self.execute(conflict) + if result is None: + self.logger.warning( + f"put_character_: failed to insert item! user_id: {user_id}" + ) + return None + return result.lastrowid + + def put_character( + self, + user_id: int, + character_id: int, + level: int, + awakening: int, + use_count: int, + ) -> None: sql = insert(character).values( user=user_id, - character_id=character_id, + characterId=character_id, level=level, awakening=awakening, - use_count=use_count, + useCount=use_count, ) conflict = sql.on_duplicate_key_update( level=level, awakening=awakening, - use_count=use_count, + useCount=use_count, ) result = self.execute(conflict) if result is None: - self.logger.warn(f"put_character: failed to insert item! user_id: {user_id}, character_id: {character_id}, level: {level}") + self.logger.warning( + f"put_character: failed to insert item! user_id: {user_id}, character_id: {character_id}, level: {level}" + ) return None return result.lastrowid - + def get_characters(self, user_id: int) -> Optional[List[Row]]: sql = character.select(character.c.user == user_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - + def get_character(self, user_id: int, character_id: int) -> Optional[Row]: - sql = character.select(and_(character.c.user == user_id, character.c.character_id == character_id)) + sql = character.select( + and_(character.c.user == user_id, character.c.character_id == character_id) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() - + def get_friend_season_ranking(self, user_id: int) -> Optional[Row]: sql = friend_season_ranking.select(friend_season_ranking.c.user == user_id) result = self.execute(sql) - if result is None:return None - return result.fetchone() - - def put_favorite(self, user_id: int, kind: int, item_id_list: List[int]) -> Optional[int]: - sql = insert(favorite).values( - user=user_id, - kind=kind, - item_id_list=item_id_list + if result is None: + return None + return result.fetchall() + + def put_friend_season_ranking( + self, aime_id: int, friend_season_ranking_data: Dict + ) -> Optional[int]: + sql = insert(friend_season_ranking).values( + user=aime_id, **friend_season_ranking_data ) - conflict = sql.on_duplicate_key_update( - item_id_list=item_id_list + conflict = sql.on_duplicate_key_update(**friend_season_ranking_data) + result = self.execute(conflict) + + if result is None: + self.logger.warning( + f"put_friend_season_ranking: failed to insert", + f"friend_season_ranking! aime_id: {aime_id}", + ) + return None + return result.lastrowid + + def put_favorite( + self, user_id: int, kind: int, item_id_list: List[int] + ) -> Optional[int]: + sql = insert(favorite).values( + user=user_id, kind=kind, item_id_list=item_id_list ) + conflict = sql.on_duplicate_key_update(item_id_list=item_id_list) + result = self.execute(conflict) if result is None: - self.logger.warn(f"put_favorite: failed to insert item! user_id: {user_id}, kind: {kind}") + self.logger.warning( + f"put_favorite: failed to insert item! user_id: {user_id}, kind: {kind}" + ) return None return result.lastrowid - + def get_favorites(self, user_id: int, kind: int = None) -> Optional[Row]: if kind is None: sql = favorite.select(favorite.c.user == user_id) else: - sql = favorite.select(and_( - favorite.c.user == user_id, - favorite.c.itemKind == kind - )) + sql = favorite.select( + and_(favorite.c.user == user_id, favorite.c.itemKind == kind) + ) result = self.execute(sql) - if result is None:return None - return result.fetchall() \ No newline at end of file + if result is None: + return None + return result.fetchall() + + def put_card( + self, + user_id: int, + card_type_id: int, + card_kind: int, + chara_id: int, + map_id: int, + start_date: datetime, + end_date: datetime, + ) -> Optional[Row]: + sql = insert(card).values( + user=user_id, + cardId=card_type_id, + cardTypeId=card_kind, + charaId=chara_id, + mapId=map_id, + startDate=start_date, + endDate=end_date, + ) + + conflict = sql.on_duplicate_key_update( + charaId=chara_id, mapId=map_id, startDate=start_date, endDate=end_date + ) + + result = self.execute(conflict) + if result is None: + self.logger.warning( + f"put_card: failed to insert card! user_id: {user_id}, kind: {card_kind}" + ) + return None + return result.lastrowid + + def get_cards(self, user_id: int, kind: int = None) -> Optional[Row]: + if kind is None: + sql = card.select(card.c.user == user_id) + else: + sql = card.select(and_(card.c.user == user_id, card.c.cardKind == kind)) + + sql = sql.order_by(card.c.startDate.desc()) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def put_charge( + self, + user_id: int, + charge_id: int, + stock: int, + purchase_date: str, + valid_date: str, + ) -> Optional[Row]: + sql = insert(charge).values( + user=user_id, + chargeId=charge_id, + stock=stock, + purchaseDate=purchase_date, + validDate=valid_date, + ) + + conflict = sql.on_duplicate_key_update( + stock=stock, purchaseDate=purchase_date, validDate=valid_date + ) + + result = self.execute(conflict) + if result is None: + self.logger.warning( + f"put_card: failed to insert charge! user_id: {user_id}, chargeId: {charge_id}" + ) + return None + return result.lastrowid + + def get_charges(self, user_id: int) -> Optional[Row]: + sql = charge.select(charge.c.user == user_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def put_user_print_detail( + self, aime_id: int, serial_id: str, user_print_data: Dict + ) -> Optional[int]: + sql = insert(print_detail).values( + user=aime_id, serialId=serial_id, **user_print_data + ) + + conflict = sql.on_duplicate_key_update(**user_print_data) + result = self.execute(conflict) + + if result is None: + self.logger.warning( + f"put_user_print_detail: Failed to insert! aime_id: {aime_id}" + ) + return None + return result.lastrowid diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index 346645c..211440d 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -14,7 +14,11 @@ detail = Table( "mai2_profile_detail", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("version", Integer, nullable=False), Column("userName", String(25)), Column("isNetMember", Integer), @@ -36,14 +40,15 @@ detail = Table( Column("charaLockSlot", JSON), Column("contentBit", BigInteger), Column("playCount", Integer), + Column("mapStock", Integer), # new with fes+ Column("eventWatchedDate", String(25)), Column("lastGameId", String(25)), Column("lastRomVersion", String(25)), Column("lastDataVersion", String(25)), Column("lastLoginDate", String(25)), - Column("lastPairLoginDate", String(25)), # new with uni+ + Column("lastPairLoginDate", String(25)), # new with uni+ Column("lastPlayDate", String(25)), - Column("lastTrialPlayDate", String(25)), # new with uni+ + Column("lastTrialPlayDate", String(25)), # new with uni+ Column("lastPlayCredit", Integer), Column("lastPlayMode", Integer), Column("lastPlaceId", Integer), @@ -90,16 +95,82 @@ detail = Table( Column("playerOldRating", BigInteger), Column("playerNewRating", BigInteger), Column("dateTime", BigInteger), - Column("banState", Integer), # new with uni+ + Column("banState", Integer), # new with uni+ UniqueConstraint("user", "version", name="mai2_profile_detail_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", +) + +detail_old = Table( + "maimai_profile_detail", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("version", Integer, nullable=False), + Column("lastDataVersion", Integer), + Column("userName", String(8)), + Column("point", Integer), + Column("totalPoint", Integer), + Column("iconId", Integer), + Column("nameplateId", Integer), + Column("frameId", Integer), + Column("trophyId", Integer), + Column("playCount", Integer), + Column("playVsCount", Integer), + Column("playSyncCount", Integer), + Column("winCount", Integer), + Column("helpCount", Integer), + Column("comboCount", Integer), + Column("feverCount", Integer), + Column("totalHiScore", Integer), + Column("totalEasyHighScore", Integer), + Column("totalBasicHighScore", Integer), + Column("totalAdvancedHighScore", Integer), + Column("totalExpertHighScore", Integer), + Column("totalMasterHighScore", Integer), + Column("totalReMasterHighScore", Integer), + Column("totalHighSync", Integer), + Column("totalEasySync", Integer), + Column("totalBasicSync", Integer), + Column("totalAdvancedSync", Integer), + Column("totalExpertSync", Integer), + Column("totalMasterSync", Integer), + Column("totalReMasterSync", Integer), + Column("playerRating", Integer), + Column("highestRating", Integer), + Column("rankAuthTailId", Integer), + Column("eventWatchedDate", String(255)), + Column("webLimitDate", String(255)), + Column("challengeTrackPhase", Integer), + Column("firstPlayBits", Integer), + Column("lastPlayDate", String(255)), + Column("lastPlaceId", Integer), + Column("lastPlaceName", String(255)), + Column("lastRegionId", Integer), + Column("lastRegionName", String(255)), + Column("lastClientId", String(255)), + Column("lastCountryCode", String(255)), + Column("eventPoint", Integer), + Column("totalLv", Integer), + Column("lastLoginBonusDay", Integer), + Column("lastSurvivalBonusDay", Integer), + Column("loginBonusLv", Integer), + UniqueConstraint("user", "version", name="maimai_profile_detail_uk"), + mysql_charset="utf8mb4", ) ghost = Table( "mai2_profile_ghost", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("version_int", Integer, nullable=False), Column("name", String(25)), Column("iconId", Integer), @@ -120,15 +191,21 @@ ghost = Table( Column("resultBitList", JSON), Column("resultNum", Integer), Column("achievement", Integer), - UniqueConstraint("user", "version", "musicId", "difficulty", name="mai2_profile_ghost_uk"), - mysql_charset='utf8mb4' + UniqueConstraint( + "user", "version", "musicId", "difficulty", name="mai2_profile_ghost_uk" + ), + mysql_charset="utf8mb4", ) extend = Table( "mai2_profile_extend", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("version", Integer, nullable=False), Column("selectMusicId", Integer), Column("selectDifficultyId", Integer), @@ -140,19 +217,25 @@ extend = Table( Column("isPhotoAgree", Boolean), Column("isGotoCodeRead", Boolean), Column("selectResultDetails", Boolean), + Column("selectResultScoreViewType", Integer), # new with fes+ Column("sortCategorySetting", Integer), Column("sortMusicSetting", Integer), Column("selectedCardList", JSON), Column("encountMapNpcList", JSON), + Column("playStatusSetting", Integer, server_default="0"), UniqueConstraint("user", "version", name="mai2_profile_extend_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) option = Table( "mai2_profile_option", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("version", Integer, nullable=False), Column("selectMusicId", Integer), Column("optionKind", Integer), @@ -160,6 +243,7 @@ option = Table( Column("slideSpeed", Integer), Column("touchSpeed", Integer), Column("tapDesign", Integer), + Column("tapSe", Integer, server_default="0"), Column("holdDesign", Integer), Column("slideDesign", Integer), Column("starType", Integer), @@ -169,6 +253,7 @@ option = Table( Column("touchSize", Integer), Column("starRotate", Integer), Column("dispCenter", Integer), + Column("outFrameType", Integer), # new with fes+ Column("dispChain", Integer), Column("dispRate", Integer), Column("dispBar", Integer), @@ -194,20 +279,119 @@ option = Table( Column("exVolume", Integer), Column("slideSe", Integer), Column("slideVolume", Integer), + Column("breakSlideVolume", Integer), # new with fes+ + Column("touchVolume", Integer), # new with fes+ Column("touchHoldVolume", Integer), Column("damageSeVolume", Integer), Column("headPhoneVolume", Integer), Column("sortTab", Integer), Column("sortMusic", Integer), UniqueConstraint("user", "version", name="mai2_profile_option_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", +) + +option_old = Table( + "maimai_profile_option", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("version", Integer, nullable=False), + Column("soudEffect", Integer), + Column("mirrorMode", Integer), + Column("guideSpeed", Integer), + Column("bgInfo", Integer), + Column("brightness", Integer), + Column("isStarRot", Integer), + Column("breakSe", Integer), + Column("slideSe", Integer), + Column("hardJudge", Integer), + Column("isTagJump", Integer), + Column("breakSeVol", Integer), + Column("slideSeVol", Integer), + Column("isUpperDisp", Integer), + Column("trackSkip", Integer), + Column("optionMode", Integer), + Column("simpleOptionParam", Integer), + Column("adjustTiming", Integer), + Column("dispTiming", Integer), + Column("timingPos", Integer), + Column("ansVol", Integer), + Column("noteVol", Integer), + Column("dmgVol", Integer), + Column("appealFlame", Integer), + Column("isFeverDisp", Integer), + Column("dispJudge", Integer), + Column("judgePos", Integer), + Column("ratingGuard", Integer), + Column("selectChara", Integer), + Column("sortType", Integer), + Column("filterGenre", Integer), + Column("filterLevel", Integer), + Column("filterRank", Integer), + Column("filterVersion", Integer), + Column("filterRec", Integer), + Column("filterFullCombo", Integer), + Column("filterAllPerfect", Integer), + Column("filterDifficulty", Integer), + Column("filterFullSync", Integer), + Column("filterReMaster", Integer), + Column("filterMaxFever", Integer), + Column("finalSelectId", Integer), + Column("finalSelectCategory", Integer), + UniqueConstraint("user", "version", name="maimai_profile_option_uk"), + mysql_charset="utf8mb4", +) + +web_opt = Table( + "maimai_profile_web_option", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("version", Integer, nullable=False), + Column("isNetMember", Boolean), + Column("dispRate", Integer), + Column("dispJudgeStyle", Integer), + Column("dispRank", Integer), + Column("dispHomeRanker", Integer), + Column("dispTotalLv", Integer), + UniqueConstraint("user", "version", name="maimai_profile_web_option_uk"), + mysql_charset="utf8mb4", +) + +grade_status = Table( + "maimai_profile_grade_status", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("gradeVersion", Integer), + Column("gradeLevel", Integer), + Column("gradeSubLevel", Integer), + Column("gradeMaxId", Integer), + UniqueConstraint("user", "gradeVersion", name="maimai_profile_grade_status_uk"), + mysql_charset="utf8mb4", ) rating = Table( "mai2_profile_rating", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("version", Integer, nullable=False), Column("rating", Integer), Column("ratingList", JSON), @@ -216,59 +400,145 @@ rating = Table( Column("nextNewRatingList", JSON), Column("udemae", JSON), UniqueConstraint("user", "version", name="mai2_profile_rating_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) region = Table( "mai2_profile_region", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("regionId", Integer), Column("playCount", Integer, server_default="1"), Column("created", String(25)), UniqueConstraint("user", "regionId", name="mai2_profile_region_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) activity = Table( "mai2_profile_activity", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column("kind", Integer, nullable=False), - Column("activityId", Integer, nullable=False), - Column("param1", Integer, nullable=False), - Column("param2", Integer, nullable=False), - Column("param3", Integer, nullable=False), - Column("param4", Integer, nullable=False), - Column("sortNumber", Integer, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("kind", Integer), + Column("activityId", Integer), + Column("param1", Integer), + Column("param2", Integer), + Column("param3", Integer), + Column("param4", Integer), + Column("sortNumber", Integer), UniqueConstraint("user", "kind", "activityId", name="mai2_profile_activity_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) +boss = Table( + "maimai_profile_boss", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("pandoraFlagList0", Integer), + Column("pandoraFlagList1", Integer), + Column("pandoraFlagList2", Integer), + Column("pandoraFlagList3", Integer), + Column("pandoraFlagList4", Integer), + Column("pandoraFlagList5", Integer), + Column("pandoraFlagList6", Integer), + Column("emblemFlagList", Integer), + UniqueConstraint("user", name="mai2_profile_boss_uk"), + mysql_charset="utf8mb4", +) + +recent_rating = Table( + "maimai_profile_recent_rating", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("userRecentRatingList", JSON), + UniqueConstraint("user", name="mai2_profile_recent_rating_uk"), + mysql_charset="utf8mb4", +) + +consec_logins = Table( + "mai2_profile_consec_logins", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("version", Integer, nullable=False), + Column("logins", Integer), + UniqueConstraint("user", "version", name="mai2_profile_consec_logins_uk"), + mysql_charset="utf8mb4", +) + + class Mai2ProfileData(BaseData): - def put_profile_detail(self, user_id: int, version: int, detail_data: Dict) -> Optional[Row]: + def put_profile_detail( + self, user_id: int, version: int, detail_data: Dict, is_dx: bool = True + ) -> Optional[Row]: detail_data["user"] = user_id detail_data["version"] = version - sql = insert(detail).values(**detail_data) + + if is_dx: + sql = insert(detail).values(**detail_data) + else: + sql = insert(detail_old).values(**detail_data) conflict = sql.on_duplicate_key_update(**detail_data) result = self.execute(conflict) if result is None: - self.logger.warn(f"put_profile: Failed to create profile! user_id {user_id}") + self.logger.warning( + f"put_profile: Failed to create profile! user_id {user_id} is_dx {is_dx}" + ) return None return result.lastrowid - def get_profile_detail(self, user_id: int, version: int) -> Optional[Row]: - sql = select(detail).where(and_(detail.c.user == user_id, detail.c.version == version)) + def get_profile_detail( + self, user_id: int, version: int, is_dx: bool = True + ) -> Optional[Row]: + if is_dx: + sql = ( + select(detail) + .where(and_(detail.c.user == user_id, detail.c.version <= version)) + .order_by(detail.c.version.desc()) + ) + + else: + sql = ( + select(detail_old) + .where( + and_(detail_old.c.user == user_id, detail_old.c.version <= version) + ) + .order_by(detail_old.c.version.desc()) + ) result = self.execute(sql) - if result is None:return None + if result is None: + return None return result.fetchone() - def put_profile_ghost(self, user_id: int, version: int, ghost_data: Dict) -> Optional[int]: + def put_profile_ghost( + self, user_id: int, version: int, ghost_data: Dict + ) -> Optional[int]: ghost_data["user"] = user_id ghost_data["version_int"] = version @@ -277,18 +547,25 @@ class Mai2ProfileData(BaseData): result = self.execute(conflict) if result is None: - self.logger.warn(f"put_profile_ghost: failed to update! {user_id}") + self.logger.warning(f"put_profile_ghost: failed to update! {user_id}") return None return result.lastrowid def get_profile_ghost(self, user_id: int, version: int) -> Optional[Row]: - sql = select(ghost).where(and_(ghost.c.user == user_id, ghost.c.version_int == version)) + sql = ( + select(ghost) + .where(and_(ghost.c.user == user_id, ghost.c.version_int <= version)) + .order_by(ghost.c.version.desc()) + ) result = self.execute(sql) - if result is None:return None + if result is None: + return None return result.fetchone() - def put_profile_extend(self, user_id: int, version: int, extend_data: Dict) -> Optional[int]: + def put_profile_extend( + self, user_id: int, version: int, extend_data: Dict + ) -> Optional[int]: extend_data["user"] = user_id extend_data["version"] = version @@ -297,38 +574,68 @@ class Mai2ProfileData(BaseData): result = self.execute(conflict) if result is None: - self.logger.warn(f"put_profile_extend: failed to update! {user_id}") + self.logger.warning(f"put_profile_extend: failed to update! {user_id}") return None return result.lastrowid def get_profile_extend(self, user_id: int, version: int) -> Optional[Row]: - sql = select(extend).where(and_(extend.c.user == user_id, extend.c.version == version)) + sql = ( + select(extend) + .where(and_(extend.c.user == user_id, extend.c.version <= version)) + .order_by(extend.c.version.desc()) + ) result = self.execute(sql) - if result is None:return None + if result is None: + return None return result.fetchone() - def put_profile_option(self, user_id: int, version: int, option_data: Dict) -> Optional[int]: + def put_profile_option( + self, user_id: int, version: int, option_data: Dict, is_dx: bool = True + ) -> Optional[int]: option_data["user"] = user_id option_data["version"] = version - sql = insert(option).values(**option_data) + if is_dx: + sql = insert(option).values(**option_data) + else: + sql = insert(option_old).values(**option_data) conflict = sql.on_duplicate_key_update(**option_data) result = self.execute(conflict) if result is None: - self.logger.warn(f"put_profile_option: failed to update! {user_id}") + self.logger.warning( + f"put_profile_option: failed to update! {user_id} is_dx {is_dx}" + ) return None return result.lastrowid - def get_profile_option(self, user_id: int, version: int) -> Optional[Row]: - sql = select(option).where(and_(option.c.user == user_id, option.c.version == version)) + def get_profile_option( + self, user_id: int, version: int, is_dx: bool = True + ) -> Optional[Row]: + if is_dx: + sql = ( + select(option) + .where(and_(option.c.user == user_id, option.c.version <= version)) + .order_by(option.c.version.desc()) + ) + else: + sql = ( + select(option_old) + .where( + and_(option_old.c.user == user_id, option_old.c.version <= version) + ) + .order_by(option_old.c.version.desc()) + ) result = self.execute(sql) - if result is None:return None + if result is None: + return None return result.fetchone() - - def put_profile_rating(self, user_id: int, version: int, rating_data: Dict) -> Optional[int]: + + def put_profile_rating( + self, user_id: int, version: int, rating_data: Dict + ) -> Optional[int]: rating_data["user"] = user_id rating_data["version"] = version @@ -337,31 +644,34 @@ class Mai2ProfileData(BaseData): result = self.execute(conflict) if result is None: - self.logger.warn(f"put_profile_rating: failed to update! {user_id}") + self.logger.warning(f"put_profile_rating: failed to update! {user_id}") return None return result.lastrowid def get_profile_rating(self, user_id: int, version: int) -> Optional[Row]: - sql = select(rating).where(and_(rating.c.user == user_id, rating.c.version == version)) + sql = ( + select(rating) + .where(and_(rating.c.user == user_id, rating.c.version <= version)) + .order_by(rating.c.version.desc()) + ) result = self.execute(sql) - if result is None:return None + if result is None: + return None return result.fetchone() def put_profile_region(self, user_id: int, region_id: int) -> Optional[int]: sql = insert(region).values( - user = user_id, - regionId = region_id, - created = datetime.strftime(datetime.now(), Mai2Constants.DATE_TIME_FORMAT) - ) - - conflict = sql.on_duplicate_key_update( - playCount = region.c.playCount + 1 + user=user_id, + regionId=region_id, + created=datetime.strftime(datetime.now(), Mai2Constants.DATE_TIME_FORMAT), ) + conflict = sql.on_duplicate_key_update(playCount=region.c.playCount + 1) + result = self.execute(conflict) if result is None: - self.logger.warn(f"put_region: failed to update! {user_id}") + self.logger.warning(f"put_region: failed to update! {user_id}") return None return result.lastrowid @@ -369,34 +679,165 @@ class Mai2ProfileData(BaseData): sql = select(region).where(region.c.user == user_id) result = self.execute(sql) - if result is None:return None + if result is None: + return None return result.fetchall() - + def put_profile_activity(self, user_id: int, activity_data: Dict) -> Optional[int]: if "id" in activity_data: activity_data["activityId"] = activity_data["id"] activity_data.pop("id") - + activity_data["user"] = user_id - + sql = insert(activity).values(**activity_data) conflict = sql.on_duplicate_key_update(**activity_data) result = self.execute(conflict) if result is None: - self.logger.warn(f"put_profile_activity: failed to update! user_id: {user_id}") + self.logger.warning( + f"put_profile_activity: failed to update! user_id: {user_id}" + ) return None return result.lastrowid - def get_profile_activity(self, user_id: int, kind: int = None) -> Optional[Row]: + def get_profile_activity( + self, user_id: int, kind: int = None + ) -> Optional[List[Row]]: sql = activity.select( and_( - activity.c.user == user_id, + activity.c.user == user_id, (activity.c.kind == kind) if kind is not None else True, ) ) result = self.execute(sql) - if result is None:return None + if result is None: + return None + return result.fetchall() + + def put_web_option( + self, user_id: int, version: int, web_opts: Dict + ) -> Optional[int]: + web_opts["user"] = user_id + web_opts["version"] = version + sql = insert(web_opt).values(**web_opts) + + conflict = sql.on_duplicate_key_update(**web_opts) + + result = self.execute(conflict) + if result is None: + self.logger.warning(f"put_web_option: failed to update! user_id: {user_id}") + return None + return result.lastrowid + + def get_web_option(self, user_id: int, version: int) -> Optional[Row]: + sql = web_opt.select( + and_(web_opt.c.user == user_id, web_opt.c.version == version) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def put_grade_status(self, user_id: int, grade_stat: Dict) -> Optional[int]: + grade_stat["user"] = user_id + sql = insert(grade_status).values(**grade_stat) + + conflict = sql.on_duplicate_key_update(**grade_stat) + + result = self.execute(conflict) + if result is None: + self.logger.warning( + f"put_grade_status: failed to update! user_id: {user_id}" + ) + return None + return result.lastrowid + + def get_grade_status(self, user_id: int) -> Optional[Row]: + sql = grade_status.select(grade_status.c.user == user_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def put_boss_list(self, user_id: int, boss_stat: Dict) -> Optional[int]: + boss_stat["user"] = user_id + sql = insert(boss).values(**boss_stat) + + conflict = sql.on_duplicate_key_update(**boss_stat) + + result = self.execute(conflict) + if result is None: + self.logger.warning(f"put_boss_list: failed to update! user_id: {user_id}") + return None + return result.lastrowid + + def get_boss_list(self, user_id: int) -> Optional[Row]: + sql = boss.select(boss.c.user == user_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def put_recent_rating(self, user_id: int, rr: Dict) -> Optional[int]: + sql = insert(recent_rating).values(user=user_id, userRecentRatingList=rr) + + conflict = sql.on_duplicate_key_update({"userRecentRatingList": rr}) + + result = self.execute(conflict) + if result is None: + self.logger.warning( + f"put_recent_rating: failed to update! user_id: {user_id}" + ) + return None + return result.lastrowid + + def get_recent_rating(self, user_id: int) -> Optional[Row]: + sql = recent_rating.select(recent_rating.c.user == user_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def add_consec_login(self, user_id: int, version: int) -> None: + sql = insert(consec_logins).values(user=user_id, version=version, logins=1) + + conflict = sql.on_duplicate_key_update(logins=consec_logins.c.logins + 1) + + result = self.execute(conflict) + if result is None: + self.logger.error( + f"Failed to update consecutive login count for user {user_id} version {version}" + ) + + def get_consec_login(self, user_id: int, version: int) -> Optional[Row]: + sql = select(consec_logins).where( + and_( + consec_logins.c.user == user_id, + consec_logins.c.version == version, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def reset_consec_login(self, user_id: int, version: int) -> Optional[Row]: + sql = consec_logins.update( + and_( + consec_logins.c.user == user_id, + consec_logins.c.version == version, + ) + ).values(logins=1) + + result = self.execute(sql) + if result is None: + return None return result.fetchone() diff --git a/titles/mai2/schema/score.py b/titles/mai2/schema/score.py index 0ef6da4..181a895 100644 --- a/titles/mai2/schema/score.py +++ b/titles/mai2/schema/score.py @@ -7,12 +7,17 @@ from sqlalchemy.engine import Row from sqlalchemy.dialects.mysql import insert from core.data.schema import BaseData, metadata +from core.data import cached best_score = Table( "mai2_score_best", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("musicId", Integer), Column("level", Integer), Column("playCount", Integer), @@ -21,15 +26,20 @@ best_score = Table( Column("syncStatus", Integer), Column("deluxscoreMax", Integer), Column("scoreRank", Integer), + Column("extNum1", Integer, server_default="0"), UniqueConstraint("user", "musicId", "level", name="mai2_score_best_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) playlog = Table( "mai2_playlog", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("userId", BigInteger), Column("orderId", Integer), Column("playlogId", BigInteger), @@ -135,14 +145,20 @@ playlog = Table( Column("isNewFree", Boolean), Column("extNum1", Integer), Column("extNum2", Integer), - mysql_charset='utf8mb4' + Column("extNum4", Integer, server_default="0"), + Column("trialPlayAchievement", Integer), + mysql_charset="utf8mb4", ) course = Table( "mai2_score_course", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("courseId", Integer), Column("isLastClear", Boolean), Column("totalRestlife", Integer), @@ -156,58 +172,180 @@ course = Table( Column("bestDeluxscore", Integer), Column("bestDeluxscoreDate", String(25)), UniqueConstraint("user", "courseId", name="mai2_score_best_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", +) + +playlog_old = Table( + "maimai_playlog", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("version", Integer), + # Pop access code + Column("orderId", Integer), + Column("sortNumber", Integer), + Column("placeId", Integer), + Column("placeName", String(255)), + Column("country", String(255)), + Column("regionId", Integer), + Column("playDate", String(255)), + Column("userPlayDate", String(255)), + Column("musicId", Integer), + Column("level", Integer), + Column("gameMode", Integer), + Column("rivalNum", Integer), + Column("track", Integer), + Column("eventId", Integer), + Column("isFreeToPlay", Boolean), + Column("playerRating", Integer), + Column("playedUserId1", Integer), + Column("playedUserId2", Integer), + Column("playedUserId3", Integer), + Column("playedUserName1", String(255)), + Column("playedUserName2", String(255)), + Column("playedUserName3", String(255)), + Column("playedMusicLevel1", Integer), + Column("playedMusicLevel2", Integer), + Column("playedMusicLevel3", Integer), + Column("achievement", Integer), + Column("score", Integer), + Column("tapScore", Integer), + Column("holdScore", Integer), + Column("slideScore", Integer), + Column("breakScore", Integer), + Column("syncRate", Integer), + Column("vsWin", Integer), + Column("isAllPerfect", Boolean), + Column("fullCombo", Integer), + Column("maxFever", Integer), + Column("maxCombo", Integer), + Column("tapPerfect", Integer), + Column("tapGreat", Integer), + Column("tapGood", Integer), + Column("tapBad", Integer), + Column("holdPerfect", Integer), + Column("holdGreat", Integer), + Column("holdGood", Integer), + Column("holdBad", Integer), + Column("slidePerfect", Integer), + Column("slideGreat", Integer), + Column("slideGood", Integer), + Column("slideBad", Integer), + Column("breakPerfect", Integer), + Column("breakGreat", Integer), + Column("breakGood", Integer), + Column("breakBad", Integer), + Column("judgeStyle", Integer), + Column("isTrackSkip", Boolean), + Column("isHighScore", Boolean), + Column("isChallengeTrack", Boolean), + Column("challengeLife", Integer), + Column("challengeRemain", Integer), + Column("isAllPerfectPlus", Integer), + mysql_charset="utf8mb4", +) + +best_score_old = Table( + "maimai_score_best", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("musicId", Integer), + Column("level", Integer), + Column("playCount", Integer), + Column("achievement", Integer), + Column("scoreMax", Integer), + Column("syncRateMax", Integer), + Column("isAllPerfect", Boolean), + Column("isAllPerfectPlus", Integer), + Column("fullCombo", Integer), + Column("maxFever", Integer), + UniqueConstraint("user", "musicId", "level", name="maimai_score_best_uk"), + mysql_charset="utf8mb4", ) class Mai2ScoreData(BaseData): - def put_best_score(self, user_id: int, score_data: Dict) -> Optional[int]: - sql = insert(best_score).values(**score_data) + def put_best_score(self, user_id: int, score_data: Dict, is_dx: bool = True) -> Optional[int]: + score_data["user"] = user_id + if is_dx: + sql = insert(best_score).values(**score_data) + else: + sql = insert(best_score_old).values(**score_data) conflict = sql.on_duplicate_key_update(**score_data) result = self.execute(conflict) if result is None: - self.logger.error(f"put_best_score: Failed to insert best score! user_id {user_id}") + self.logger.error( + f"put_best_score: Failed to insert best score! user_id {user_id} is_dx {is_dx}" + ) return None return result.lastrowid - def get_best_scores(self, user_id: int, song_id: int = None) -> Optional[List[Row]]: - sql = best_score.select( - and_( + @cached(2) + def get_best_scores(self, user_id: int, song_id: int = None, is_dx: bool = True) -> Optional[List[Row]]: + if is_dx: + 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.song_id == song_id) if song_id is not None else True, + ) + ) + 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, ) ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - - def get_best_score(self, user_id: int, song_id: int, chart_id: int) -> Optional[Row]: + + def get_best_score( + self, user_id: int, song_id: int, chart_id: int + ) -> Optional[Row]: sql = best_score.select( and_( - best_score.c.user == user_id, - best_score.c.song_id == song_id, - best_score.c.chart_id == chart_id - ) + best_score.c.user == user_id, + best_score.c.song_id == song_id, + best_score.c.chart_id == chart_id, ) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() - def put_playlog(self, user_id: int, playlog_data: Dict) -> Optional[int]: - sql = insert(playlog).values(**playlog_data) + def put_playlog(self, user_id: int, playlog_data: Dict, is_dx: bool = True) -> Optional[int]: + playlog_data["user"] = user_id + + if is_dx: + sql = insert(playlog).values(**playlog_data) + else: + sql = insert(playlog_old).values(**playlog_data) conflict = sql.on_duplicate_key_update(**playlog_data) result = self.execute(conflict) if result is None: - self.logger.error(f"put_playlog: Failed to insert! user_id {user_id}") + self.logger.error(f"put_playlog: Failed to insert! user_id {user_id} is_dx {is_dx}") return None return result.lastrowid - + def put_course(self, user_id: int, course_data: Dict) -> Optional[int]: + course_data["user"] = user_id sql = insert(course).values(**course_data) conflict = sql.on_duplicate_key_update(**course_data) @@ -217,10 +355,11 @@ class Mai2ScoreData(BaseData): self.logger.error(f"put_course: Failed to insert! user_id {user_id}") return None return result.lastrowid - + def get_courses(self, user_id: int) -> Optional[List[Row]]: - sql = course.select(best_score.c.user == user_id) + sql = course.select(course.c.user == user_id) result = self.execute(sql) - if result is None: return None - return result.fetchone() + if result is None: + return None + return result.fetchall() diff --git a/titles/mai2/schema/static.py b/titles/mai2/schema/static.py index 733e2ef..0f5bfad 100644 --- a/titles/mai2/schema/static.py +++ b/titles/mai2/schema/static.py @@ -12,20 +12,21 @@ event = Table( "mai2_static_event", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("version", Integer,nullable=False), + Column("version", Integer, nullable=False), Column("eventId", Integer), Column("type", Integer), Column("name", String(255)), + Column("startDate", TIMESTAMP, server_default=func.now()), Column("enabled", Boolean, server_default="1"), UniqueConstraint("version", "eventId", "type", name="mai2_static_event_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) music = Table( "mai2_static_music", metadata, - Column("id", Integer, primary_key=True, nullable=False), - Column("version", Integer,nullable=False), + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), Column("songId", Integer), Column("chartId", Integer), Column("title", String(255)), @@ -36,39 +37,58 @@ music = Table( Column("difficulty", Float), Column("noteDesigner", String(255)), UniqueConstraint("songId", "chartId", "version", name="mai2_static_music_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) ticket = Table( "mai2_static_ticket", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("version", Integer,nullable=False), + Column("version", Integer, nullable=False), Column("ticketId", Integer), Column("kind", Integer), Column("name", String(255)), Column("price", Integer, server_default="1"), Column("enabled", Boolean, server_default="1"), - UniqueConstraint("version","ticketId", name="mai2_static_ticket_uk"), - mysql_charset='utf8mb4' + UniqueConstraint("version", "ticketId", name="mai2_static_ticket_uk"), + mysql_charset="utf8mb4", ) +cards = Table( + "mai2_static_cards", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column("cardId", Integer, nullable=False), + Column("cardName", String(255), nullable=False), + Column("startDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"), + Column("endDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"), + Column("noticeStartDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"), + Column("noticeEndDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"), + Column("enabled", Boolean, server_default="1"), + UniqueConstraint("version", "cardId", "cardName", name="mai2_static_cards_uk"), + mysql_charset="utf8mb4", +) + + class Mai2StaticData(BaseData): - def put_game_event(self, version: int, type: int, event_id: int, name: str) -> Optional[int]: + def put_game_event( + self, version: int, type: int, event_id: int, name: str + ) -> Optional[int]: sql = insert(event).values( - version = version, - type = type, - eventId = event_id, - name = name, + version=version, + type=type, + eventId=event_id, + name=name, ) - conflict = sql.on_duplicate_key_update( - eventId = event_id - ) + conflict = sql.on_duplicate_key_update(eventId=event_id) result = self.execute(conflict) if result is None: - self.logger.warning(f"put_game_event: Failed to insert event! event_id {event_id} type {type} name {name}") + self.logger.warning( + f"put_game_event: Failed to insert event! event_id {event_id} type {type} name {name}" + ) return result.lastrowid def get_game_events(self, version: int) -> Optional[List[Row]]: @@ -78,101 +98,153 @@ class Mai2StaticData(BaseData): if result is None: return None return result.fetchall() - + def get_enabled_events(self, version: int) -> Optional[List[Row]]: - sql = select(event).where(and_( - event.c.version == version, - event.c.enabled == True - )) - - result = self.execute(sql) - if result is None: return None - return result.fetchall() - - def toggle_game_events(self, version: int, event_id: int, toggle: bool) -> Optional[List]: - sql = event.update(and_(event.c.version == version, event.c.event_id == event_id)).values( - enabled = int(toggle) + sql = select(event).where( + and_(event.c.version == version, event.c.enabled == True) ) result = self.execute(sql) if result is None: - self.logger.warning(f"toggle_game_events: Failed to update event! event_id {event_id} toggle {toggle}") + return None + return result.fetchall() + + def toggle_game_event( + self, version: int, event_id: int, toggle: bool + ) -> Optional[List]: + sql = event.update( + and_(event.c.version == version, event.c.eventId == event_id) + ).values(enabled=int(toggle)) + + result = self.execute(sql) + if result is None: + self.logger.warning( + f"toggle_game_event: Failed to update event! event_id {event_id} toggle {toggle}" + ) return result.last_updated_params() - - def put_game_music(self, version: int, song_id: int, chart_id: int, title: str, artist: str, - genre: str, bpm: str, added_version: str, difficulty: float, note_designer: str) -> None: + + def put_game_music( + self, + version: int, + song_id: int, + chart_id: int, + title: str, + artist: str, + genre: str, + bpm: str, + added_version: str, + difficulty: float, + note_designer: str, + ) -> None: sql = insert(music).values( - version = version, - songId = song_id, - chartId = chart_id, - title = title, - artist = artist, - genre = genre, - bpm = bpm, - addedVersion = added_version, - difficulty = difficulty, - noteDesigner = note_designer, + version=version, + songId=song_id, + chartId=chart_id, + title=title, + artist=artist, + genre=genre, + bpm=bpm, + addedVersion=added_version, + difficulty=difficulty, + noteDesigner=note_designer, ) conflict = sql.on_duplicate_key_update( - title = title, - artist = artist, - genre = genre, - bpm = bpm, - addedVersion = added_version, - difficulty = difficulty, - noteDesigner = note_designer, + title=title, + artist=artist, + genre=genre, + bpm=bpm, + addedVersion=added_version, + difficulty=difficulty, + noteDesigner=note_designer, ) result = self.execute(conflict) if result is None: - self.logger.warn(f"Failed to insert song {song_id} chart {chart_id}") + self.logger.warning(f"Failed to insert song {song_id} chart {chart_id}") return None return result.lastrowid - - def put_game_ticket(self, version: int, ticket_id: int, ticket_type: int, ticket_price: int, name: str) -> Optional[int]: + + def put_game_ticket( + self, + version: int, + ticket_id: int, + ticket_type: int, + ticket_price: int, + name: str, + ) -> Optional[int]: sql = insert(ticket).values( - version = version, - ticketId = ticket_id, - kind = ticket_type, - price = ticket_price, - name = name + version=version, + ticketId=ticket_id, + kind=ticket_type, + price=ticket_price, + name=name, ) - - conflict = sql.on_duplicate_key_update( - price = ticket_price + + conflict = sql.on_duplicate_key_update(price=ticket_price) + + conflict = sql.on_duplicate_key_update(price=ticket_price) + + result = self.execute(conflict) + if result is None: + self.logger.warning(f"Failed to insert charge {ticket_id} type {ticket_type}") + return None + return result.lastrowid + + def get_enabled_tickets( + self, version: int, kind: int = None + ) -> Optional[List[Row]]: + if kind is not None: + sql = select(ticket).where( + and_( + ticket.c.version == version, + ticket.c.enabled == True, + ticket.c.kind == kind, + ) + ) + else: + sql = select(ticket).where( + and_(ticket.c.version == version, ticket.c.enabled == True) ) - result = self.execute(conflict) - if result is None: - self.logger.warn(f"Failed to insert charge {ticket_id} type {ticket_type}") - return None - return result.lastrowid - - def get_enabled_tickets(self, version: int, kind: int = None) -> Optional[List[Row]]: - if kind is not None: - sql = select(ticket).where(and_( - ticket.c.version == version, - ticket.c.enabled == True, - ticket.c.kind == kind - )) - else: - sql = select(ticket).where(and_( - ticket.c.version == version, - ticket.c.enabled == True - )) - result = self.execute(sql) - if result is None:return None + if result is None: + return None return result.fetchall() - def get_music_chart(self, version: int, song_id: int, chart_id: int) -> Optional[List[Row]]: - sql = select(music).where(and_( - music.c.version == version, - music.c.songId == song_id, - music.c.chartId == chart_id - )) + def get_music_chart( + self, version: int, song_id: int, chart_id: int + ) -> Optional[List[Row]]: + sql = select(music).where( + and_( + music.c.version == version, + music.c.songId == song_id, + music.c.chartId == chart_id, + ) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() + + def put_card(self, version: int, card_id: int, card_name: str, **card_data) -> int: + sql = insert(cards).values( + version=version, cardId=card_id, cardName=card_name, **card_data + ) + + conflict = sql.on_duplicate_key_update(**card_data) + + result = self.execute(conflict) + if result is None: + self.logger.warning(f"Failed to insert card {card_id}") + return None + return result.lastrowid + + def get_enabled_cards(self, version: int) -> Optional[List[Row]]: + sql = cards.select(and_(cards.c.version == version, cards.c.enabled == True)) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() diff --git a/titles/mai2/splash.py b/titles/mai2/splash.py index 690645b..026e3ae 100644 --- a/titles/mai2/splash.py +++ b/titles/mai2/splash.py @@ -4,11 +4,12 @@ import pytz import json from core.config import CoreConfig -from titles.mai2.base import Mai2Base +from titles.mai2.dxplus import Mai2DXPlus from titles.mai2.config import Mai2Config from titles.mai2.const import Mai2Constants -class Mai2Splash(Mai2Base): + +class Mai2Splash(Mai2DXPlus): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) - self.version = Mai2Constants.VER_MAIMAI_DX_SPLASH \ No newline at end of file + self.version = Mai2Constants.VER_MAIMAI_DX_SPLASH diff --git a/titles/mai2/splashplus.py b/titles/mai2/splashplus.py index eb6f940..438941f 100644 --- a/titles/mai2/splashplus.py +++ b/titles/mai2/splashplus.py @@ -4,11 +4,12 @@ import pytz import json from core.config import CoreConfig -from titles.mai2.base import Mai2Base +from titles.mai2.splash import Mai2Splash from titles.mai2.config import Mai2Config from titles.mai2.const import Mai2Constants -class Mai2SplashPlus(Mai2Base): + +class Mai2SplashPlus(Mai2Splash): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) - self.version = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS \ No newline at end of file + self.version = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS diff --git a/titles/mai2/universe.py b/titles/mai2/universe.py index be6472c..d25a295 100644 --- a/titles/mai2/universe.py +++ b/titles/mai2/universe.py @@ -1,14 +1,220 @@ 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.base import Mai2Base +from titles.mai2.splashplus import Mai2SplashPlus from titles.mai2.const import Mai2Constants from titles.mai2.config import Mai2Config -class Mai2Universe(Mai2Base): + +class Mai2Universe(Mai2SplashPlus): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) - self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE \ No newline at end of file + self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE + + def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + p = 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, + } + + def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: + # user already exists, because the preview checks that already + p = self.data.profile.get_profile_detail(data["userId"], self.version) + + cards = 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} + + def handle_cm_login_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1} + + def handle_cm_logout_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1} + + def handle_cm_get_selling_card_api_request(self, data: Dict) -> Dict: + selling_cards = 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} + + def handle_cm_get_user_card_api_request(self, data: Dict) -> Dict: + user_cards = 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], + } + + def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: + super().handle_get_user_item_api_request(data) + + def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: + characters = 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, + } + + def handle_cm_get_user_card_print_error_api_request(self, data: Dict) -> Dict: + return {"length": 0, "userPrintDetailList": []} + + 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"] + 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 = 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 + 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") + + 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), + } + + def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: + return { + "returnCode": 1, + "orderId": 0, + "serialId": data["userPrintlog"]["serialId"], + } + + def handle_cm_upsert_buy_card_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1} diff --git a/titles/mai2/universeplus.py b/titles/mai2/universeplus.py index 795206e..e45c719 100644 --- a/titles/mai2/universeplus.py +++ b/titles/mai2/universeplus.py @@ -1,14 +1,19 @@ -from typing import Any, List, Dict -from datetime import datetime, timedelta -import pytz -import json +from typing import Dict from core.config import CoreConfig -from titles.mai2.base import Mai2Base +from titles.mai2.universe import Mai2Universe from titles.mai2.const import Mai2Constants from titles.mai2.config import Mai2Config -class Mai2UniversePlus(Mai2Base): + +class Mai2UniversePlus(Mai2Universe): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) - self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS \ No newline at end of file + self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS + + def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = super().handle_cm_get_user_preview_api_request(data) + + # hardcode lastDataVersion for CardMaker 1.35 + user_data["lastDataVersion"] = "1.25.00" + return user_data diff --git a/titles/ongeki/__init__.py b/titles/ongeki/__init__.py index 26e107c..f12343d 100644 --- a/titles/ongeki/__init__.py +++ b/titles/ongeki/__init__.py @@ -2,17 +2,11 @@ from titles.ongeki.index import OngekiServlet from titles.ongeki.const import OngekiConstants from titles.ongeki.database import OngekiData from titles.ongeki.read import OngekiReader +from titles.ongeki.frontend import OngekiFrontend index = OngekiServlet database = OngekiData reader = OngekiReader - -use_default_title = True -include_protocol = True -title_secure = False +frontend = OngekiFrontend game_codes = [OngekiConstants.GAME_CODE] -trailing_slash = True -use_default_host = False -host = "" - -current_schema_version = 2 \ No newline at end of file +current_schema_version = 6 diff --git a/titles/ongeki/base.py b/titles/ongeki/base.py index 8778645..596fb22 100644 --- a/titles/ongeki/base.py +++ b/titles/ongeki/base.py @@ -4,6 +4,7 @@ import json import logging from enum import Enum +import pytz from core.config import CoreConfig from core.data.cache import cached from titles.ongeki.const import OngekiConstants @@ -11,6 +12,7 @@ from titles.ongeki.config import OngekiConfig from titles.ongeki.database import OngekiData from titles.ongeki.config import OngekiConfig + class OngekiBattleGrade(Enum): FAILED = 0 DRAW = 1 @@ -21,6 +23,7 @@ class OngekiBattleGrade(Enum): UNBELIEVABLE_GOLD = 6 UNBELIEVABLE_RAINBOW = 7 + class OngekiBattlePointGrade(Enum): FRESHMAN = 0 KYU10 = 1 @@ -45,20 +48,22 @@ class OngekiBattlePointGrade(Enum): DAN10 = 20 SODEN = 21 + class OngekiTechnicalGrade(Enum): - D = 0 - C = 1 - B = 2 - BB = 3 - BBB = 4 - A = 5 - AA = 6 - AAA = 7 - S = 8 - SS = 9 - SSS = 10 + D = 0 + C = 1 + B = 2 + BB = 3 + BBB = 4 + A = 5 + AA = 6 + AAA = 7 + S = 8 + SS = 9 + SSS = 10 SSSp = 11 + class OngekiDifficulty(Enum): BASIC = 0 ADVANCED = 1 @@ -66,6 +71,7 @@ class OngekiDifficulty(Enum): MASTER = 3 LUNATIC = 10 + class OngekiGPLogKind(Enum): NONE = 0 BUY1_START = 1 @@ -82,22 +88,46 @@ class OngekiGPLogKind(Enum): PAY_MAS_UNLOCK = 13 PAY_MONEY = 14 -class OngekiBase(): +class OngekiBase: def __init__(self, core_cfg: CoreConfig, game_cfg: OngekiConfig) -> None: self.core_cfg = core_cfg self.game_cfg = game_cfg self.data = OngekiData(core_cfg) self.date_time_format = "%Y-%m-%d %H:%M:%S" - self.date_time_format_ext = "%Y-%m-%d %H:%M:%S.%f" # needs to be lopped off at [:-5] + self.date_time_format_ext = ( + "%Y-%m-%d %H:%M:%S.%f" # needs to be lopped off at [:-5] + ) self.date_time_format_short = "%Y-%m-%d" self.logger = logging.getLogger("ongeki") self.game = OngekiConstants.GAME_CODE self.version = OngekiConstants.VER_ONGEKI def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - reboot_start = date.strftime(datetime.now() + timedelta(hours=3), self.date_time_format) - reboot_end = date.strftime(datetime.now() + timedelta(hours=4), self.date_time_format) + # if reboot start/end time is not defined use the default behavior of being a few hours ago + if self.core_cfg.title.reboot_start_time == "" or self.core_cfg.title.reboot_end_time == "": + reboot_start = datetime.strftime( + datetime.utcnow() + timedelta(hours=6), self.date_time_format + ) + reboot_end = datetime.strftime( + datetime.utcnow() + timedelta(hours=7), self.date_time_format + ) + else: + # get current datetime in JST + current_jst = datetime.now(pytz.timezone('Asia/Tokyo')).date() + + # parse config start/end times into datetime + reboot_start_time = datetime.strptime(self.core_cfg.title.reboot_start_time, "%H:%M") + reboot_end_time = datetime.strptime(self.core_cfg.title.reboot_end_time, "%H:%M") + + # offset datetimes with current date/time + reboot_start_time = reboot_start_time.replace(year=current_jst.year, month=current_jst.month, day=current_jst.day, tzinfo=pytz.timezone('Asia/Tokyo')) + reboot_end_time = reboot_end_time.replace(year=current_jst.year, month=current_jst.month, day=current_jst.day, tzinfo=pytz.timezone('Asia/Tokyo')) + + # create strings for use in gameSetting + reboot_start = reboot_start_time.strftime(self.date_time_format) + reboot_end = reboot_end_time.strftime(self.date_time_format) + return { "gameSetting": { "dataVersion": "1.00.00", @@ -127,21 +157,43 @@ class OngekiBase(): return {"type": data["type"], "length": 0, "gameIdlistList": []} def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: - return {"length": 0, "gameRankingList": []} - + game_ranking_list = self.data.static.get_ranking_list(self.version) + + ranking_list = [] + for music in game_ranking_list: + tmp = music._asdict() + ranking_list.append(tmp) + + if ranking_list is None: + return {"length": 0, "gameRankingList": []} + return { + "type": data["type"], + "gameRankingList": ranking_list, + } + def handle_get_game_point_api_request(self, data: Dict) -> Dict: - """ - Sets the GP ammount for A and B sets for 1 - 3 crdits - """ - return {"length":6,"gamePointList":[ - {"type":0,"cost":100,"startDate":"2000-01-01 05:00:00.0","endDate":"2099-01-01 05:00:00.0"}, - {"type":1,"cost":200,"startDate":"2000-01-01 05:00:00.0","endDate":"2099-01-01 05:00:00.0"}, - {"type":2,"cost":300,"startDate":"2000-01-01 05:00:00.0","endDate":"2099-01-01 05:00:00.0"}, - {"type":3,"cost":120,"startDate":"2000-01-01 05:00:00.0","endDate":"2099-01-01 05:00:00.0"}, - {"type":4,"cost":240,"startDate":"2000-01-01 05:00:00.0","endDate":"2099-01-01 05:00:00.0"}, - {"type":5,"cost":360,"startDate":"2000-01-01 05:00:00.0","endDate":"2099-01-01 05:00:00.0"} - ]} - + get_game_point = self.data.static.get_static_game_point() + game_point = [] + + if not get_game_point: + self.logger.info(f"GP table is empty, inserting defaults") + self.data.static.put_static_game_point_defaults() + get_game_point = self.data.static.get_static_game_point() + for gp in get_game_point: + tmp = gp._asdict() + game_point.append(tmp) + return { + "length": len(game_point), + "gamePointList": game_point, + } + for gp in get_game_point: + tmp = gp._asdict() + game_point.append(tmp) + return { + "length": len(game_point), + "gamePointList": game_point, + } + def handle_game_login_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "gameLogin"} @@ -152,11 +204,39 @@ class OngekiBase(): return {"returnCode": 1, "apiName": "ExtendLockTimeApi"} def handle_get_game_reward_api_request(self, data: Dict) -> Dict: - # TODO: reward list - return {"length": 0, "gameRewardList": []} + get_game_rewards = self.data.static.get_reward_list(self.version) + + reward_list = [] + for reward in get_game_rewards: + tmp = reward._asdict() + tmp.pop("id") + tmp.pop("version") + tmp.pop("rewardname") + reward_list.append(tmp) + + if reward_list is None: + return {"length": 0, "gameRewardList": []} + return { + "length": len(reward_list), + "gameRewardList": reward_list, + } def handle_get_game_present_api_request(self, data: Dict) -> Dict: - return {"length": 0, "gamePresentList": []} + get_present = self.data.static.get_present_list(self.version) + + present_list = [] + for present in get_present: + tmp = present._asdict() + tmp.pop("id") + tmp.pop("version") + present_list.append(tmp) + + if present_list is None: + return {"length": 0, "gamePresentList": []} + return { + "length": len(present_list), + "gamePresentList": present_list, + } def handle_get_game_message_api_request(self, data: Dict) -> Dict: return {"length": 0, "gameMessageList": []} @@ -165,12 +245,41 @@ class OngekiBase(): return {"length": 0, "gameSaleList": []} def handle_get_game_tech_music_api_request(self, data: Dict) -> Dict: - return {"length": 0, "gameTechMusicList": []} + music_list = self.data.static.get_tech_music(self.version) + + prep_music_list = [] + for music in music_list: + tmp = music._asdict() + tmp.pop("id") + tmp.pop("version") + prep_music_list.append(tmp) + + if prep_music_list is None: + return {"length": 0, "gameTechMusicList": []} + + return { + "length": len(prep_music_list), + "gameTechMusicList": prep_music_list, + } def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: + if self.core_cfg.server.is_develop: + return {"returnCode": 1, "apiName": "UpsertClientSettingApi"} + + client_id = data["clientId"] + client_setting_data = data["clientSetting"] + cab = self.data.arcade.get_machine(client_id) + if cab is not None: + self.data.static.put_client_setting_data(cab['id'], client_setting_data) return {"returnCode": 1, "apiName": "UpsertClientSettingApi"} def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict: + if self.core_cfg.server.is_develop: + return {"returnCode": 1, "apiName": "UpsertClientTestmodeApi"} + + region_id = data["regionId"] + client_testmode_data = data["clientTestmode"] + self.data.static.put_client_testmode_data(region_id, client_testmode_data) return {"returnCode": 1, "apiName": "UpsertClientTestmodeApi"} def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: @@ -184,11 +293,19 @@ class OngekiBase(): def handle_upsert_user_gplog_api_request(self, data: Dict) -> Dict: user = data["userId"] - if user >= 200000000000000: # Account for guest play + if user >= 200000000000000: # Account for guest play user = None - self.data.log.put_gp_log(user, data["usedCredit"], data["placeName"], data["userGplog"]["trxnDate"], - data["userGplog"]["placeId"], data["userGplog"]["kind"], data["userGplog"]["pattern"], data["userGplog"]["currentGP"]) + self.data.log.put_gp_log( + user, + data["usedCredit"], + data["placeName"], + data["userGplog"]["trxnDate"], + data["userGplog"]["placeId"], + data["userGplog"]["kind"], + data["userGplog"]["pattern"], + data["userGplog"]["currentGP"], + ) return {"returnCode": 1, "apiName": "UpsertUserGplogApi"} @@ -197,39 +314,63 @@ class OngekiBase(): def handle_get_game_event_api_request(self, data: Dict) -> Dict: evts = self.data.static.get_enabled_events(self.version) - + + if evts is None: + return { + "type": data["type"], + "length": 0, + "gameEventList": [], + } + evt_list = [] for event in evts: - evt_list.append({ - "type": event["type"], - "id": event["eventId"], - "startDate": "2017-12-05 07:00:00.0", - "endDate": "2099-12-31 00:00:00.0" - }) + evt_list.append( + { + "type": event["type"], + "id": event["eventId"], + # actually use the startDate from the import so it + # properly shows all the events when new ones are imported + "startDate": datetime.strftime(event["startDate"], "%Y-%m-%d %H:%M:%S.0"), + #"endDate": "2099-12-31 00:00:00.0", + "endDate": datetime.strftime(event["endDate"], "%Y-%m-%d %H:%M:%S.0"), + } + ) - return {"type": data["type"], "length": len(evt_list), "gameEventList": evt_list} + return { + "type": data["type"], + "length": len(evt_list), + "gameEventList": evt_list, + } def handle_get_game_id_list_api_request(self, data: Dict) -> Dict: - game_idlist: list[str, Any] = [] #1 to 230 & 8000 to 8050 - + game_idlist: List[str, Any] = [] # 1 to 230 & 8000 to 8050 + if data["type"] == 1: - for i in range(1,231): + for i in range(1, 231): game_idlist.append({"type": 1, "id": i}) - return {"type": data["type"], "length": len(game_idlist), "gameIdlistList": game_idlist} + return { + "type": data["type"], + "length": len(game_idlist), + "gameIdlistList": game_idlist, + } elif data["type"] == 2: - for i in range(8000,8051): + for i in range(8000, 8051): game_idlist.append({"type": 2, "id": i}) - return {"type": data["type"], "length": len(game_idlist), "gameIdlistList": game_idlist} + return { + "type": data["type"], + "length": len(game_idlist), + "gameIdlistList": game_idlist, + } def handle_get_user_region_api_request(self, data: Dict) -> Dict: return {"userId": data["userId"], "length": 0, "userRegionList": []} def handle_get_user_preview_api_request(self, data: Dict) -> Dict: profile = self.data.profile.get_profile_preview(data["userId"], self.version) - - if profile is None: + + if profile is None: return { - "userId": data["userId"], + "userId": data["userId"], "isLogin": False, "lastLoginDate": "0000-00-00 00:00:00", "userName": "", @@ -240,12 +381,12 @@ class OngekiBase(): "lastGameId": "", "lastRomVersion": "", "lastDataVersion": "", - "lastPlayDate": "", + "lastPlayDate": "", "nameplateId": 0, - "trophyId": 0, - "cardId": 0, - "dispPlayerLv": 0, - "dispRating": 0, + "trophyId": 0, + "cardId": 0, + "dispPlayerLv": 0, + "dispRating": 0, "dispBP": 0, "headphone": 0, "banStatus": 0, @@ -253,7 +394,7 @@ class OngekiBase(): } return { - "userId": data["userId"], + "userId": data["userId"], "isLogin": False, "lastLoginDate": profile["lastPlayDate"], "userName": profile["userName"], @@ -264,12 +405,12 @@ class OngekiBase(): "lastGameId": profile["lastGameId"], "lastRomVersion": profile["lastRomVersion"], "lastDataVersion": profile["lastDataVersion"], - "lastPlayDate": profile["lastPlayDate"], + "lastPlayDate": profile["lastPlayDate"], "nameplateId": profile["nameplateId"], - "trophyId": profile["trophyId"], - "cardId": profile["cardId"], - "dispPlayerLv": profile["dispPlayerLv"], - "dispRating": profile["dispRating"], + "trophyId": profile["trophyId"], + "cardId": profile["cardId"], + "dispPlayerLv": profile["dispPlayerLv"], + "dispRating": profile["dispRating"], "dispBP": profile["dispBP"], "headphone": profile["headphone"], "banStatus": profile["banStatus"], @@ -296,14 +437,16 @@ class OngekiBase(): } def handle_get_user_tech_event_api_request(self, data: Dict) -> Dict: - user_tech_event_list = self.data.item.get_tech_event(data["userId"]) - if user_tech_event_list is None: return {} + user_tech_event_list = self.data.item.get_tech_event(self.version, data["userId"]) + if user_tech_event_list is None: + return {} tech_evt = [] for evt in user_tech_event_list: tmp = evt._asdict() tmp.pop("id") tmp.pop("user") + tmp.pop("version") tech_evt.append(tmp) return { @@ -313,15 +456,24 @@ class OngekiBase(): } def handle_get_user_tech_event_ranking_api_request(self, data: Dict) -> Dict: - #user_event_ranking_list = self.data.item.get_tech_event_ranking(data["userId"]) - #if user_event_ranking_list is None: return {} + user_tech_event_ranks = self.data.item.get_tech_event_ranking(self.version, data["userId"]) + if user_tech_event_ranks is None: + return { + "userId": data["userId"], + "length": 0, + "userTechEventRankingList": [], + } + # collect the whole table and clear other players, to preserve proper ranking evt_ranking = [] - #for evt in user_event_ranking_list: - # tmp = evt._asdict() - # tmp.pop("id") - # tmp.pop("user") - # evt_ranking.append(tmp) + for evt in user_tech_event_ranks: + tmp = evt._asdict() + if tmp["user"] != data["userId"]: + tmp.clear() + else: + tmp.pop("id") + tmp.pop("user") + evt_ranking.append(tmp) return { "userId": data["userId"], @@ -331,7 +483,8 @@ class OngekiBase(): def handle_get_user_kop_api_request(self, data: Dict) -> Dict: kop_list = self.data.profile.get_kop(data["userId"]) - if kop_list is None: return {} + if kop_list is None: + return {} for kop in kop_list: kop.pop("user") @@ -349,10 +502,10 @@ class OngekiBase(): next_idx = data["nextIndex"] start_idx = next_idx end_idx = max_ct + start_idx - + if len(song_list[start_idx:]) > max_ct: next_idx += max_ct - + else: next_idx = -1 @@ -360,17 +513,22 @@ class OngekiBase(): "userId": data["userId"], "length": len(song_list[start_idx:end_idx]), "nextIndex": next_idx, - "userMusicList": song_list[start_idx:end_idx] + "userMusicList": song_list[start_idx:end_idx], } def handle_get_user_item_api_request(self, data: Dict) -> Dict: kind = data["nextIndex"] / 10000000000 p = self.data.item.get_items(data["userId"], kind) - if p is None: - return {"userId": data["userId"], "nextIndex": -1, "itemKind": kind, "userItemList": []} + if p is None: + return { + "userId": data["userId"], + "nextIndex": -1, + "itemKind": kind, + "userItemList": [], + } - items: list[Dict[str, Any]] = [] + items: List[Dict[str, Any]] = [] for i in range(data["nextIndex"] % 10000000000, len(p)): if len(items) > data["maxCount"]: break @@ -381,14 +539,23 @@ class OngekiBase(): xout = kind * 10000000000 + (data["nextIndex"] % 10000000000) + len(items) - if len(items) < data["maxCount"] or data["maxCount"] == 0: nextIndex = 0 - else: nextIndex = xout + if len(items) < data["maxCount"] or data["maxCount"] == 0: + nextIndex = 0 + else: + nextIndex = xout - return {"userId": data["userId"], "nextIndex": int(nextIndex), "itemKind": int(kind), "length": len(items), "userItemList": items} + return { + "userId": data["userId"], + "nextIndex": int(nextIndex), + "itemKind": int(kind), + "length": len(items), + "userItemList": items, + } def handle_get_user_option_api_request(self, data: Dict) -> Dict: o = self.data.profile.get_profile_options(data["userId"]) - if o is None: return {} + if o is None: + return {} # get the dict representation of the row so we can modify values user_opts = o._asdict() @@ -401,14 +568,17 @@ class OngekiBase(): def handle_get_user_data_api_request(self, data: Dict) -> Dict: p = self.data.profile.get_profile_data(data["userId"], self.version) - if p is None: return {} + if p is None: + return {} cards = 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']}") + 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() @@ -422,28 +592,35 @@ class OngekiBase(): # add access code that we don't store user_data["accessCode"] = cards[0]["access_code"] - return {"userId": data["userId"], "userData":user_data} + return {"userId": data["userId"], "userData": user_data} def handle_get_user_event_ranking_api_request(self, data: Dict) -> Dict: - #user_event_ranking_list = self.data.item.get_event_ranking(data["userId"]) - #if user_event_ranking_list is None: return {} + user_event_ranking_list = self.data.item.get_ranking_event_ranks(self.version, data["userId"]) + if user_event_ranking_list is None: + return {} - evt_ranking = [] - #for evt in user_event_ranking_list: - # tmp = evt._asdict() - # tmp.pop("id") - # tmp.pop("user") - # evt_ranking.append(tmp) + # We collect the whole ranking table, and clear out any not needed data, this way we preserve the proper ranking + # In official spec this should be done server side, in maintenance period + prep_event_ranking = [] + for evt in user_event_ranking_list: + tmp = evt._asdict() + if tmp["user"] != data["userId"]: + tmp.clear() + else: + tmp.pop("id") + tmp.pop("user") + prep_event_ranking.append(tmp) return { "userId": data["userId"], - "length": len(evt_ranking), - "userEventRankingList": evt_ranking, + "length": len(prep_event_ranking), + "userEventRankingList": prep_event_ranking, } def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: user_login_bonus_list = self.data.item.get_login_bonuses(data["userId"]) - if user_login_bonus_list is None: return {} + if user_login_bonus_list is None: + return {} login_bonuses = [] for scenerio in user_login_bonus_list: @@ -451,16 +628,19 @@ class OngekiBase(): tmp.pop("id") tmp.pop("user") login_bonuses.append(tmp) - + return { - "userId": data["userId"], - "length": len(login_bonuses), - "userLoginBonusList": login_bonuses + "userId": data["userId"], + "length": len(login_bonuses), + "userLoginBonusList": login_bonuses, } def handle_get_user_bp_base_request(self, data: Dict) -> Dict: - p = self.data.profile.get_profile(self.game, self.version, user_id = data["userId"]) - if p is None: return {} + p = self.data.profile.get_profile( + self.game, self.version, user_id=data["userId"] + ) + if p is None: + return {} profile = json.loads(p["data"]) return { "userId": data["userId"], @@ -470,7 +650,12 @@ class OngekiBase(): def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict: recent_rating = self.data.profile.get_profile_recent_rating(data["userId"]) - if recent_rating is None: return {} + if recent_rating is None: + return { + "userId": data["userId"], + "length": 0, + "userRecentRatingList": [], + } userRecentRatingList = recent_rating["recentRating"] @@ -482,31 +667,35 @@ class OngekiBase(): def handle_get_user_activity_api_request(self, data: Dict) -> Dict: activity = self.data.profile.get_profile_activity(data["userId"], data["kind"]) - if activity is None: return {} - + if activity is None: + return {} + user_activity = [] - + for act in activity: - user_activity.append({ - "kind": act["kind"], - "id": act["activityId"], - "sortNumber": act["sortNumber"], - "param1": act["param1"], - "param2": act["param2"], - "param3": act["param3"], - "param4": act["param4"], - }) + user_activity.append( + { + "kind": act["kind"], + "id": act["activityId"], + "sortNumber": act["sortNumber"], + "param1": act["param1"], + "param2": act["param2"], + "param3": act["param3"], + "param4": act["param4"], + } + ) return { - "userId": data["userId"], + "userId": data["userId"], "length": len(user_activity), "kind": data["kind"], - "userActivityList": user_activity + "userActivityList": user_activity, } def handle_get_user_story_api_request(self, data: Dict) -> Dict: user_stories = self.data.item.get_stories(data["userId"]) - if user_stories is None: return {} + if user_stories is None: + return {} story_list = [] for story in user_stories: @@ -516,14 +705,15 @@ class OngekiBase(): story_list.append(tmp) return { - "userId": data["userId"], - "length": len(story_list), - "userStoryList": story_list + "userId": data["userId"], + "length": len(story_list), + "userStoryList": story_list, } def handle_get_user_chapter_api_request(self, data: Dict) -> Dict: user_chapters = self.data.item.get_chapters(data["userId"]) - if user_chapters is None: return {} + if user_chapters is None: + return {} chapter_list = [] for chapter in user_chapters: @@ -531,11 +721,11 @@ class OngekiBase(): tmp.pop("id") tmp.pop("user") chapter_list.append(tmp) - + return { - "userId": data["userId"], - "length": len(chapter_list), - "userChapterList": chapter_list + "userId": data["userId"], + "length": len(chapter_list), + "userChapterList": chapter_list, } def handle_get_user_training_room_by_key_api_request(self, data: Dict) -> Dict: @@ -547,7 +737,8 @@ class OngekiBase(): def handle_get_user_character_api_request(self, data: Dict) -> Dict: user_characters = self.data.item.get_characters(data["userId"]) - if user_characters is None: return {} + if user_characters is None: + return {} character_list = [] for character in user_characters: @@ -555,16 +746,17 @@ class OngekiBase(): tmp.pop("id") tmp.pop("user") character_list.append(tmp) - + return { - "userId": data["userId"], - "length": len(character_list), - "userCharacterList": character_list + "userId": data["userId"], + "length": len(character_list), + "userCharacterList": character_list, } def handle_get_user_card_api_request(self, data: Dict) -> Dict: user_cards = self.data.item.get_cards(data["userId"]) - if user_cards is None: return {} + if user_cards is None: + return {} card_list = [] for card in user_cards: @@ -572,17 +764,18 @@ class OngekiBase(): tmp.pop("id") tmp.pop("user") card_list.append(tmp) - + return { - "userId": data["userId"], - "length": len(card_list), - "userCardList": card_list + "userId": data["userId"], + "length": len(card_list), + "userCardList": card_list, } def handle_get_user_deck_by_key_api_request(self, data: Dict) -> Dict: # Auth key doesn't matter, it just wants all the decks decks = self.data.item.get_decks(data["userId"]) - if decks is None: return {} + if decks is None: + return {} deck_list = [] for deck in decks: @@ -599,7 +792,8 @@ class OngekiBase(): def handle_get_user_trade_item_api_request(self, data: Dict) -> Dict: user_trade_items = self.data.item.get_trade_items(data["userId"]) - if user_trade_items is None: return {} + if user_trade_items is None: + return {} trade_item_list = [] for trade_item in user_trade_items: @@ -616,7 +810,8 @@ class OngekiBase(): def handle_get_user_scenario_api_request(self, data: Dict) -> Dict: user_scenerio = self.data.item.get_scenerios(data["userId"]) - if user_scenerio is None: return {} + if user_scenerio is None: + return {} scenerio_list = [] for scenerio in user_scenerio: @@ -624,7 +819,7 @@ class OngekiBase(): tmp.pop("id") tmp.pop("user") scenerio_list.append(tmp) - + return { "userId": data["userId"], "length": len(scenerio_list), @@ -633,7 +828,8 @@ class OngekiBase(): def handle_get_user_ratinglog_api_request(self, data: Dict) -> Dict: rating_log = self.data.profile.get_profile_rating_log(data["userId"]) - if rating_log is None: return {} + if rating_log is None: + return {} userRatinglogList = [] for rating in rating_log: @@ -649,16 +845,19 @@ class OngekiBase(): } def handle_get_user_mission_point_api_request(self, data: Dict) -> Dict: - user_mission_point_list = self.data.item.get_mission_points(data["userId"]) - if user_mission_point_list is None: return {} + user_mission_point_list = self.data.item.get_mission_points(self.version, data["userId"]) + if user_mission_point_list is None: + return {} mission_point_list = [] for evt_music in user_mission_point_list: tmp = evt_music._asdict() tmp.pop("id") tmp.pop("user") + tmp.pop("version") mission_point_list.append(tmp) + return { "userId": data["userId"], "length": len(mission_point_list), @@ -667,13 +866,18 @@ class OngekiBase(): def handle_get_user_event_point_api_request(self, data: Dict) -> Dict: user_event_point_list = self.data.item.get_event_points(data["userId"]) - if user_event_point_list is None: return {} + if user_event_point_list is None: + return {} event_point_list = [] for evt_music in user_event_point_list: tmp = evt_music._asdict() tmp.pop("id") tmp.pop("user") + # pop other stuff event_point doesn't want + tmp.pop("rank") + tmp.pop("type") + tmp.pop("date") event_point_list.append(tmp) return { @@ -684,7 +888,8 @@ class OngekiBase(): def handle_get_user_music_item_api_request(self, data: Dict) -> Dict: user_music_item_list = self.data.item.get_music_items(data["userId"]) - if user_music_item_list is None: return {} + if user_music_item_list is None: + return {} music_item_list = [] for evt_music in user_music_item_list: @@ -701,7 +906,8 @@ class OngekiBase(): def handle_get_user_event_music_api_request(self, data: Dict) -> Dict: user_evt_music_list = self.data.item.get_event_music(data["userId"]) - if user_evt_music_list is None: return {} + if user_evt_music_list is None: + return {} evt_music_list = [] for evt_music in user_evt_music_list: @@ -718,7 +924,8 @@ class OngekiBase(): def handle_get_user_boss_api_request(self, data: Dict) -> Dict: p = self.data.item.get_bosses(data["userId"]) - if p is None: return {} + if p is None: + return {} boss_list = [] for boss in p: @@ -740,7 +947,9 @@ class OngekiBase(): # The isNew fields are new as of Red and up. We just won't use them for now. if "userData" in upsert and len(upsert["userData"]) > 0: - self.data.profile.put_profile_data(user_id, self.version, upsert["userData"][0]) + self.data.profile.put_profile_data( + user_id, self.version, upsert["userData"][0] + ) if "userOption" in upsert and len(upsert["userOption"]) > 0: self.data.profile.put_profile_options(user_id, upsert["userOption"][0]) @@ -751,27 +960,37 @@ class OngekiBase(): if "userActivityList" in upsert: for act in upsert["userActivityList"]: - self.data.profile.put_profile_activity(user_id, act["kind"], act["id"], act["sortNumber"], act["param1"], - act["param2"], act["param3"], act["param4"]) - + self.data.profile.put_profile_activity( + user_id, + act["kind"], + act["id"], + act["sortNumber"], + act["param1"], + act["param2"], + act["param3"], + act["param4"], + ) + if "userRecentRatingList" in upsert: - self.data.profile.put_profile_recent_rating(user_id, upsert["userRecentRatingList"]) - + self.data.profile.put_profile_recent_rating( + user_id, upsert["userRecentRatingList"] + ) + if "userBpBaseList" in upsert: self.data.profile.put_profile_bp_list(user_id, upsert["userBpBaseList"]) - + if "userMusicDetailList" in upsert: for x in upsert["userMusicDetailList"]: self.data.score.put_best_score(user_id, x) - + if "userCharacterList" in upsert: for x in upsert["userCharacterList"]: self.data.item.put_character(user_id, x) - + if "userCardList" in upsert: for x in upsert["userCardList"]: self.data.item.put_card(user_id, x) - + if "userDeckList" in upsert: for x in upsert["userDeckList"]: self.data.item.put_deck(user_id, x) @@ -779,39 +998,45 @@ class OngekiBase(): if "userTrainingRoomList" in upsert: for x in upsert["userTrainingRoomList"]: self.data.profile.put_training_room(user_id, x) - + if "userStoryList" in upsert: for x in upsert["userStoryList"]: self.data.item.put_story(user_id, x) - + if "userChapterList" in upsert: for x in upsert["userChapterList"]: self.data.item.put_chapter(user_id, x) - + + if "userMemoryChapterList" in upsert: + for x in upsert["userMemoryChapterList"]: + self.data.item.put_memorychapter(user_id, x) + if "userItemList" in upsert: for x in upsert["userItemList"]: self.data.item.put_item(user_id, x) - + if "userMusicItemList" in upsert: for x in upsert["userMusicItemList"]: self.data.item.put_music_item(user_id, x) - + if "userLoginBonusList" in upsert: for x in upsert["userLoginBonusList"]: self.data.item.put_login_bonus(user_id, x) - + if "userEventPointList" in upsert: for x in upsert["userEventPointList"]: - self.data.item.put_event_point(user_id, x) - + self.data.item.put_event_point(user_id, self.version, x) + if "userMissionPointList" in upsert: for x in upsert["userMissionPointList"]: - self.data.item.put_mission_point(user_id, x) - + self.data.item.put_mission_point(user_id, self.version, x) + if "userRatinglogList" in upsert: for x in upsert["userRatinglogList"]: - self.data.profile.put_profile_rating_log(user_id, x["dataVersion"], x["highestRating"]) - + self.data.profile.put_profile_rating_log( + user_id, x["dataVersion"], x["highestRating"] + ) + if "userBossList" in upsert: for x in upsert["userBossList"]: self.data.item.put_boss(user_id, x) @@ -834,48 +1059,53 @@ class OngekiBase(): if "userTechEventList" in upsert: for x in upsert["userTechEventList"]: - self.data.item.put_tech_event(user_id, x) + self.data.item.put_tech_event(user_id, self.version, x) + + # This should be updated once a day in maintenance window, but for time being we will push the update on each upsert + self.data.item.put_tech_event_ranking(user_id, self.version, x) if "userKopList" in upsert: for x in upsert["userKopList"]: self.data.profile.put_kop(user_id, x) - return {'returnCode': 1, 'apiName': 'upsertUserAll'} + return {"returnCode": 1, "apiName": "upsertUserAll"} def handle_get_user_rival_api_request(self, data: Dict) -> Dict: """ Added in Bright """ - rival_list = self.data.profile.get_rivals(data["userId"]) - if rival_list is None or len(rival_list) < 1: + + rival_list = [] + user_rivals = self.data.profile.get_rivals(data["userId"]) + for rival in user_rivals: + tmp = {} + tmp["rivalUserId"] = rival[0] + rival_list.append(tmp) + + if user_rivals is None or len(rival_list) < 1: return { "userId": data["userId"], "length": 0, "userRivalList": [], } - return { "userId": data["userId"], "length": len(rival_list), - "userRivalList": rival_list._asdict(), + "userRivalList": rival_list, } - def handle_get_user_rival_data_api_reqiest(self, data:Dict) -> Dict: + def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict: """ Added in Bright """ rivals = [] - for rival in data["userRivalList"]: - name = self.data.profile.get_profile_name(rival["rivalUserId"], self.version) + name = self.data.profile.get_profile_name( + rival["rivalUserId"], self.version + ) if name is None: continue - - rivals.append({ - "rivalUserId": rival["rival"], - "rivalUserName": name - }) - + rivals.append({"rivalUserId": rival["rivalUserId"], "rivalUserName": name}) return { "userId": data["userId"], "length": len(rivals), @@ -889,16 +1119,13 @@ class OngekiBase(): rival_id = data["rivalUserId"] next_idx = data["nextIndex"] max_ct = data["maxCount"] - music = self.handle_get_user_music_api_request({ - "userId": rival_id, - "nextIndex": next_idx, - "maxCount": max_ct - }) + music = self.handle_get_user_music_api_request( + {"userId": rival_id, "nextIndex": next_idx, "maxCount": max_ct} + ) for song in music["userMusicList"]: song["userRivalMusicDetailList"] = song["userMusicDetailList"] song.pop("userMusicDetailList") - return { "userId": data["userId"], "rivalUserId": rival_id, @@ -917,18 +1144,15 @@ class OngekiBase(): tmp = md._asdict() tmp.pop("user") tmp.pop("id") - + for song in song_list: if song["userMusicDetailList"][0]["musicId"] == tmp["musicId"]: found = True song["userMusicDetailList"].append(tmp) song["length"] = len(song["userMusicDetailList"]) break - + if not found: - song_list.append({ - "length": 1, - "userMusicDetailList": [tmp] - }) - + song_list.append({"length": 1, "userMusicDetailList": [tmp]}) + return song_list diff --git a/titles/ongeki/bright.py b/titles/ongeki/bright.py index d92ed48..49d6216 100644 --- a/titles/ongeki/bright.py +++ b/titles/ongeki/bright.py @@ -1,5 +1,6 @@ from datetime import date, datetime, timedelta from typing import Any, Dict +from random import randint import pytz import json @@ -8,8 +9,8 @@ from titles.ongeki.base import OngekiBase from titles.ongeki.const import OngekiConstants from titles.ongeki.config import OngekiConfig -class OngekiBright(OngekiBase): +class OngekiBright(OngekiBase): def __init__(self, core_cfg: CoreConfig, game_cfg: OngekiConfig) -> None: super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_BRIGHT @@ -19,3 +20,612 @@ class OngekiBright(OngekiBase): ret["gameSetting"]["dataVersion"] = "1.30.00" ret["gameSetting"]["onlineDataVersion"] = "1.30.00" return ret + + def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: + # check for a bright profile + p = self.data.profile.get_profile_data(data["userId"], self.version) + if p is None: + return {} + + cards = 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") + + # add access code that we don't store + user_data["accessCode"] = cards[0]["access_code"] + + # add the compatible card maker version from config + card_maker_ver = self.game_cfg.version.version(self.version) + if card_maker_ver and card_maker_ver.get("card_maker"): + # Card Maker 1.30 = 1.30.01+ + # Card Maker 1.35 = 1.35.03+ + user_data["compatibleCmVersion"] = card_maker_ver.get("card_maker") + + return {"userId": data["userId"], "userData": user_data} + + def handle_printer_login_api_request(self, data: Dict): + return {"returnCode": 1} + + def handle_printer_logout_api_request(self, data: Dict): + return {"returnCode": 1} + + def handle_cm_get_user_card_api_request(self, data: Dict) -> Dict: + user_cards = self.data.item.get_cards(data["userId"]) + if user_cards is None: + return {} + + 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 = -1 + + card_list = [] + for card in user_cards: + tmp = card._asdict() + tmp.pop("id") + tmp.pop("user") + card_list.append(tmp) + + return { + "userId": data["userId"], + "length": len(card_list[start_idx:end_idx]), + "nextIndex": next_idx, + "userCardList": card_list[start_idx:end_idx], + } + + def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: + user_characters = self.data.item.get_characters(data["userId"]) + if user_characters is None: + return { + "userId": data["userId"], + "length": 0, + "nextIndex": 0, + "userCharacterList": [], + } + + max_ct = data["maxCount"] + next_idx = data["nextIndex"] + start_idx = next_idx + end_idx = max_ct + start_idx + + if len(user_characters[start_idx:]) > max_ct: + next_idx += max_ct + else: + next_idx = -1 + + character_list = [] + for character in user_characters: + tmp = character._asdict() + tmp.pop("id") + tmp.pop("user") + character_list.append(tmp) + + return { + "userId": data["userId"], + "length": len(character_list[start_idx:end_idx]), + "nextIndex": next_idx, + "userCharacterList": character_list[start_idx:end_idx], + } + + def handle_get_user_gacha_api_request(self, data: Dict) -> Dict: + user_gachas = self.data.item.get_user_gachas(data["userId"]) + if user_gachas is None: + return {"userId": data["userId"], "length": 0, "userGachaList": []} + + user_gacha_list = [] + for gacha in user_gachas: + tmp = gacha._asdict() + tmp.pop("id") + tmp.pop("user") + tmp["dailyGachaDate"] = datetime.strftime(tmp["dailyGachaDate"], "%Y-%m-%d") + user_gacha_list.append(tmp) + + return { + "userId": data["userId"], + "length": len(user_gacha_list), + "userGachaList": user_gacha_list, + } + + def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: + return self.handle_get_user_item_api_request(data) + + def handle_cm_get_user_gacha_supply_api_request(self, data: Dict) -> Dict: + # not used for now? not sure what it even does + user_gacha_supplies = self.data.item.get_user_gacha_supplies(data["userId"]) + if user_gacha_supplies is None: + return {"supplyId": 1, "length": 0, "supplyCardList": []} + + supply_list = [gacha["cardId"] for gacha in user_gacha_supplies] + + return { + "supplyId": 1, + "length": len(supply_list), + "supplyCardList": supply_list, + } + + def handle_get_game_gacha_api_request(self, data: Dict) -> Dict: + """ + returns all current active banners (gachas) + "Select Gacha" requires maxSelectPoint set and isCeiling set to 1 + """ + game_gachas = [] + # for every gacha_id in the OngekiConfig, grab the banner from the db + for gacha_id in self.game_cfg.gachas.enabled_gachas: + game_gacha = self.data.static.get_gacha(self.version, gacha_id) + if game_gacha: + game_gachas.append(game_gacha) + + # clean the database rows + game_gacha_list = [] + for gacha in game_gachas: + tmp = gacha._asdict() + tmp.pop("id") + tmp.pop("version") + tmp["startDate"] = datetime.strftime(tmp["startDate"], "%Y-%m-%d %H:%M:%S") + tmp["endDate"] = datetime.strftime(tmp["endDate"], "%Y-%m-%d %H:%M:%S") + tmp["noticeStartDate"] = datetime.strftime( + tmp["noticeStartDate"], "%Y-%m-%d %H:%M:%S" + ) + tmp["noticeEndDate"] = datetime.strftime( + tmp["noticeEndDate"], "%Y-%m-%d %H:%M:%S" + ) + tmp["convertEndDate"] = datetime.strftime( + tmp["convertEndDate"], "%Y-%m-%d %H:%M:%S" + ) + + # make sure to only show gachas for the current version + # so only up to bright, 1140 is the first bright memory gacha + if self.version == OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY: + game_gacha_list.append(tmp) + elif ( + self.version == OngekiConstants.VER_ONGEKI_BRIGHT + and tmp["gachaId"] < 1140 + ): + game_gacha_list.append(tmp) + + return { + "length": len(game_gacha_list), + "gameGachaList": game_gacha_list, + # no clue + "registIdList": [], + } + + def handle_roll_gacha_api_request(self, data: Dict) -> Dict: + """ + Handle a gacha roll API request + """ + gacha_id = data["gachaId"] + num_rolls = data["times"] + # change_rate is the 5 gacha rool SR gurantee once a week + change_rate = data["changeRate"] + # SSR book which guarantees a SSR card, itemKind=15, itemId=1 + book_used = data["bookUseCount"] + if num_rolls not in {1, 5, 11}: + return {} + + # https://gamerch.com/ongeki/entry/462978 + + # 77% chance of gett ing a R card + # 20% chance of getting a SR card + # 3% chance of getting a SSR card + rarity = [1 for _ in range(77)] + rarity += [2 for _ in range(20)] + rarity += [3 for _ in range(3)] + + # gachaId 1011 is "無料ガチャ" (free gacha), which requires GatchaTickets + # itemKind=11, itemId=1 and appearenty sucks + # 94% chance of getting a R card + # 5% chance of getting a SR card + # 1% chance of getting a SSR card + if gacha_id == 1011: + rarity = [1 for _ in range(94)] + rarity += [2 for _ in range(5)] + rarity += [3 for _ in range(1)] + + # gachaId 1012 is "無料ガチャ(SR確定)" (SR confirmed! free gacha), which + # requires GatchaTickets itemKind=11, itemId=4 and always guarantees + # a SR card or higher + # 92% chance of getting a SR card + # 8% chance of getting a SSR card + elif gacha_id == 1012: + rarity = [2 for _ in range(92)] + rarity += [3 for _ in range(8)] + + assert len(rarity) == 100 + + # uniform distribution to get the rarity of the card + rolls = [rarity[randint(0, len(rarity) - 1)] for _ in range(num_rolls)] + + # if SSR book used, make sure you always get one SSR + if book_used == 1: + if rolls.count(3) == 0: + # if there is no SSR, re-roll + return self.handle_roll_gacha_api_request(data) + # make sure that 11 rolls always have at least 1 SR or SSR + elif (num_rolls == 5 and change_rate is True) or num_rolls == 11: + if rolls.count(2) == 0 and rolls.count(3) == 0: + # if there is no SR or SSR, re-roll + return self.handle_roll_gacha_api_request(data) + + # get a list of cards for each rarity + cards_r = self.data.static.get_cards_by_rarity(self.version, 1) + cards_sr, cards_ssr = [], [] + + # free gachas are only allowed to get their specific cards! (R irrelevant) + if gacha_id in {1011, 1012}: + gacha_cards = self.data.static.get_gacha_cards(gacha_id) + for card in gacha_cards: + if card["rarity"] == 3: + cards_sr.append({"cardId": card["cardId"], "rarity": 2}) + elif card["rarity"] == 4: + cards_ssr.append({"cardId": card["cardId"], "rarity": 3}) + else: + cards_sr = self.data.static.get_cards_by_rarity(self.version, 2) + cards_ssr = self.data.static.get_cards_by_rarity(self.version, 3) + + # get the promoted cards for that gacha and add them multiple + # times to increase chances by factor chances + chances = 10 + + gacha_cards = self.data.static.get_gacha_cards(gacha_id) + for card in gacha_cards: + # make sure to add the cards to the corresponding rarity + if card["rarity"] == 2: + cards_r += [{"cardId": card["cardId"], "rarity": 1}] * chances + if card["rarity"] == 3: + cards_sr += [{"cardId": card["cardId"], "rarity": 2}] * chances + elif card["rarity"] == 4: + cards_ssr += [{"cardId": card["cardId"], "rarity": 3}] * chances + + # get the card id for each roll + rolled_cards = [] + for i in range(len(rolls)): + if rolls[i] == 1: + rolled_cards.append(cards_r[randint(0, len(cards_r) - 1)]) + elif rolls[i] == 2: + rolled_cards.append(cards_sr[randint(0, len(cards_sr) - 1)]) + elif rolls[i] == 3: + rolled_cards.append(cards_ssr[randint(0, len(cards_ssr) - 1)]) + + game_gacha_card_list = [] + for card in rolled_cards: + game_gacha_card_list.append( + { + "gachaId": data["gachaId"], + "cardId": card["cardId"], + # +1 because Card Maker is weird + "rarity": card["rarity"] + 1, + "weight": 1, + "isPickup": False, + "isSelect": False, + } + ) + + return { + "length": len(game_gacha_card_list), + "gameGachaCardList": game_gacha_card_list, + } + + def handle_cm_upsert_user_gacha_api_request(self, data: Dict): + upsert = data["cmUpsertUserGacha"] + user_id = data["userId"] + + gacha_id = data["gachaId"] + gacha_count = data["gachaCnt"] + play_date = datetime.strptime(data["playDate"][:10], "%Y-%m-%d") + select_point = data["selectPoint"] + + total_gacha_count, ceiling_gacha_count = 0, 0 + # 0 = can still use Gacha Select, 1 = already used Gacha Select + use_select_point = 0 + daily_gacha_cnt, five_gacha_cnt, eleven_gacha_cnt = 0, 0, 0 + daily_gacha_date = datetime.strptime("2000-01-01", "%Y-%m-%d") + + # check if the user previously rolled the exact same gacha + user_gacha = self.data.item.get_user_gacha(user_id, gacha_id) + if user_gacha: + total_gacha_count = user_gacha["totalGachaCnt"] + ceiling_gacha_count = user_gacha["ceilingGachaCnt"] + daily_gacha_cnt = user_gacha["dailyGachaCnt"] + five_gacha_cnt = user_gacha["fiveGachaCnt"] + eleven_gacha_cnt = user_gacha["elevenGachaCnt"] + # if the Gacha Select has been used, make sure to keep it + if user_gacha["useSelectPoint"] == 1: + use_select_point = 1 + # parse just the year, month and date + daily_gacha_date = user_gacha["dailyGachaDate"] + + # if the saved dailyGachaDate is different from the roll, + # reset dailyGachaCnt and change the date + if daily_gacha_date != play_date: + daily_gacha_date = play_date + daily_gacha_cnt = 0 + + self.data.item.put_user_gacha( + user_id, + gacha_id, + totalGachaCnt=total_gacha_count + gacha_count, + ceilingGachaCnt=ceiling_gacha_count + gacha_count, + selectPoint=select_point, + useSelectPoint=use_select_point, + dailyGachaCnt=daily_gacha_cnt + gacha_count, + fiveGachaCnt=five_gacha_cnt + 1 if gacha_count == 5 else five_gacha_cnt, + elevenGachaCnt=eleven_gacha_cnt + 1 + if gacha_count == 11 + else eleven_gacha_cnt, + dailyGachaDate=daily_gacha_date, + ) + + if "userData" in upsert and len(upsert["userData"]) > 0: + # check if the profile is a bright memory profile + p = self.data.profile.get_profile_data(data["userId"], self.version) + if p is not None: + # save the bright memory profile + self.data.profile.put_profile_data( + user_id, self.version, upsert["userData"][0] + ) + else: + # save the bright profile + self.data.profile.put_profile_data( + user_id, self.version, upsert["userData"][0] + ) + + if "userCharacterList" in upsert: + for x in upsert["userCharacterList"]: + self.data.item.put_character(user_id, x) + + if "userItemList" in upsert: + for x in upsert["userItemList"]: + self.data.item.put_item(user_id, x) + + if "userCardList" in upsert: + for x in upsert["userCardList"]: + self.data.item.put_card(user_id, x) + + # TODO? + # if "gameGachaCardList" in upsert: + # for x in upsert["gameGachaCardList"]: + + return {"returnCode": 1, "apiName": "CMUpsertUserGachaApi"} + + def handle_cm_upsert_user_select_gacha_api_request(self, data: Dict) -> Dict: + upsert = data["cmUpsertUserSelectGacha"] + user_id = data["userId"] + + if "userData" in upsert and len(upsert["userData"]) > 0: + # check if the profile is a bright memory profile + p = self.data.profile.get_profile_data(data["userId"], self.version) + if p is not None: + # save the bright memory profile + self.data.profile.put_profile_data( + user_id, self.version, upsert["userData"][0] + ) + else: + # save the bright profile + self.data.profile.put_profile_data( + user_id, self.version, upsert["userData"][0] + ) + + if "userCharacterList" in upsert: + for x in upsert["userCharacterList"]: + self.data.item.put_character(user_id, x) + + if "userCardList" in upsert: + for x in upsert["userCardList"]: + self.data.item.put_card(user_id, x) + + if "selectGachaLogList" in data: + for x in data["selectGachaLogList"]: + self.data.item.put_user_gacha( + user_id, + x["gachaId"], + selectPoint=0, + useSelectPoint=x["useSelectPoint"], + ) + + return {"returnCode": 1, "apiName": "cmUpsertUserSelectGacha"} + + def handle_get_game_gacha_card_by_id_api_request(self, data: Dict) -> Dict: + game_gacha_cards = self.data.static.get_gacha_cards(data["gachaId"]) + if game_gacha_cards == []: + # fallback to be at least able to select that gacha + return { + "gachaId": data["gachaId"], + "length": 6, + "isPickup": False, + "gameGachaCardList": [ + { + "gachaId": data["gachaId"], + "cardId": 100984, + "rarity": 4, + "weight": 1, + "isPickup": False, + "isSelect": True, + }, + { + "gachaId": data["gachaId"], + "cardId": 100997, + "rarity": 3, + "weight": 2, + "isPickup": False, + "isSelect": True, + }, + { + "gachaId": data["gachaId"], + "cardId": 100998, + "rarity": 3, + "weight": 2, + "isPickup": False, + "isSelect": True, + }, + { + "gachaId": data["gachaId"], + "cardId": 101020, + "rarity": 2, + "weight": 3, + "isPickup": False, + "isSelect": True, + }, + { + "gachaId": data["gachaId"], + "cardId": 101021, + "rarity": 2, + "weight": 3, + "isPickup": False, + "isSelect": True, + }, + { + "gachaId": data["gachaId"], + "cardId": 101022, + "rarity": 2, + "weight": 3, + "isPickup": False, + "isSelect": True, + }, + ], + "emissionList": [], + "afterCalcList": [], + "ssrBookCalcList": [], + } + + game_gacha_card_list = [] + for gacha_card in game_gacha_cards: + tmp = gacha_card._asdict() + tmp.pop("id") + game_gacha_card_list.append(tmp) + + return { + "gachaId": data["gachaId"], + "length": len(game_gacha_card_list), + "isPickup": False, + "gameGachaCardList": game_gacha_card_list, + # again no clue + "emissionList": [], + "afterCalcList": [], + "ssrBookCalcList": [], + } + + def handle_get_game_theater_api_request(self, data: Dict) -> Dict: + """ + shows a banner after every print, not sure what its used for + """ + + """ + return { + "length": 1, + "gameTheaterList": [{ + "theaterId": 1, + "theaterName": "theaterName", + "startDate": "2018-01-01 00:00:00.0", + "endDate": "2038-01-01 00:00:00.0", + "gameSubTheaterList": [{ + "theaterId": 1, + "id": 2, + "no": 4 + }] + } + ], + "registIdList": [] + } + """ + + return {"length": 0, "gameTheaterList": [], "registIdList": []} + + def handle_cm_upsert_user_print_playlog_api_request(self, data: Dict) -> Dict: + return { + "returnCode": 1, + "orderId": 0, + "serialId": "11111111111111111111", + "apiName": "CMUpsertUserPrintPlaylogApi", + } + + def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: + return { + "returnCode": 1, + "orderId": 0, + "serialId": "11111111111111111111", + "apiName": "CMUpsertUserPrintlogApi", + } + + def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: + user_print_detail = data["userPrintDetail"] + + # generate random serial id + serial_id = "".join([str(randint(0, 9)) for _ in range(20)]) + + # not needed because are either zero or unset + user_print_detail.pop("orderId") + user_print_detail.pop("printNumber") + user_print_detail.pop("serialId") + user_print_detail["printDate"] = datetime.strptime( + user_print_detail["printDate"], "%Y-%m-%d" + ) + + # add the entry to the user print table with the random serialId + self.data.item.put_user_print_detail( + data["userId"], serial_id, user_print_detail + ) + + return { + "returnCode": 1, + "serialId": serial_id, + "apiName": "CMUpsertUserPrintApi", + } + + def handle_cm_upsert_user_all_api_request(self, data: Dict) -> Dict: + upsert = data["cmUpsertUserAll"] + user_id = data["userId"] + + if "userData" in upsert and len(upsert["userData"]) > 0: + # check if the profile is a bright memory profile + p = self.data.profile.get_profile_data(data["userId"], self.version) + if p is not None: + # save the bright memory profile + self.data.profile.put_profile_data( + user_id, self.version, upsert["userData"][0] + ) + else: + # save the bright profile + self.data.profile.put_profile_data( + user_id, self.version, upsert["userData"][0] + ) + + if "userActivityList" in upsert: + for act in upsert["userActivityList"]: + self.data.profile.put_profile_activity( + user_id, + act["kind"], + act["id"], + act["sortNumber"], + act["param1"], + act["param2"], + act["param3"], + act["param4"], + ) + + if "userItemList" in upsert: + for x in upsert["userItemList"]: + self.data.item.put_item(user_id, x) + + if "userCardList" in upsert: + for x in upsert["userCardList"]: + self.data.item.put_card(user_id, x) + + return {"returnCode": 1, "apiName": "cmUpsertUserAll"} diff --git a/titles/ongeki/brightmemory.py b/titles/ongeki/brightmemory.py new file mode 100644 index 0000000..d7103a3 --- /dev/null +++ b/titles/ongeki/brightmemory.py @@ -0,0 +1,138 @@ +from datetime import date, datetime, timedelta +from typing import Any, Dict +import pytz +import json + +from core.config import CoreConfig +from titles.ongeki.base import OngekiBase +from titles.ongeki.bright import OngekiBright +from titles.ongeki.const import OngekiConstants +from titles.ongeki.config import OngekiConfig + + +class OngekiBrightMemory(OngekiBright): + def __init__(self, core_cfg: CoreConfig, game_cfg: OngekiConfig) -> None: + super().__init__(core_cfg, game_cfg) + self.version = OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY + + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = super().handle_get_game_setting_api_request(data) + ret["gameSetting"]["dataVersion"] = "1.35.00" + ret["gameSetting"]["onlineDataVersion"] = "1.35.00" + ret["gameSetting"]["maxCountCharacter"] = 50 + ret["gameSetting"]["maxCountCard"] = 300 + ret["gameSetting"]["maxCountItem"] = 300 + ret["gameSetting"]["maxCountMusic"] = 50 + ret["gameSetting"]["maxCountMusicItem"] = 300 + ret["gameSetting"]["maxCountRivalMusic"] = 300 + return ret + + def handle_get_user_memory_chapter_api_request(self, data: Dict) -> Dict: + memories = self.data.item.get_memorychapters(data["userId"]) + if not memories: + return { + "userId": data["userId"], + "length": 6, + "userMemoryChapterList": [ + { + "gaugeId": 0, + "isClear": False, + "gaugeNum": 0, + "chapterId": 70001, + "jewelCount": 0, + "isBossWatched": False, + "isStoryWatched": False, + "isDialogWatched": False, + "isEndingWatched": False, + "lastPlayMusicId": 0, + "lastPlayMusicLevel": 0, + "lastPlayMusicCategory": 0, + }, + { + "gaugeId": 0, + "isClear": False, + "gaugeNum": 0, + "chapterId": 70002, + "jewelCount": 0, + "isBossWatched": False, + "isStoryWatched": False, + "isDialogWatched": False, + "isEndingWatched": False, + "lastPlayMusicId": 0, + "lastPlayMusicLevel": 0, + "lastPlayMusicCategory": 0, + }, + { + "gaugeId": 0, + "isClear": False, + "gaugeNum": 0, + "chapterId": 70003, + "jewelCount": 0, + "isBossWatched": False, + "isStoryWatched": False, + "isDialogWatched": False, + "isEndingWatched": False, + "lastPlayMusicId": 0, + "lastPlayMusicLevel": 0, + "lastPlayMusicCategory": 0, + }, + { + "gaugeId": 0, + "isClear": False, + "gaugeNum": 0, + "chapterId": 70004, + "jewelCount": 0, + "isBossWatched": False, + "isStoryWatched": False, + "isDialogWatched": False, + "isEndingWatched": False, + "lastPlayMusicId": 0, + "lastPlayMusicLevel": 0, + "lastPlayMusicCategory": 0, + }, + { + "gaugeId": 0, + "isClear": False, + "gaugeNum": 0, + "chapterId": 70005, + "jewelCount": 0, + "isBossWatched": False, + "isStoryWatched": False, + "isDialogWatched": False, + "isEndingWatched": False, + "lastPlayMusicId": 0, + "lastPlayMusicLevel": 0, + "lastPlayMusicCategory": 0, + }, + { + "gaugeId": 0, + "isClear": False, + "gaugeNum": 0, + "chapterId": 70099, + "jewelCount": 0, + "isBossWatched": False, + "isStoryWatched": False, + "isDialogWatched": False, + "isEndingWatched": False, + "lastPlayMusicId": 0, + "lastPlayMusicLevel": 0, + "lastPlayMusicCategory": 0, + }, + ], + } + + memory_chp = [] + for chp in memories: + tmp = chp._asdict() + tmp.pop("id") + tmp.pop("user") + memory_chp.append(tmp) + + return { + "userId": data["userId"], + "length": len(memory_chp), + "userMemoryChapterList": memory_chp, + } + + def handle_get_game_music_release_state_api_request(self, data: Dict) -> Dict: + return {"techScore": 0, "cardNum": 0} diff --git a/titles/ongeki/config.py b/titles/ongeki/config.py index 722677c..b952b2d 100644 --- a/titles/ongeki/config.py +++ b/titles/ongeki/config.py @@ -1,17 +1,83 @@ +from ast import Dict +from typing import List + from core.config import CoreConfig -class OngekiServerConfig(): + +class OngekiServerConfig: def __init__(self, parent_config: "OngekiConfig") -> None: self.__config = parent_config - + @property def enable(self) -> bool: - return CoreConfig.get_config_field(self.__config, 'ongeki', 'server', 'enable', default=True) - + return CoreConfig.get_config_field( + self.__config, "ongeki", "server", "enable", default=True + ) + @property def loglevel(self) -> int: - return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'ongeki', 'server', 'loglevel', default="info")) + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "ongeki", "server", "loglevel", default="info" + ) + ) + + @property + def use_https(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "ongeki", "server", "use_https", default=False + ) + + +class OngekiGachaConfig: + def __init__(self, parent_config: "OngekiConfig") -> None: + self.__config = parent_config + + @property + def enabled_gachas(self) -> List[int]: + return CoreConfig.get_config_field( + self.__config, "ongeki", "gachas", "enabled_gachas", default=[] + ) + + +class OngekiCardMakerVersionConfig: + def __init__(self, parent_config: "OngekiConfig") -> None: + self.__config = parent_config + + def version(self, version: int) -> Dict: + """ + in the form of: + : {"card_maker": } + 6: {"card_maker": 1.30.01} + """ + return CoreConfig.get_config_field( + self.__config, "ongeki", "version", default={} + ).get(version) + +class OngekiCryptoConfig: + def __init__(self, parent_config: "OngekiConfig") -> None: + self.__config = parent_config + + @property + def keys(self) -> Dict: + """ + in the form of: + internal_version: [key, iv] + all values are hex strings + """ + return CoreConfig.get_config_field( + self.__config, "ongeki", "crypto", "keys", default={} + ) + + @property + def encrypted_only(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "ongeki", "crypto", "encrypted_only", default=False + ) class OngekiConfig(dict): def __init__(self) -> None: self.server = OngekiServerConfig(self) + self.gachas = OngekiGachaConfig(self) + self.version = OngekiCardMakerVersionConfig(self) + self.crypto = OngekiCryptoConfig(self) diff --git a/titles/ongeki/const.py b/titles/ongeki/const.py index da9614e..a7658f7 100644 --- a/titles/ongeki/const.py +++ b/titles/ongeki/const.py @@ -1,8 +1,12 @@ from typing import Final, Dict from enum import Enum -class OngekiConstants(): + + +class OngekiConstants: GAME_CODE = "SDDT" + CONFIG_NAME = "ongeki.yaml" + VER_ONGEKI = 0 VER_ONGEKI_PLUS = 1 VER_ONGEKI_SUMMER = 2 @@ -10,29 +14,76 @@ class OngekiConstants(): VER_ONGEKI_RED = 4 VER_ONGEKI_RED_PLUS = 5 VER_ONGEKI_BRIGHT = 6 + VER_ONGEKI_BRIGHT_MEMORY = 7 - EVT_TYPES: Enum = Enum('EVT_TYPES', [ - 'None', - 'Announcement', - 'Movie', - 'AddMyList', - 'UnlockChapter', - 'JewelEvent', - 'RankingEvent', - 'AcceptRankingEvent', - 'UnlockMusic', - 'UnlockCard', - 'UnlockTrophy', - 'UnlockNamePlate', - 'UnlockLimitBreakItem', - 'MissionEvent', - 'DailyBonus', - 'UnlockBossLockEarly', - 'UnlockPurchaseItem', - 'TechChallengeEvent', - 'AcceptTechChallengeEvent', - 'SilverJewelEvent', - ]) + EVT_TYPES: Enum = Enum( + "EVT_TYPES", + [ + "Announcement", + "Movie", + "AddMyList", + "UnlockChapter", + "JewelEvent", + "RankingEvent", + "AcceptRankingEvent", + "UnlockMusic", + "UnlockCard", + "UnlockTrophy", + "UnlockNamePlate", + "UnlockLimitBreakItem", + "MissionEvent", + "DailyBonus", + "UnlockBossLockEarly", + "UnlockPurchaseItem", + "TechChallengeEvent", + "AcceptTechChallengeEvent", + "SilverJewelEvent", + "Max", + "None", + ], + ) + + REWARD_TYPES: Enum = Enum( + "REWARD_TYPES", + [ + "Card", + "NamePlate", + "Trophy", + "LimitBreakItem", + "AlmightyJewel", + "Money", + "Music", + "ProfileVoice", + "Present", + "ChapterJewel", + "GachaTicket", + "KaikaItem", + "ExpUpItem", + "IntimateUpItem", + "BookItem", + "SystemVoice", + "Costume", + "Medal", + "Attachment", + "UnlockItem", + "Max", + "None", + ], + ) + + class CM_GACHA_KINDS(Enum): + Normal = 0 + Pickup = 1 + BonusRestored = 2 + Free = 3 + PickupBonusRestored = 4 + + class RARITY_TYPES(Enum): + N = 0 + R = 1 + SR = 2 + SSR = 3 + SRPlus = 12 # The game expects the server to give Lunatic an ID of 10, while the game uses 4 internally... except in Music.xml class DIFF_NAME(Enum): @@ -42,9 +93,17 @@ class OngekiConstants(): Master = 3 Lunatic = 10 - VERSION_NAMES = ("ONGEKI", "ONGEKI+", "ONGEKI Summer", "ONGEKI Summer+", "ONGEKI Red", "ONGEKI Red+", - "ONGEKI Bright") + VERSION_NAMES = ( + "ONGEKI", + "ONGEKI +", + "ONGEKI SUMMER", + "ONGEKI SUMMER +", + "ONGEKI R.E.D.", + "ONGEKI R.E.D. +", + "ONGEKI bright", + "ONGEKI bright MEMORY", + ) @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] diff --git a/titles/ongeki/database.py b/titles/ongeki/database.py index a2168e4..89255c0 100644 --- a/titles/ongeki/database.py +++ b/titles/ongeki/database.py @@ -3,6 +3,7 @@ from core.config import CoreConfig from titles.ongeki.schema import OngekiItemData, OngekiProfileData, OngekiScoreData from titles.ongeki.schema import OngekiStaticData, OngekiLogData + class OngekiData(Data): def __init__(self, cfg: CoreConfig) -> None: super().__init__(cfg) @@ -11,4 +12,4 @@ class OngekiData(Data): self.profile = OngekiProfileData(cfg, self.session) self.score = OngekiScoreData(cfg, self.session) self.static = OngekiStaticData(cfg, self.session) - self.log = OngekiLogData(cfg, self.session) \ No newline at end of file + self.log = OngekiLogData(cfg, self.session) diff --git a/titles/ongeki/frontend.py b/titles/ongeki/frontend.py new file mode 100644 index 0000000..987776f --- /dev/null +++ b/titles/ongeki/frontend.py @@ -0,0 +1,87 @@ +import yaml +import jinja2 +from twisted.web.http import Request +from os import path +from twisted.web.util import redirectTo +from twisted.web.server import Session + +from core.frontend import FE_Base, IUserSession +from core.config import CoreConfig + +from titles.ongeki.config import OngekiConfig +from titles.ongeki.const import OngekiConstants +from titles.ongeki.database import OngekiData +from titles.ongeki.base import OngekiBase + + +class OngekiFrontend(FE_Base): + def __init__( + self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str + ) -> None: + super().__init__(cfg, environment) + self.data = OngekiData(cfg) + self.game_cfg = OngekiConfig() + if path.exists(f"{cfg_dir}/{OngekiConstants.CONFIG_NAME}"): + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{OngekiConstants.CONFIG_NAME}")) + ) + self.nav_name = "O.N.G.E.K.I." + self.version_list = OngekiConstants.VERSION_NAMES + + def render_GET(self, request: Request) -> bytes: + template = self.environment.get_template( + "titles/ongeki/frontend/ongeki_index.jinja" + ) + sesh: Session = request.getSession() + usr_sesh = IUserSession(sesh) + self.version = usr_sesh.ongeki_version + if getattr(usr_sesh, "userId", 0) != 0: + profile_data =self.data.profile.get_profile_data(usr_sesh.userId, self.version) + rival_list = self.data.profile.get_rivals(usr_sesh.userId) + rival_data = { + "userRivalList": rival_list, + "userId": usr_sesh.userId + } + rival_info = OngekiBase.handle_get_user_rival_data_api_request(self, rival_data) + + return template.render( + data=self.data.profile, + title=f"{self.core_config.server.name} | {self.nav_name}", + game_list=self.environment.globals["game_list"], + gachas=self.game_cfg.gachas.enabled_gachas, + profile_data=profile_data, + rival_info=rival_info["userRivalDataList"], + version_list=self.version_list, + version=self.version, + sesh=vars(usr_sesh) + ).encode("utf-16") + else: + return redirectTo(b"/gate/", request) + + def render_POST(self, request: Request): + uri = request.uri.decode() + sesh: Session = request.getSession() + usr_sesh = IUserSession(sesh) + if hasattr(usr_sesh, "userId"): + if uri == "/game/ongeki/rival.add": + rival_id = request.args[b"rivalUserId"][0].decode() + self.data.profile.put_rival(usr_sesh.userId, rival_id) + # self.logger.info(f"{usr_sesh.userId} added a rival") + return redirectTo(b"/game/ongeki/", request) + + elif uri == "/game/ongeki/rival.delete": + rival_id = request.args[b"rivalUserId"][0].decode() + self.data.profile.delete_rival(usr_sesh.userId, rival_id) + # self.logger.info(f"{response}") + return redirectTo(b"/game/ongeki/", request) + + elif uri == "/game/ongeki/version.change": + ongeki_version=request.args[b"version"][0].decode() + if(ongeki_version.isdigit()): + usr_sesh.ongeki_version=int(ongeki_version) + return redirectTo(b"/game/ongeki/", request) + + else: + return b"Something went wrong" + else: + return b"User is not logged in" diff --git a/titles/ongeki/frontend/js/ongeki_scripts.js b/titles/ongeki/frontend/js/ongeki_scripts.js new file mode 100644 index 0000000..6de309b --- /dev/null +++ b/titles/ongeki/frontend/js/ongeki_scripts.js @@ -0,0 +1,24 @@ +function deleteRival(rivalUserId){ + + $(document).ready(function () { + $.post("/game/ongeki/rival.delete", + { + rivalUserId + }, + function(data,status){ + window.location.replace("/game/ongeki/") + }) + }); +} +function changeVersion(sel){ + + $(document).ready(function () { + $.post("/game/ongeki/version.change", + { + version: sel.value + }, + function(data,status){ + window.location.replace("/game/ongeki/") + }) + }); +} diff --git a/titles/ongeki/frontend/ongeki_index.jinja b/titles/ongeki/frontend/ongeki_index.jinja new file mode 100644 index 0000000..b7b5a90 --- /dev/null +++ b/titles/ongeki/frontend/ongeki_index.jinja @@ -0,0 +1,83 @@ +{% extends "core/frontend/index.jinja" %} +{% block content %} + +{% if sesh is defined and sesh["userId"] > 0 %} +
+
+
+ +
+
+

Profile

+

Version: + +

+
+
+
+
+

Name: {{ profile_data.userName if profile_data.userName is defined else "Profile not found" }}

+
+
+

ID: {{ profile_data.user if profile_data.user is defined else 'Profile not found' }}

+
+
+
+
+

Rivals

+
+
+ + + + + + + + + + {% for rival in rival_info%} + + + + + + {% endfor %} + +
IDNameDelete
{{rival.rivalUserId}}{{rival.rivalUserName}}
+
+ +
+ + +{% else %} +

Not Currently Logged In

+{% endif %} +{% endblock content %} \ No newline at end of file diff --git a/titles/ongeki/index.py b/titles/ongeki/index.py index d3b107e..baf1ee3 100644 --- a/titles/ongeki/index.py +++ b/titles/ongeki/index.py @@ -3,26 +3,41 @@ import json import inflection import yaml import string -import logging, coloredlogs +import logging +import coloredlogs import zlib from logging.handlers import TimedRotatingFileHandler +from Crypto.Cipher import AES +from Crypto.Util.Padding import pad +from Crypto.Protocol.KDF import PBKDF2 +from Crypto.Hash import SHA1 +from os import path +from typing import Tuple, Dict, List from core.config import CoreConfig -from titles.ongeki.config import OngekiConfig -from titles.ongeki.const import OngekiConstants -from titles.ongeki.base import OngekiBase -from titles.ongeki.plus import OngekiPlus -from titles.ongeki.summer import OngekiSummer -from titles.ongeki.summerplus import OngekiSummerPlus -from titles.ongeki.red import OngekiRed -from titles.ongeki.redplus import OngekiRedPlus -from titles.ongeki.bright import OngekiBright +from core.utils import Utils +from core.title import BaseServlet +from .config import OngekiConfig +from .const import OngekiConstants +from .base import OngekiBase +from .plus import OngekiPlus +from .summer import OngekiSummer +from .summerplus import OngekiSummerPlus +from .red import OngekiRed +from .redplus import OngekiRedPlus +from .bright import OngekiBright +from .brightmemory import OngekiBrightMemory -class OngekiServlet(): + +class OngekiServlet(BaseServlet): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: - self.core_cfg = core_cfg + super().__init__(core_cfg, cfg_dir) self.game_cfg = OngekiConfig() - self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/ongeki.yaml"))) + self.hash_table: Dict[Dict[str, str]] = {} + if path.exists(f"{cfg_dir}/{OngekiConstants.CONFIG_NAME}"): + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{OngekiConstants.CONFIG_NAME}")) + ) self.versions = [ OngekiBase(core_cfg, self.game_cfg), @@ -32,83 +47,220 @@ class OngekiServlet(): OngekiRed(core_cfg, self.game_cfg), OngekiRedPlus(core_cfg, self.game_cfg), OngekiBright(core_cfg, self.game_cfg), + OngekiBrightMemory(core_cfg, self.game_cfg), ] self.logger = logging.getLogger("ongeki") - log_fmt_str = "[%(asctime)s] Ongeki | %(levelname)s | %(message)s" - log_fmt = logging.Formatter(log_fmt_str) - fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.core_cfg.server.log_dir, "ongeki"), encoding='utf8', - when="d", backupCount=10) - fileHandler.setFormatter(log_fmt) + if not hasattr(self.logger, "inited"): + log_fmt_str = "[%(asctime)s] Ongeki | %(levelname)s | %(message)s" + log_fmt = logging.Formatter(log_fmt_str) + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.core_cfg.server.log_dir, "ongeki"), + encoding="utf8", + when="d", + backupCount=10, + ) + + fileHandler.setFormatter(log_fmt) + + consoleHandler = logging.StreamHandler() + consoleHandler.setFormatter(log_fmt) + + self.logger.addHandler(fileHandler) + self.logger.addHandler(consoleHandler) + + self.logger.setLevel(self.game_cfg.server.loglevel) + coloredlogs.install( + level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str + ) + self.logger.inited = True + + for version, keys in self.game_cfg.crypto.keys.items(): + if len(keys) < 3: + continue + + self.hash_table[version] = {} + + method_list = [ + method + for method in dir(self.versions[version]) + if not method.startswith("__") + ] + for method in method_list: + method_fixed = inflection.camelize(method)[6:-7] + # number of iterations is 64 on Bright Memory + iter_count = 64 + hash = PBKDF2( + method_fixed, + bytes.fromhex(keys[2]), + 128, + count=iter_count, + hmac_hash_module=SHA1, + ) + + hashed_name = hash.hex()[:32] # truncate unused bytes like the game does + self.hash_table[version][hashed_name] = method_fixed + + self.logger.debug( + f"Hashed v{version} method {method_fixed} with {bytes.fromhex(keys[2])} to get {hash.hex()[:32]}" + ) + + @classmethod + def is_game_enabled(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> bool: + game_cfg = OngekiConfig() + + if path.exists(f"{cfg_dir}/{OngekiConstants.CONFIG_NAME}"): + game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{OngekiConstants.CONFIG_NAME}")) + ) + + if not game_cfg.server.enable: + return False - consoleHandler = logging.StreamHandler() - consoleHandler.setFormatter(log_fmt) + return True + + def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: + return ( + [], + [("render_POST", "/SDDT/{version}/{endpoint}", {})] + ) + + def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: + title_port_int = Utils.get_title_port(self.core_cfg) + title_port_ssl_int = Utils.get_title_port_ssl(self.core_cfg) + proto = "https" if self.game_cfg.server.use_https and game_ver >= 120 else "http" - self.logger.addHandler(fileHandler) - self.logger.addHandler(consoleHandler) + if proto == "https": + t_port = f":{title_port_ssl_int}" if title_port_ssl_int and not self.core_cfg.server.is_using_proxy else "" - self.logger.setLevel(self.game_cfg.server.loglevel) - coloredlogs.install(level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str) + else: + t_port = f":{title_port_int}" if title_port_int and not self.core_cfg.server.is_using_proxy else "" + + return ( + f"{proto}://{self.core_cfg.title.hostname}{t_port}/{game_code}/{game_ver}/", + f"{self.core_cfg.title.hostname}{t_port}/", + ) + + def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes: + endpoint = matchers['endpoint'] + version = int(matchers['version']) + if endpoint.lower() == "ping": + return zlib.compress(b'{"returnCode": 1}') - def render_POST(self, request: Request, version: int, url_path: str) -> bytes: req_raw = request.content.getvalue() - url_split = url_path.split("/") + encrtped = False internal_ver = 0 - endpoint = url_split[len(url_split) - 1] + client_ip = Utils.get_ip_addr(request) - if version < 105: # 1.0 + if version < 105: # 1.0 internal_ver = OngekiConstants.VER_ONGEKI - elif version >= 105 and version < 110: # Plus + elif version >= 105 and version < 110: # Plus internal_ver = OngekiConstants.VER_ONGEKI_PLUS - elif version >= 110 and version < 115: # Summer + elif version >= 110 and version < 115: # Summer internal_ver = OngekiConstants.VER_ONGEKI_SUMMER - elif version >= 115 and version < 120: # Summer Plus + elif version >= 115 and version < 120: # Summer Plus internal_ver = OngekiConstants.VER_ONGEKI_SUMMER_PLUS - elif version >= 120 and version < 125: # Red + elif version >= 120 and version < 125: # Red internal_ver = OngekiConstants.VER_ONGEKI_RED - elif version >= 125 and version < 130: # Red Plus + elif version >= 125 and version < 130: # Red Plus internal_ver = OngekiConstants.VER_ONGEKI_RED_PLUS - elif version >= 130 and version < 135: # Red Plus + elif version >= 130 and version < 135: # Bright internal_ver = OngekiConstants.VER_ONGEKI_BRIGHT + elif version >= 135 and version < 140: # Bright Memory + internal_ver = OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY 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 + # 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 # technically not 0 - self.logger.error("Encryption not supported at this time") + if internal_ver not in self.hash_table: + self.logger.error( + f"v{version} does not support encryption or no keys entered" + ) + return zlib.compress(b'{"stat": "0"}') - try: + elif endpoint.lower() not in self.hash_table[internal_ver]: + self.logger.error( + f"No hash found for v{version} endpoint {endpoint}" + ) + return zlib.compress(b'{"stat": "0"}') + + endpoint = self.hash_table[internal_ver][endpoint.lower()] + + try: + crypt = AES.new( + bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][0]), + AES.MODE_CBC, + bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]), + ) + + req_raw = crypt.decrypt(req_raw) + + except Exception as e: + self.logger.error( + f"Failed to decrypt v{version} request to {endpoint} -> {e}" + ) + return zlib.compress(b'{"stat": "0"}') + + encrtped = True + + if ( + not encrtped + and self.game_cfg.crypto.encrypted_only + and version >= 120 + ): + self.logger.error( + f"Unencrypted v{version} {endpoint} request, but config is set to encrypted only: {req_raw}" + ) + return zlib.compress(b'{"stat": "0"}') + + try: unzip = zlib.decompress(req_raw) - + except zlib.error as e: - self.logger.error(f"Failed to decompress v{version} {endpoint} request -> {e}") - return zlib.compress("{\"stat\": \"0\"}".encode("utf-8")) - + self.logger.error( + f"Failed to decompress v{version} {endpoint} request -> {e}" + ) + return zlib.compress(b'{"stat": "0"}') + req_data = json.loads(unzip) - - self.logger.info(f"v{version} {endpoint} request - {req_data}") + + self.logger.info( + f"v{version} {endpoint} request from {client_ip}" + ) + self.logger.debug(req_data) func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" + if not hasattr(self.versions[internal_ver], func_to_find): + self.logger.warning(f"Unhandled v{version} request {endpoint}") + return zlib.compress(b'{"returnCode": 1}') + try: handler = getattr(self.versions[internal_ver], func_to_find) resp = handler(req_data) - - except AttributeError as e: - self.logger.warning(f"Unhandled v{version} request {endpoint} - {e}") - return zlib.compress("{\"stat\": \"0\"}".encode("utf-8")) except Exception as e: self.logger.error(f"Error handling v{version} method {endpoint} - {e}") - return zlib.compress("{\"stat\": \"0\"}".encode("utf-8")) - + return zlib.compress(b'{"stat": "0"}') + if resp == None: - resp = {'returnCode': 1} - - self.logger.info(f"Response {resp}") - - return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) + resp = {"returnCode": 1} + self.logger.debug(f"Response {resp}") - \ No newline at end of file + zipped = zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) + + if not encrtped or version < 120: + return zipped + + padded = pad(zipped, 16) + + crypt = AES.new( + bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][0]), + AES.MODE_CBC, + bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]), + ) + + return crypt.encrypt(padded) \ No newline at end of file diff --git a/titles/ongeki/plus.py b/titles/ongeki/plus.py index 8875503..9168576 100644 --- a/titles/ongeki/plus.py +++ b/titles/ongeki/plus.py @@ -5,11 +5,12 @@ from titles.ongeki.base import OngekiBase from titles.ongeki.const import OngekiConstants from titles.ongeki.config import OngekiConfig + class OngekiPlus(OngekiBase): def __init__(self, core_cfg: CoreConfig, game_cfg: OngekiConfig) -> None: super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_PLUS - + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: ret = super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.05.00" diff --git a/titles/ongeki/read.py b/titles/ongeki/read.py index 8b1be3d..6e94094 100644 --- a/titles/ongeki/read.py +++ b/titles/ongeki/read.py @@ -11,29 +11,112 @@ from titles.ongeki.database import OngekiData from titles.ongeki.const import OngekiConstants from titles.ongeki.config import OngekiConfig + class OngekiReader(BaseReader): - def __init__(self, config: CoreConfig, version: int, bin_dir: Optional[str], opt_dir: Optional[str], extra: Optional[str]) -> None: + 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 = OngekiData(config) try: - self.logger.info(f"Start importer for {OngekiConstants.game_ver_to_string(version)}") + self.logger.info( + f"Start importer for {OngekiConstants.game_ver_to_string(version)}" + ) except IndexError: self.logger.error(f"Invalid ongeki version {version}") exit(1) - + def read(self) -> None: data_dirs = [] if self.bin_dir is not None: data_dirs += self.get_data_directories(self.bin_dir) - + if self.opt_dir is not None: data_dirs += self.get_data_directories(self.opt_dir) - + for dir in data_dirs: self.read_events(f"{dir}/event") self.read_music(f"{dir}/music") - + self.read_card(f"{dir}/card") + self.read_reward(f"{dir}/reward") + + def read_card(self, base_dir: str) -> None: + self.logger.info(f"Reading cards from {base_dir}...") + + version_ids = { + "1000": OngekiConstants.VER_ONGEKI, + "1005": OngekiConstants.VER_ONGEKI_PLUS, + "1010": OngekiConstants.VER_ONGEKI_SUMMER, + "1015": OngekiConstants.VER_ONGEKI_SUMMER_PLUS, + "1020": OngekiConstants.VER_ONGEKI_RED, + "1025": OngekiConstants.VER_ONGEKI_RED_PLUS, + "1030": OngekiConstants.VER_ONGEKI_BRIGHT, + "1035": OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY, + } + + for root, dirs, files in os.walk(base_dir): + for dir in dirs: + if os.path.exists(f"{root}/{dir}/Card.xml"): + with open(f"{root}/{dir}/Card.xml", "r", encoding="utf-8") as f: + troot = ET.fromstring(f.read()) + + card_id = int(troot.find("Name").find("id").text) + + # skip already existing cards + if ( + self.data.static.get_card( + OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY, card_id + ) + is not None + ): + self.logger.info(f"Card {card_id} already added, skipping") + continue + + name = troot.find("Name").find("str").text + chara_id = int(troot.find("CharaID").find("id").text) + nick_name = troot.find("NickName").text + school = troot.find("School").find("str").text + attribute = troot.find("Attribute").text + gakunen = troot.find("Gakunen").find("str").text + rarity = OngekiConstants.RARITY_TYPES[ + troot.find("Rarity").text + ].value + + level_param = [] + for lvl in troot.find("LevelParam").findall("int"): + level_param.append(lvl.text) + + skill_id = int(troot.find("SkillID").find("id").text) + cho_kai_ka_skill_id = int( + troot.find("ChoKaikaSkillID").find("id").text + ) + + version = version_ids[troot.find("VersionID").find("id").text] + card_number = troot.find("CardNumberString").text + + self.data.static.put_card( + version, + card_id, + name=name, + charaId=chara_id, + nickName=nick_name, + school=school, + attribute=attribute, + gakunen=gakunen, + rarity=rarity, + levelParam=",".join(level_param), + skillId=skill_id, + choKaikaSkillId=cho_kai_ka_skill_id, + cardNumber=card_number, + ) + self.logger.info(f"Added card {card_id}") + def read_events(self, base_dir: str) -> None: self.logger.info(f"Reading events from {base_dir}...") @@ -43,14 +126,15 @@ class OngekiReader(BaseReader): with open(f"{root}/{dir}/Event.xml", "r", encoding="utf-8") as f: troot = ET.fromstring(f.read()) - name = troot.find('Name').find('str').text - id = int(troot.find('Name').find('id').text) - event_type = OngekiConstants.EVT_TYPES[troot.find('EventType').text].value - + name = troot.find("Name").find("str").text + id = int(troot.find("Name").find("id").text) + event_type = OngekiConstants.EVT_TYPES[ + troot.find("EventType").text + ].value self.data.static.put_event(self.version, id, event_type, name) self.logger.info(f"Added event {id}") - + def read_music(self, base_dir: str) -> None: self.logger.info(f"Reading music from {base_dir}...") @@ -67,23 +151,49 @@ class OngekiReader(BaseReader): if root is None: continue - name = troot.find('Name') - song_id = name.find('id').text - title = name.find('str').text - artist = troot.find('ArtistName').find('str').text - genre = troot.find('Genre').find('str').text - + name = troot.find("Name") + song_id = name.find("id").text + title = name.find("str").text + artist = troot.find("ArtistName").find("str").text + genre = troot.find("Genre").find("str").text + fumens = troot.find("FumenData") - for fumens_data in fumens.findall('FumenData'): - path = fumens_data.find('FumenFile').find('path').text + for fumens_data in fumens.findall("FumenData"): + path = fumens_data.find("FumenFile").find("path").text if path is None or not os.path.exists(f"{root}/{dir}/{path}"): continue chart_id = int(path.split(".")[0].split("_")[1]) level = float( f"{fumens_data.find('FumenConstIntegerPart').text}.{fumens_data.find('FumenConstFractionalPart').text}" - ) - - self.data.static.put_chart(self.version, song_id, chart_id, title, artist, genre, level) + ) + + self.data.static.put_chart( + self.version, song_id, chart_id, title, artist, genre, level + ) self.logger.info(f"Added song {song_id} chart {chart_id}") + def read_reward(self, base_dir: str) -> None: + self.logger.info(f"Reading rewards from {base_dir}...") + + for root, dirs, files in os.walk(base_dir): + for dir in dirs: + if os.path.exists(f"{root}/{dir}/Reward.xml"): + strdata = "" + + with open(f"{root}/{dir}/Reward.xml", "r", encoding="utf-8") as f: + strdata = f.read() + + troot = ET.fromstring(strdata) + + if root is None: + continue + + name = troot.find("Name") + rewardId = name.find("id").text + rewardname = name.find("str").text + itemKind = OngekiConstants.REWARD_TYPES[troot.find("ItemType").text].value + itemId = troot.find("RewardItem").find("ItemName").find("id").text + + self.data.static.put_reward(self.version, rewardId, rewardname, itemKind, itemId) + self.logger.info(f"Added reward {rewardId}") diff --git a/titles/ongeki/red.py b/titles/ongeki/red.py index 047286e..52b9d59 100644 --- a/titles/ongeki/red.py +++ b/titles/ongeki/red.py @@ -5,11 +5,12 @@ from titles.ongeki.base import OngekiBase from titles.ongeki.const import OngekiConstants from titles.ongeki.config import OngekiConfig + class OngekiRed(OngekiBase): def __init__(self, core_cfg: CoreConfig, game_cfg: OngekiConfig) -> None: super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_RED - + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: ret = super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.20.00" diff --git a/titles/ongeki/redplus.py b/titles/ongeki/redplus.py index a4df205..1f69690 100644 --- a/titles/ongeki/redplus.py +++ b/titles/ongeki/redplus.py @@ -5,11 +5,12 @@ from titles.ongeki.base import OngekiBase from titles.ongeki.const import OngekiConstants from titles.ongeki.config import OngekiConfig + class OngekiRedPlus(OngekiBase): def __init__(self, core_cfg: CoreConfig, game_cfg: OngekiConfig) -> None: super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_RED_PLUS - + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: ret = super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.25.00" diff --git a/titles/ongeki/schema/__init__.py b/titles/ongeki/schema/__init__.py index 9b1e1da..b93a16c 100644 --- a/titles/ongeki/schema/__init__.py +++ b/titles/ongeki/schema/__init__.py @@ -4,4 +4,10 @@ from titles.ongeki.schema.static import OngekiStaticData from titles.ongeki.schema.score import OngekiScoreData from titles.ongeki.schema.log import OngekiLogData -__all__ = [OngekiProfileData, OngekiItemData, OngekiStaticData, OngekiScoreData, OngekiLogData] \ No newline at end of file +__all__ = [ + OngekiProfileData, + OngekiItemData, + OngekiStaticData, + OngekiScoreData, + OngekiLogData, +] diff --git a/titles/ongeki/schema/item.py b/titles/ongeki/schema/item.py index cc09fda..55b4c68 100644 --- a/titles/ongeki/schema/item.py +++ b/titles/ongeki/schema/item.py @@ -1,7 +1,9 @@ +from datetime import date, datetime, timedelta from typing import Dict, Optional, List from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON from sqlalchemy.schema import ForeignKey +from sqlalchemy.engine import Row from sqlalchemy.sql import func, select from sqlalchemy.dialects.mysql import insert @@ -27,7 +29,7 @@ card = Table( Column("isAcquired", Boolean), Column("created", String(25)), UniqueConstraint("user", "cardId", name="ongeki_user_card_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) deck = Table( @@ -40,7 +42,7 @@ deck = Table( Column("cardId2", Integer), Column("cardId3", Integer), UniqueConstraint("user", "deckId", name="ongeki_user_deck_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) character = Table( @@ -58,10 +60,10 @@ character = Table( Column("intimateCountDate", String(25)), Column("isNew", Boolean), UniqueConstraint("user", "characterId", name="ongeki_user_character_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) -boss = Table ( +boss = Table( "ongeki_user_boss", metadata, Column("id", Integer, primary_key=True, nullable=False), @@ -71,10 +73,10 @@ boss = Table ( Column("isClear", Boolean), Column("eventId", Integer), UniqueConstraint("user", "musicId", "eventId", name="ongeki_user_boss_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) -story = Table ( +story = Table( "ongeki_user_story", metadata, Column("id", Integer, primary_key=True, nullable=False), @@ -86,7 +88,7 @@ story = Table ( Column("lastPlayMusicCategory", Integer), Column("lastPlayMusicLevel", Integer), UniqueConstraint("user", "storyId", name="ongeki_user_story_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) chapter = Table( @@ -104,7 +106,28 @@ chapter = Table( Column("skipTiming1", Integer), Column("skipTiming2", Integer), UniqueConstraint("user", "chapterId", name="ongeki_user_chapter_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", +) + +memorychapter = Table( + "ongeki_user_memorychapter", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("chapterId", Integer), + Column("gaugeId", Integer), + Column("gaugeNum", Integer), + Column("jewelCount", Integer), + Column("isStoryWatched", Boolean), + Column("isBossWatched", Boolean), + Column("isDialogWatched", Boolean), + Column("isEndingWatched", Boolean), + Column("isClear", Boolean), + Column("lastPlayMusicId", Integer), + Column("lastPlayMusicLevel", Integer), + Column("lastPlayMusicCategory", Integer), + UniqueConstraint("user", "chapterId", name="ongeki_user_memorychapter_uk"), + mysql_charset="utf8mb4", ) item = Table( @@ -117,7 +140,7 @@ item = Table( Column("stock", Integer), Column("isValid", Boolean), UniqueConstraint("user", "itemKind", "itemId", name="ongeki_user_item_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) music_item = Table( @@ -128,7 +151,7 @@ music_item = Table( Column("musicId", Integer), Column("status", Integer), UniqueConstraint("user", "musicId", name="ongeki_user_music_item_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) login_bonus = Table( @@ -140,7 +163,7 @@ login_bonus = Table( Column("bonusCount", Integer), Column("lastUpdateDate", String(25)), UniqueConstraint("user", "bonusId", name="ongeki_user_login_bonus_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) event_point = Table( @@ -148,11 +171,15 @@ event_point = Table( metadata, Column("id", Integer, primary_key=True, nullable=False), Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), - Column("eventId", Integer), - Column("point", Integer), + Column("version", Integer, nullable=False), + Column("eventId", Integer, nullable=False), + Column("point", Integer, nullable=False), + Column("rank", Integer), + Column("type", Integer, nullable=False), + Column("date", String(25)), Column("isRankingRewarded", Boolean), UniqueConstraint("user", "eventId", name="ongeki_user_event_point_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) mission_point = Table( @@ -160,10 +187,11 @@ mission_point = Table( metadata, Column("id", Integer, primary_key=True, nullable=False), Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("version", Integer), Column("eventId", Integer), Column("point", Integer), UniqueConstraint("user", "eventId", name="ongeki_user_mission_point_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) scenerio = Table( @@ -174,7 +202,7 @@ scenerio = Table( Column("scenarioId", Integer), Column("playCount", Integer), UniqueConstraint("user", "scenarioId", name="ongeki_user_scenerio_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) trade_item = Table( @@ -185,8 +213,10 @@ trade_item = Table( Column("chapterId", Integer), Column("tradeItemId", Integer), Column("tradeCount", Integer), - UniqueConstraint("user", "chapterId", "tradeItemId", name="ongeki_user_trade_item_uk"), - mysql_charset='utf8mb4' + UniqueConstraint( + "user", "chapterId", "tradeItemId", name="ongeki_user_trade_item_uk" + ), + mysql_charset="utf8mb4", ) event_music = Table( @@ -202,8 +232,10 @@ event_music = Table( Column("platinumScoreMax", Integer), Column("techRecordDate", String(25)), Column("isTechNewRecord", Boolean), - UniqueConstraint("user", "eventId", "type", "musicId", "level", name="ongeki_user_event_music"), - mysql_charset='utf8mb4' + UniqueConstraint( + "user", "eventId", "type", "musicId", "level", name="ongeki_user_event_music" + ), + mysql_charset="utf8mb4", ) tech_event = Table( @@ -211,18 +243,102 @@ tech_event = Table( metadata, Column("id", Integer, primary_key=True, nullable=False), Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), - Column("eventId", Integer), - Column("totalTechScore", Integer), - Column("totalPlatinumScore", Integer), + Column("version", Integer, nullable=False), + Column("eventId", Integer, nullable=False), + Column("totalTechScore", Integer, nullable=False), + Column("totalPlatinumScore", Integer, nullable=False), Column("techRecordDate", String(25)), Column("isRankingRewarded", Boolean), Column("isTotalTechNewRecord", Boolean), UniqueConstraint("user", "eventId", name="ongeki_user_tech_event_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", +) + +tech_ranking = Table( + "ongeki_tech_event_ranking", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("version", Integer, nullable=False), + Column("date", String(25)), + Column("eventId", Integer, nullable=False), + Column("rank", Integer), + Column("totalPlatinumScore", Integer, nullable=False), + Column("totalTechScore", Integer, nullable=False), + UniqueConstraint("user", "eventId", name="ongeki_tech_event_ranking_uk"), + mysql_charset="utf8mb4", +) + +gacha = Table( + "ongeki_user_gacha", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("gachaId", Integer, nullable=False), + Column("totalGachaCnt", Integer, server_default="0"), + Column("ceilingGachaCnt", Integer, server_default="0"), + Column("selectPoint", Integer, server_default="0"), + Column("useSelectPoint", Integer, server_default="0"), + Column("dailyGachaCnt", Integer, server_default="0"), + Column("fiveGachaCnt", Integer, server_default="0"), + Column("elevenGachaCnt", Integer, server_default="0"), + Column("dailyGachaDate", TIMESTAMP, nullable=False, server_default=func.now()), + UniqueConstraint("user", "gachaId", name="ongeki_user_gacha_uk"), + mysql_charset="utf8mb4", +) + +gacha_supply = Table( + "ongeki_user_gacha_supply", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("cardId", Integer, nullable=False), + UniqueConstraint("user", "cardId", name="ongeki_user_gacha_supply_uk"), + mysql_charset="utf8mb4", ) -class OngekiItemData(BaseData): +print_detail = Table( + "ongeki_user_print_detail", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("cardId", Integer, nullable=False), + Column("cardType", Integer, server_default="0"), + Column("printDate", TIMESTAMP, nullable=False), + Column("serialId", String(20), nullable=False), + Column("placeId", Integer, nullable=False), + Column("clientId", String(11), nullable=False), + Column("printerSerialId", String(20), nullable=False), + Column("isHolograph", Boolean, server_default="0"), + Column("isAutographed", Boolean, server_default="0"), + Column("printOption1", Boolean, server_default="1"), + Column("printOption2", Boolean, server_default="1"), + Column("printOption3", Boolean, server_default="1"), + Column("printOption4", Boolean, server_default="1"), + Column("printOption5", Boolean, server_default="1"), + Column("printOption6", Boolean, server_default="1"), + Column("printOption7", Boolean, server_default="1"), + Column("printOption8", Boolean, server_default="1"), + Column("printOption9", Boolean, server_default="1"), + Column("printOption10", Boolean, server_default="0"), + UniqueConstraint("serialId", name="ongeki_user_print_detail_uk"), + mysql_charset="utf8mb4", +) + +class OngekiItemData(BaseData): def put_card(self, aime_id: int, card_data: Dict) -> Optional[int]: card_data["user"] = aime_id @@ -231,7 +347,7 @@ class OngekiItemData(BaseData): result = self.execute(conflict) if result is None: - self.logger.warn(f"put_card: Failed to update! aime_id: {aime_id}") + self.logger.warning(f"put_card: Failed to update! aime_id: {aime_id}") return None return result.lastrowid @@ -239,7 +355,8 @@ class OngekiItemData(BaseData): sql = select(card).where(card.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() def put_character(self, aime_id: int, character_data: Dict) -> Optional[int]: @@ -250,15 +367,16 @@ class OngekiItemData(BaseData): result = self.execute(conflict) if result is None: - self.logger.warn(f"put_character: Failed to update! aime_id: {aime_id}") + self.logger.warning(f"put_character: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - + def get_characters(self, aime_id: int) -> Optional[List[Dict]]: sql = select(character).where(character.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() def put_deck(self, aime_id: int, deck_data: Dict) -> Optional[int]: @@ -269,22 +387,24 @@ class OngekiItemData(BaseData): result = self.execute(conflict) if result is None: - self.logger.warn(f"put_deck: Failed to update! aime_id: {aime_id}") + self.logger.warning(f"put_deck: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - + def get_deck(self, aime_id: int, deck_id: int) -> Optional[Dict]: sql = select(deck).where(and_(deck.c.user == aime_id, deck.c.deckId == deck_id)) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() - + def get_decks(self, aime_id: int) -> Optional[List[Dict]]: sql = select(deck).where(deck.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() def put_boss(self, aime_id: int, boss_data: Dict) -> Optional[int]: @@ -295,10 +415,10 @@ class OngekiItemData(BaseData): result = self.execute(conflict) if result is None: - self.logger.warn(f"put_boss: Failed to update! aime_id: {aime_id}") + self.logger.warning(f"put_boss: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - + def put_story(self, aime_id: int, story_data: Dict) -> Optional[int]: story_data["user"] = aime_id @@ -307,15 +427,16 @@ class OngekiItemData(BaseData): result = self.execute(conflict) if result is None: - self.logger.warn(f"put_story: Failed to update! aime_id: {aime_id}") + self.logger.warning(f"put_story: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - + def get_stories(self, aime_id: int) -> Optional[List[Dict]]: sql = select(story).where(story.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() def put_chapter(self, aime_id: int, chapter_data: Dict) -> Optional[int]: @@ -326,15 +447,16 @@ class OngekiItemData(BaseData): result = self.execute(conflict) if result is None: - self.logger.warn(f"put_chapter: Failed to update! aime_id: {aime_id}") + self.logger.warning(f"put_chapter: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - + def get_chapters(self, aime_id: int) -> Optional[List[Dict]]: sql = select(chapter).where(chapter.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() def put_item(self, aime_id: int, item_data: Dict) -> Optional[int]: @@ -345,25 +467,29 @@ class OngekiItemData(BaseData): result = self.execute(conflict) if result is None: - self.logger.warn(f"put_item: Failed to update! aime_id: {aime_id}") + self.logger.warning(f"put_item: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - + def get_item(self, aime_id: int, item_id: int, item_kind: int) -> Optional[Dict]: sql = select(item).where(and_(item.c.user == aime_id, item.c.itemId == item_id)) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() def get_items(self, aime_id: int, item_kind: int = None) -> Optional[List[Dict]]: if item_kind is None: sql = select(item).where(item.c.user == aime_id) else: - sql = select(item).where(and_(item.c.user == aime_id, item.c.itemKind == item_kind)) + sql = select(item).where( + and_(item.c.user == aime_id, item.c.itemKind == item_kind) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() def put_music_item(self, aime_id: int, music_item_data: Dict) -> Optional[int]: @@ -374,15 +500,16 @@ class OngekiItemData(BaseData): result = self.execute(conflict) if result is None: - self.logger.warn(f"put_music_item: Failed to update! aime_id: {aime_id}") + self.logger.warning(f"put_music_item: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - + def get_music_items(self, aime_id: int) -> Optional[List[Dict]]: sql = select(music_item).where(music_item.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() def put_login_bonus(self, aime_id: int, login_bonus_data: Dict) -> Optional[int]: @@ -393,18 +520,20 @@ class OngekiItemData(BaseData): result = self.execute(conflict) if result is None: - self.logger.warn(f"put_login_bonus: Failed to update! aime_id: {aime_id}") + self.logger.warning(f"put_login_bonus: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - + def get_login_bonuses(self, aime_id: int) -> Optional[List[Dict]]: sql = select(login_bonus).where(login_bonus.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - def put_mission_point(self, aime_id: int, mission_point_data: Dict) -> Optional[int]: + def put_mission_point(self, aime_id: int, version: int, mission_point_data: Dict) -> Optional[int]: + mission_point_data["version"] = version mission_point_data["user"] = aime_id sql = insert(mission_point).values(**mission_point_data) @@ -412,34 +541,42 @@ class OngekiItemData(BaseData): result = self.execute(conflict) if result is None: - self.logger.warn(f"put_mission_point: Failed to update! aime_id: {aime_id}") + self.logger.warning(f"put_mission_point: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - - def get_mission_points(self, aime_id: int) -> Optional[List[Dict]]: - sql = select(mission_point).where(mission_point.c.user == aime_id) + + def get_mission_points(self, version: int, aime_id: int) -> Optional[List[Dict]]: + sql = select(mission_point).where(and_(mission_point.c.user == aime_id, mission_point.c.version == version)) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - - def put_event_point(self, aime_id: int, event_point_data: Dict) -> Optional[int]: + + def put_event_point(self, aime_id: int, version: int, event_point_data: Dict) -> Optional[int]: + # We update only the newest (type: 1) entry, in official spec game watches for both latest(type:1) and previous (type:2) entries to give an additional info how many ranks has player moved up or down + # This fully featured is on TODO list, at the moment we just update the tables as data comes and give out rank as request comes event_point_data["user"] = aime_id + event_point_data["version"] = version + event_point_data["type"] = 1 + event_point_time = datetime.now() + event_point_data["date"] = datetime.strftime(event_point_time, "%Y-%m-%d %H:%M") sql = insert(event_point).values(**event_point_data) conflict = sql.on_duplicate_key_update(**event_point_data) result = self.execute(conflict) if result is None: - self.logger.warn(f"put_event_point: Failed to update! aime_id: {aime_id}") + self.logger.warning(f"put_event_point: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - + def get_event_points(self, aime_id: int) -> Optional[List[Dict]]: sql = select(event_point).where(event_point.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() def put_scenerio(self, aime_id: int, scenerio_data: Dict) -> Optional[int]: @@ -450,15 +587,16 @@ class OngekiItemData(BaseData): result = self.execute(conflict) if result is None: - self.logger.warn(f"put_scenerio: Failed to update! aime_id: {aime_id}") + self.logger.warning(f"put_scenerio: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - + def get_scenerios(self, aime_id: int) -> Optional[List[Dict]]: sql = select(scenerio).where(scenerio.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() def put_trade_item(self, aime_id: int, trade_item_data: Dict) -> Optional[int]: @@ -469,17 +607,18 @@ class OngekiItemData(BaseData): result = self.execute(conflict) if result is None: - self.logger.warn(f"put_trade_item: Failed to update! aime_id: {aime_id}") + self.logger.warning(f"put_trade_item: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - + def get_trade_items(self, aime_id: int) -> Optional[List[Dict]]: sql = select(trade_item).where(trade_item.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - + def put_event_music(self, aime_id: int, event_music_data: Dict) -> Optional[int]: event_music_data["user"] = aime_id @@ -488,39 +627,152 @@ class OngekiItemData(BaseData): result = self.execute(conflict) if result is None: - self.logger.warn(f"put_event_music: Failed to update! aime_id: {aime_id}") + self.logger.warning(f"put_event_music: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - + def get_event_music(self, aime_id: int) -> Optional[List[Dict]]: sql = select(event_music).where(event_music.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - def put_tech_event(self, aime_id: int, tech_event_data: Dict) -> Optional[int]: + def put_tech_event(self, aime_id: int, version: int, tech_event_data: Dict) -> Optional[int]: tech_event_data["user"] = aime_id + tech_event_data["version"] = version sql = insert(tech_event).values(**tech_event_data) conflict = sql.on_duplicate_key_update(**tech_event_data) result = self.execute(conflict) if result is None: - self.logger.warn(f"put_tech_event: Failed to update! aime_id: {aime_id}") + self.logger.warning(f"put_tech_event: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - - def get_tech_event(self, aime_id: int) -> Optional[List[Dict]]: - sql = select(tech_event).where(tech_event.c.user == aime_id) + + def put_tech_event_ranking(self, aime_id: int, version: int, tech_event_data: Dict) -> Optional[int]: + tech_event_data["user"] = aime_id + tech_event_data["version"] = version + tech_event_data.pop("isRankingRewarded") + tech_event_data.pop("isTotalTechNewRecord") + tech_event_data["date"] = tech_event_data.pop("techRecordDate") + tech_event_data["rank"] = 0 + + sql = insert(tech_ranking).values(**tech_event_data) + conflict = sql.on_duplicate_key_update(**tech_event_data) + result = self.execute(conflict) + + if result is None: + self.logger.warning(f"put_tech_event_ranking: Failed to update ranking! aime_id {aime_id}") + return None + return result.lastrowid + + def get_tech_event(self, version: int, aime_id: int) -> Optional[List[Dict]]: + sql = select(tech_event).where(and_(tech_event.c.user == aime_id, tech_event.c.version == version)) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() def get_bosses(self, aime_id: int) -> Optional[List[Dict]]: sql = select(boss).where(boss.c.user == aime_id) result = self.execute(sql) - if result is None: return None - return result.fetchall() \ No newline at end of file + if result is None: + return None + return result.fetchall() + + def put_memorychapter( + self, aime_id: int, memorychapter_data: Dict + ) -> Optional[int]: + memorychapter_data["user"] = aime_id + + sql = insert(memorychapter).values(**memorychapter_data) + conflict = sql.on_duplicate_key_update(**memorychapter_data) + result = self.execute(conflict) + + if result is None: + self.logger.warning(f"put_memorychapter: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def get_memorychapters(self, aime_id: int) -> Optional[List[Dict]]: + sql = select(memorychapter).where(memorychapter.c.user == aime_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_user_gacha(self, aime_id: int, gacha_id: int) -> Optional[Row]: + sql = gacha.select(and_(gacha.c.user == aime_id, gacha.c.gachaId == gacha_id)) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_user_gachas(self, aime_id: int) -> Optional[List[Row]]: + sql = gacha.select(gacha.c.user == aime_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_user_gacha_supplies(self, aime_id: int) -> Optional[List[Row]]: + sql = gacha_supply.select(gacha_supply.c.user == aime_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def put_user_gacha(self, aime_id: int, gacha_id: int, **data) -> Optional[int]: + sql = insert(gacha).values(user=aime_id, gachaId=gacha_id, **data) + + conflict = sql.on_duplicate_key_update(user=aime_id, gachaId=gacha_id, **data) + result = self.execute(conflict) + + if result is None: + self.logger.warning(f"put_user_gacha: Failed to insert! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_user_print_detail( + self, aime_id: int, serial_id: str, user_print_data: Dict + ) -> Optional[int]: + sql = insert(print_detail).values( + user=aime_id, serialId=serial_id, **user_print_data + ) + + conflict = sql.on_duplicate_key_update(user=aime_id, **user_print_data) + result = self.execute(conflict) + + if result is None: + self.logger.warning( + f"put_user_print_detail: Failed to insert! aime_id: {aime_id}" + ) + return None + return result.lastrowid + + + def get_ranking_event_ranks(self, version: int, aime_id: int) -> Optional[List[Dict]]: + # Calculates player rank on GameRequest from server, and sends it back, official spec would rank players in maintenance period, on TODO list + sql = select(event_point.c.id, event_point.c.user, event_point.c.eventId, event_point.c.type, func.row_number().over(partition_by=event_point.c.eventId, order_by=event_point.c.point.desc()).label('rank'), event_point.c.date, event_point.c.point).where(event_point.c.version == version) + result = self.execute(sql) + if result is None: + self.logger.error(f"failed to rank aime_id: {aime_id} ranking event positions") + return None + return result.fetchall() + + def get_tech_event_ranking(self, version: int, aime_id: int) -> Optional[List[Dict]]: + sql = select(tech_ranking.c.id, tech_ranking.c.user, tech_ranking.c.date, tech_ranking.c.eventId, func.row_number().over(partition_by=tech_ranking.c.eventId, order_by=[tech_ranking.c.totalTechScore.desc(),tech_ranking.c.totalPlatinumScore.desc()]).label('rank'), tech_ranking.c.totalTechScore, tech_ranking.c.totalPlatinumScore).where(tech_ranking.c.version == version) + result = self.execute(sql) + if result is None: + self.logger.warning(f"aime_id: {aime_id} has no tech ranking ranks") + return None + return result.fetchall() diff --git a/titles/ongeki/schema/log.py b/titles/ongeki/schema/log.py index 67ed778..bd5b071 100644 --- a/titles/ongeki/schema/log.py +++ b/titles/ongeki/schema/log.py @@ -15,11 +15,13 @@ gp_log = Table( Column("usedCredit", Integer), Column("placeName", String(255)), Column("trxnDate", String(255)), - Column("placeId", Integer), # Making this an FK would mess with people playing with default KC - Column("kind", Integer), - Column("pattern", Integer), + Column( + "placeId", Integer + ), # Making this an FK would mess with people playing with default KC + Column("kind", Integer), + Column("pattern", Integer), Column("currentGP", Integer), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) session_log = Table( @@ -32,12 +34,22 @@ session_log = Table( Column("playDate", String(10)), Column("userPlayDate", String(25)), Column("isPaid", Boolean), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) + class OngekiLogData(BaseData): - def put_gp_log(self, aime_id: Optional[int], used_credit: int, place_name: str, tx_date: str, place_id: int, - kind: int, pattern: int, current_gp: int) -> Optional[int]: + def put_gp_log( + self, + aime_id: Optional[int], + used_credit: int, + place_name: str, + tx_date: str, + place_id: int, + kind: int, + pattern: int, + current_gp: int, + ) -> Optional[int]: sql = insert(gp_log).values( user=aime_id, usedCredit=used_credit, @@ -51,5 +63,7 @@ class OngekiLogData(BaseData): result = self.execute(sql) if result is None: - self.logger.warn(f"put_gp_log: Failed to insert GP log! aime_id: {aime_id} kind {kind} pattern {pattern} current_gp {current_gp}") - return result.lastrowid \ No newline at end of file + self.logger.warning( + f"put_gp_log: Failed to insert GP log! aime_id: {aime_id} kind {kind} pattern {pattern} current_gp {current_gp}" + ) + return result.lastrowid diff --git a/titles/ongeki/schema/profile.py b/titles/ongeki/schema/profile.py index bdff67d..6071bad 100644 --- a/titles/ongeki/schema/profile.py +++ b/titles/ongeki/schema/profile.py @@ -3,7 +3,7 @@ from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, an from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, BigInteger from sqlalchemy.engine.base import Connection from sqlalchemy.schema import ForeignKey -from sqlalchemy.sql import func, select +from sqlalchemy.sql import func, select, delete from sqlalchemy.engine import Row from sqlalchemy.dialects.mysql import insert @@ -16,7 +16,11 @@ profile = Table( "ongeki_profile_data", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("version", Integer, nullable=False), Column("userName", String(8)), Column("level", Integer), @@ -78,8 +82,10 @@ profile = Table( Column("overDamageBattlePoint", Integer, server_default="0"), Column("bestBattlePoint", Integer, server_default="0"), Column("lastEmoneyBrand", Integer, server_default="0"), + Column("lastEmoneyCredit", Integer, server_default="0"), + Column("isDialogWatchedSuggestMemory", Boolean, server_default="0"), UniqueConstraint("user", "version", name="ongeki_profile_profile_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) # No point setting defaults since the game sends everything on profile creation anyway @@ -87,7 +93,11 @@ option = Table( "ongeki_profile_option", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("optionSet", Integer), Column("speed", Integer), Column("mirror", Integer), @@ -126,14 +136,18 @@ option = Table( Column("stealthField", Integer), Column("colorWallBright", Integer), UniqueConstraint("user", name="ongeki_profile_option_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) activity = Table( "ongeki_profile_activity", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("kind", Integer), Column("activityId", Integer), Column("sortNumber", Integer), @@ -142,69 +156,81 @@ activity = Table( Column("param3", Integer), Column("param4", Integer), UniqueConstraint("user", "kind", "activityId", name="ongeki_profile_activity_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) recent_rating = Table( "ongeki_profile_recent_rating", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("recentRating", JSON), UniqueConstraint("user", name="ongeki_profile_recent_rating_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) rating_log = Table( "ongeki_profile_rating_log", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("highestRating", Integer), Column("dataVersion", String(10)), UniqueConstraint("user", "dataVersion", name="ongeki_profile_rating_log_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) region = Table( "ongeki_profile_region", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("regionId", Integer), Column("playCount", Integer), Column("created", String(25)), UniqueConstraint("user", "regionId", name="ongeki_profile_region_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) -training_room = Table ( +training_room = Table( "ongeki_profile_training_room", metadata, Column("id", Integer, primary_key=True, nullable=False), Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), - Column("roomId", Integer), + Column("roomId", Integer), Column("authKey", Integer), Column("cardId", Integer), Column("valueDate", String(25)), UniqueConstraint("user", "roomId", name="ongeki_profile_training_room_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) -kop = Table ( +kop = Table( "ongeki_profile_kop", metadata, Column("id", Integer, primary_key=True, nullable=False), Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), Column("authKey", Integer), - Column("kopId", Integer), + Column("kopId", Integer), Column("areaId", Integer), Column("totalTechScore", Integer), Column("totalPlatinumScore", Integer), Column("techRecordDate", String(25)), Column("isTotalTechNewRecord", Boolean), UniqueConstraint("user", "kopId", name="ongeki_profile_kop_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) rival = Table( @@ -212,87 +238,114 @@ rival = Table( metadata, Column("id", Integer, primary_key=True, nullable=False), Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), - Column("rivalUserId", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column( + "rivalUserId", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + ), UniqueConstraint("user", "rivalUserId", name="ongeki_profile_rival_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) + class OngekiProfileData(BaseData): def __init__(self, cfg: CoreConfig, conn: Connection) -> None: super().__init__(cfg, conn) - self.date_time_format_ext = "%Y-%m-%d %H:%M:%S.%f" # needs to be lopped off at [:-5] + self.date_time_format_ext = ( + "%Y-%m-%d %H:%M:%S.%f" # needs to be lopped off at [:-5] + ) self.date_time_format_short = "%Y-%m-%d" def get_profile_name(self, aime_id: int, version: int) -> Optional[str]: - sql = select(profile.c.userName).where(and_(profile.c.user == aime_id, profile.c.version == version)) - - result = self.execute(sql) - if result is None: return None - - row = result.fetchone() - if row is None: return None - - return row["userName"] - - def get_profile_preview(self, aime_id: int, version: int) -> Optional[Row]: - sql = select([profile, option]).join(option, profile.c.user == option.c.user).filter( + sql = select(profile.c.userName).where( and_(profile.c.user == aime_id, profile.c.version == version) ) result = self.execute(sql) - if result is None: return None + if result is None: + return None + + row = result.fetchone() + if row is None: + return None + + return row["userName"] + + def get_profile_preview(self, aime_id: int, version: int) -> Optional[Row]: + sql = ( + select([profile, option]) + .join(option, profile.c.user == option.c.user) + .filter(and_(profile.c.user == aime_id, profile.c.version == version)) + ) + + result = self.execute(sql) + if result is None: + return None return result.fetchone() def get_profile_data(self, aime_id: int, version: int) -> Optional[Row]: - sql = select(profile).where(and_( - profile.c.user == aime_id, - profile.c.version == version, - )) + sql = select(profile).where( + and_( + profile.c.user == aime_id, + profile.c.version == version, + ) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() def get_profile_options(self, aime_id: int) -> Optional[Row]: - sql = select(option).where(and_( - option.c.user == aime_id, - )) + sql = select(option).where( + and_( + option.c.user == aime_id, + ) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() def get_profile_recent_rating(self, aime_id: int) -> Optional[List[Row]]: sql = select(recent_rating).where(recent_rating.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() def get_profile_rating_log(self, aime_id: int) -> Optional[List[Row]]: - sql = select(rating_log).where(recent_rating.c.user == aime_id) + sql = select(rating_log).where(rating_log.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - def get_profile_activity(self, aime_id: int, kind: int = None) -> Optional[List[Row]]: - sql = select(activity).where(and_( - activity.c.user == aime_id, - (activity.c.kind == kind) if kind is not None else True - )) + def get_profile_activity( + self, aime_id: int, kind: int = None + ) -> Optional[List[Row]]: + sql = select(activity).where( + and_( + activity.c.user == aime_id, + (activity.c.kind == kind) if kind is not None else True, + ) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - + def get_kop(self, aime_id: int) -> Optional[List[Row]]: sql = select(kop).where(kop.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - + def get_rivals(self, aime_id: int) -> Optional[List[Row]]: sql = select(rival.c.rivalUserId).where(rival.c.user == aime_id) @@ -311,7 +364,7 @@ class OngekiProfileData(BaseData): result = self.execute(conflict) if result is None: - self.logger.warn(f"put_profile_data: Failed to update! aime_id: {aime_id}") + self.logger.warning(f"put_profile_data: Failed to update! aime_id: {aime_id}") return None return result.lastrowid @@ -323,48 +376,62 @@ class OngekiProfileData(BaseData): result = self.execute(conflict) if result is None: - self.logger.warn(f"put_profile_options: Failed to update! aime_id: {aime_id}") + self.logger.warning( + f"put_profile_options: Failed to update! aime_id: {aime_id}" + ) return None return result.lastrowid - def put_profile_recent_rating(self, aime_id: int, recent_rating_data: List[Dict]) -> Optional[int]: + def put_profile_recent_rating( + self, aime_id: int, recent_rating_data: List[Dict] + ) -> Optional[int]: sql = insert(recent_rating).values( - user=aime_id, - recentRating=recent_rating_data + user=aime_id, recentRating=recent_rating_data ) - conflict = sql.on_duplicate_key_update( - recentRating=recent_rating_data - ) + conflict = sql.on_duplicate_key_update(recentRating=recent_rating_data) result = self.execute(conflict) if result is None: - self.logger.warn(f"put_profile_recent_rating: failed to update recent rating! aime_id {aime_id}") + self.logger.warning( + f"put_profile_recent_rating: failed to update recent rating! aime_id {aime_id}" + ) return None return result.lastrowid - def put_profile_bp_list(self, aime_id: int, bp_base_list: List[Dict]) -> Optional[int]: + def put_profile_bp_list( + self, aime_id: int, bp_base_list: List[Dict] + ) -> Optional[int]: pass - - def put_profile_rating_log(self, aime_id: int, data_version: str, highest_rating: int) -> Optional[int]: + + def put_profile_rating_log( + self, aime_id: int, data_version: str, highest_rating: int + ) -> Optional[int]: sql = insert(rating_log).values( - user=aime_id, - dataVersion=data_version, - highestRating=highest_rating + user=aime_id, dataVersion=data_version, highestRating=highest_rating ) - conflict = sql.on_duplicate_key_update( - highestRating=highest_rating - ) + conflict = sql.on_duplicate_key_update(highestRating=highest_rating) result = self.execute(conflict) if result is None: - self.logger.warn(f"put_profile_rating_log: failed to update rating log! aime_id {aime_id} data_version {data_version} highest_rating {highest_rating}") + self.logger.warning( + f"put_profile_rating_log: failed to update rating log! aime_id {aime_id} data_version {data_version} highest_rating {highest_rating}" + ) return None return result.lastrowid - def put_profile_activity(self, aime_id: int, kind: int, activity_id: int, sort_num: int, - p1: int, p2: int, p3: int, p4: int) -> Optional[int]: + def put_profile_activity( + self, + aime_id: int, + kind: int, + activity_id: int, + sort_num: int, + p1: int, + p2: int, + p3: int, + p4: int, + ) -> Optional[int]: sql = insert(activity).values( user=aime_id, kind=kind, @@ -373,29 +440,24 @@ class OngekiProfileData(BaseData): param1=p1, param2=p2, param3=p3, - param4=p4 + param4=p4, ) conflict = sql.on_duplicate_key_update( - sortNumber=sort_num, - param1=p1, - param2=p2, - param3=p3, - param4=p4 + sortNumber=sort_num, param1=p1, param2=p2, param3=p3, param4=p4 ) result = self.execute(conflict) if result is None: - self.logger.warn(f"put_profile_activity: failed to put activity! aime_id {aime_id} kind {kind} activity_id {activity_id}") + self.logger.warning( + f"put_profile_activity: failed to put activity! aime_id {aime_id} kind {kind} activity_id {activity_id}" + ) return None return result.lastrowid - + def put_profile_region(self, aime_id: int, region: int, date: str) -> Optional[int]: sql = insert(activity).values( - user=aime_id, - region=region, - playCount=1, - created=date + user=aime_id, region=region, playCount=1, created=date ) conflict = sql.on_duplicate_key_update( @@ -404,10 +466,12 @@ class OngekiProfileData(BaseData): result = self.execute(conflict) if result is None: - self.logger.warn(f"put_profile_region: failed to update! aime_id {aime_id} region {region}") + self.logger.warning( + f"put_profile_region: failed to update! aime_id {aime_id} region {region}" + ) return None return result.lastrowid - + def put_training_room(self, aime_id: int, room_detail: Dict) -> Optional[int]: room_detail["user"] = aime_id @@ -416,10 +480,10 @@ class OngekiProfileData(BaseData): result = self.execute(conflict) if result is None: - self.logger.warn(f"put_best_score: Failed to add score! aime_id: {aime_id}") + self.logger.warning(f"put_best_score: Failed to add score! aime_id: {aime_id}") return None return result.lastrowid - + def put_kop(self, aime_id: int, kop_data: Dict) -> Optional[int]: kop_data["user"] = aime_id @@ -428,20 +492,26 @@ class OngekiProfileData(BaseData): result = self.execute(conflict) if result is None: - self.logger.warn(f"put_kop: Failed to add score! aime_id: {aime_id}") + self.logger.warning(f"put_kop: Failed to add score! aime_id: {aime_id}") return None return result.lastrowid - - def put_rival(self, aime_id: int, rival_id: int) -> Optional[int]: - sql = insert(rival).values( - user = aime_id, - rivalUserId = rival_id - ) - conflict = sql.on_duplicate_key_update(rival = rival_id) + def put_rival(self, aime_id: int, rival_id: int) -> Optional[int]: + sql = insert(rival).values(user=aime_id, rivalUserId=rival_id) + + conflict = sql.on_duplicate_key_update(rivalUserId=rival_id) result = self.execute(conflict) if result is None: - self.logger.warn(f"put_rival: failed to update! aime_id: {aime_id}, rival_id: {rival_id}") + self.logger.warning( + f"put_rival: failed to update! aime_id: {aime_id}, rival_id: {rival_id}" + ) return None return result.lastrowid + def delete_rival(self, aime_id: int, rival_id: int) -> Optional[int]: + sql = delete(rival).where(rival.c.user==aime_id, rival.c.rivalUserId==rival_id) + result = self.execute(sql) + if result is None: + self.logger.error(f"delete_rival: failed to delete! aime_id: {aime_id}, rival_id: {rival_id}") + else: + return result.rowcount \ No newline at end of file diff --git a/titles/ongeki/schema/score.py b/titles/ongeki/schema/score.py index 08a6a86..7c8ce15 100644 --- a/titles/ongeki/schema/score.py +++ b/titles/ongeki/schema/score.py @@ -11,14 +11,18 @@ score_best = Table( "ongeki_score_best", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("musicId", Integer, nullable=False), Column("level", Integer, nullable=False), - Column("playCount", Integer, nullable=False), + Column("playCount", Integer, nullable=False), Column("techScoreMax", Integer, nullable=False), Column("techScoreRank", Integer, nullable=False), Column("battleScoreMax", Integer, nullable=False), - Column("battleScoreRank", Integer, nullable=False), + Column("battleScoreRank", Integer, nullable=False), Column("maxComboCount", Integer, nullable=False), Column("maxOverKill", Float, nullable=False), Column("maxTeamOverKill", Float, nullable=False), @@ -28,15 +32,20 @@ score_best = Table( Column("isLock", Boolean, nullable=False), Column("clearStatus", Boolean, nullable=False), Column("isStoryWatched", Boolean, nullable=False), + Column("platinumScoreMax", Integer), UniqueConstraint("user", "musicId", "level", name="ongeki_best_score_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) playlog = Table( "ongeki_score_playlog", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("sortNumber", Integer), Column("placeId", Integer), Column("placeName", String(255)), @@ -96,25 +105,32 @@ playlog = Table( Column("isAllBreak", Boolean), Column("playerRating", Integer), Column("battlePoint", Integer), - mysql_charset='utf8mb4' + Column("platinumScore", Integer), + Column("platinumScoreMax", Integer), + mysql_charset="utf8mb4", ) tech_count = Table( "ongeki_score_tech_count", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("levelId", Integer, nullable=False), Column("allBreakCount", Integer), Column("allBreakPlusCount", Integer), UniqueConstraint("user", "levelId", name="ongeki_tech_count_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) + class OngekiScoreData(BaseData): def get_tech_count(self, aime_id: int) -> Optional[List[Dict]]: return [] - + def put_tech_count(self, aime_id: int, tech_count_data: Dict) -> Optional[int]: tech_count_data["user"] = aime_id @@ -123,20 +139,23 @@ class OngekiScoreData(BaseData): result = self.execute(conflict) if result is None: - self.logger.warn(f"put_tech_count: Failed to update! aime_id: {aime_id}") + self.logger.warning(f"put_tech_count: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - + def get_best_scores(self, aime_id: int) -> Optional[List[Dict]]: sql = select(score_best).where(score_best.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - def get_best_score(self, aime_id: int, song_id: int, chart_id: int = None) -> Optional[List[Dict]]: + def get_best_score( + self, aime_id: int, song_id: int, chart_id: int = None + ) -> Optional[List[Dict]]: return [] - + def put_best_score(self, aime_id: int, music_detail: Dict) -> Optional[int]: music_detail["user"] = aime_id @@ -145,7 +164,7 @@ class OngekiScoreData(BaseData): result = self.execute(conflict) if result is None: - self.logger.warn(f"put_best_score: Failed to add score! aime_id: {aime_id}") + self.logger.warning(f"put_best_score: Failed to add score! aime_id: {aime_id}") return None return result.lastrowid @@ -156,6 +175,6 @@ class OngekiScoreData(BaseData): result = self.execute(sql) if result is None: - self.logger.warn(f"put_playlog: Failed to add playlog! aime_id: {aime_id}") + self.logger.warning(f"put_playlog: Failed to add playlog! aime_id: {aime_id}") return None - return result.lastrowid \ No newline at end of file + return result.lastrowid diff --git a/titles/ongeki/schema/static.py b/titles/ongeki/schema/static.py index e98ec58..695d39a 100644 --- a/titles/ongeki/schema/static.py +++ b/titles/ongeki/schema/static.py @@ -7,6 +7,7 @@ from sqlalchemy.engine import Row from sqlalchemy.dialects.mysql import insert from core.data.schema import BaseData, metadata +from core.data.schema.arcade import machine events = Table( "ongeki_static_events", @@ -16,9 +17,11 @@ events = Table( Column("eventId", Integer), Column("type", Integer), Column("name", String(255)), + Column("startDate", TIMESTAMP, server_default=func.now()), + Column("endDate", TIMESTAMP, server_default=func.now()), Column("enabled", Boolean, server_default="1"), UniqueConstraint("version", "eventId", "type", name="ongeki_static_events_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) @@ -34,86 +37,476 @@ music = Table( Column("genre", String(255)), Column("level", Float), UniqueConstraint("version", "songId", "chartId", name="ongeki_static_music_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", +) + +gachas = Table( + "ongeki_static_gachas", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column("gachaId", Integer, nullable=False), + Column("gachaName", String(255), nullable=False), + Column("type", Integer, nullable=False, server_default="0"), + Column("kind", Integer, nullable=False, server_default="0"), + Column("isCeiling", Boolean, server_default="0"), + Column("maxSelectPoint", Integer, server_default="0"), + Column("ceilingCnt", Integer, server_default="10"), + Column("changeRateCnt1", Integer, server_default="0"), + Column("changeRateCnt2", Integer, server_default="0"), + Column("startDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"), + Column("endDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"), + Column("noticeStartDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"), + Column("noticeEndDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"), + Column("convertEndDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"), + UniqueConstraint("version", "gachaId", "gachaName", name="ongeki_static_gachas_uk"), + mysql_charset="utf8mb4", +) + +gacha_cards = Table( + "ongeki_static_gacha_cards", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("gachaId", Integer, nullable=False), + Column("cardId", Integer, nullable=False), + Column("rarity", Integer, nullable=False), + Column("weight", Integer, server_default="1"), + Column("isPickup", Boolean, server_default="0"), + Column("isSelect", Boolean, server_default="0"), + UniqueConstraint("gachaId", "cardId", name="ongeki_static_gacha_cards_uk"), + mysql_charset="utf8mb4", +) + +cards = Table( + "ongeki_static_cards", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column("cardId", Integer, nullable=False), + Column("name", String(255), nullable=False), + Column("charaId", Integer, nullable=False), + Column("nickName", String(255)), + Column("school", String(255), nullable=False), + Column("attribute", String(5), nullable=False), + Column("gakunen", String(255), nullable=False), + Column("rarity", Integer, nullable=False), + Column("levelParam", String(255), nullable=False), + Column("skillId", Integer, nullable=False), + Column("choKaikaSkillId", Integer, nullable=False), + Column("cardNumber", String(255)), + UniqueConstraint("version", "cardId", name="ongeki_static_cards_uk"), + mysql_charset="utf8mb4", +) + +music_ranking = Table( + "ongeki_static_music_ranking_list", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column("musicId", Integer, nullable=False), + Column("point", Integer, nullable=False), + Column("userName", String(255)), + UniqueConstraint("version", "musicId", name="ongeki_static_music_ranking_uk"), + mysql_charset="utf8mb4", +) + +rewards = Table( + "ongeki_static_rewards", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column("rewardId", Integer, nullable=False), + Column("rewardname", String(255), nullable=False), + Column("itemKind", Integer, nullable=False), + Column("itemId", Integer, nullable=False), + UniqueConstraint("version", "rewardId", name="ongeki_static_rewards_uk"), + mysql_charset="utf8mb4", +) + +present = Table( + "ongeki_static_present_list", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column("presentId", Integer, nullable=False), + Column("presentName", String(255), nullable=False), + Column("rewardId", Integer, nullable=False), + Column("stock", Integer, nullable=False), + Column("message", String(255)), + Column("startDate", String(25), nullable=False), + Column("endDate", String(25), nullable=False), + UniqueConstraint("version", "presentId", name="ongeki_static_present_list_uk"), + mysql_charset="utf8mb4", +) + +tech_music = Table( + "ongeki_static_tech_music", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column("eventId", Integer, nullable=False), + Column("musicId", Integer, nullable=False), + Column("level", Integer, nullable=False), + UniqueConstraint("version", "musicId", name="ongeki_static_tech_music_uk"), + mysql_charset="utf8mb4", +) + +client_testmode = Table( + "ongeki_static_client_testmode", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("regionId", Integer, nullable=False), + Column("placeId", Integer, nullable=False), + Column("clientId", String(11), nullable=False), + Column("updateDate", TIMESTAMP, nullable=False), + Column("isDelivery", Boolean, nullable=False), + Column("groupId", Integer, nullable=False), + Column("groupRole", Integer, nullable=False), + Column("continueMode", Integer, nullable=False), + Column("selectMusicTime", Integer, nullable=False), + Column("advertiseVolume", Integer, nullable=False), + Column("eventMode", Integer, nullable=False), + Column("eventMusicNum", Integer, nullable=False), + Column("patternGp", Integer, nullable=False), + Column("limitGp", Integer, nullable=False), + Column("maxLeverMovable", Integer, nullable=False), + Column("minLeverMovable", Integer, nullable=False), + UniqueConstraint("clientId", name="ongeki_static_client_testmode_uk"), + mysql_charset="utf8mb4", +) + +game_point = Table( + "ongeki_static_game_point", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("type", Integer, nullable=False), + Column("cost", Integer, nullable=False), + Column("startDate", String(25), nullable=False, server_default="2000-01-01 05:00:00.0"), + Column("endDate", String(25), nullable=False, server_default="2099-01-01 05:00:00.0"), + UniqueConstraint("type", name="ongeki_static_game_point_uk"), + mysql_charset="utf8mb4", ) class OngekiStaticData(BaseData): - def put_event(self, version: int, event_id: int, event_type: int, event_name: str) -> Optional[int]: - sql = insert(events).values( - version = version, - eventId = event_id, - type = event_type, - name = event_name, + def put_card(self, version: int, card_id: int, **card_data) -> Optional[int]: + sql = insert(cards).values(version=version, cardId=card_id, **card_data) + + conflict = sql.on_duplicate_key_update(**card_data) + + result = self.execute(conflict) + if result is None: + self.logger.warning(f"Failed to insert card! card_id {card_id}") + return None + return result.lastrowid + + def get_card(self, version: int, card_id: int) -> Optional[Dict]: + sql = cards.select(and_(cards.c.version <= version, cards.c.cardId == card_id)) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_card_by_card_number(self, version: int, card_number: str) -> Optional[Dict]: + if not card_number.startswith("[O.N.G.E.K.I.]"): + card_number = f"[O.N.G.E.K.I.]{card_number}" + + sql = cards.select( + and_(cards.c.version <= version, cards.c.cardNumber == card_number) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_card_by_name(self, version: int, name: str) -> Optional[Dict]: + sql = cards.select(and_(cards.c.version <= version, cards.c.name == name)) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_cards(self, version: int) -> Optional[List[Dict]]: + sql = cards.select(cards.c.version <= version) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_cards_by_rarity(self, version: int, rarity: int) -> Optional[List[Dict]]: + sql = cards.select(and_(cards.c.version <= version, cards.c.rarity == rarity)) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def put_gacha( + self, + version: int, + gacha_id: int, + gacha_name: int, + gacha_kind: int, + **gacha_data, + ) -> Optional[int]: + sql = insert(gachas).values( + version=version, + gachaId=gacha_id, + gachaName=gacha_name, + kind=gacha_kind, + **gacha_data, ) conflict = sql.on_duplicate_key_update( - name = event_name, + version=version, + gachaId=gacha_id, + gachaName=gacha_name, + kind=gacha_kind, + **gacha_data, ) result = self.execute(conflict) if result is None: - self.logger.warn(f"Failed to insert event! event_id {event_id}") + self.logger.warning(f"Failed to insert gacha! gacha_id {gacha_id}") + return None + return result.lastrowid + + def get_gacha(self, version: int, gacha_id: int) -> Optional[Dict]: + sql = gachas.select( + and_(gachas.c.version <= version, gachas.c.gachaId == gacha_id) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_gachas(self, version: int) -> Optional[List[Dict]]: + sql = gachas.select(gachas.c.version == version).order_by( + gachas.c.gachaId.asc() + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def put_gacha_card( + self, gacha_id: int, card_id: int, **gacha_card + ) -> Optional[int]: + sql = insert(gacha_cards).values(gachaId=gacha_id, cardId=card_id, **gacha_card) + + conflict = sql.on_duplicate_key_update( + gachaId=gacha_id, cardId=card_id, **gacha_card + ) + + result = self.execute(conflict) + if result is None: + self.logger.warning(f"Failed to insert gacha card! gacha_id {gacha_id}") + return None + return result.lastrowid + + def get_gacha_cards(self, gacha_id: int) -> Optional[List[Dict]]: + sql = gacha_cards.select(gacha_cards.c.gachaId == gacha_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def put_event( + self, version: int, event_id: int, event_type: int, event_name: str + ) -> Optional[int]: + sql = insert(events).values( + version=version, + eventId=event_id, + type=event_type, + name=event_name, + endDate=f"2038-01-01 00:00:00", + ) + + conflict = sql.on_duplicate_key_update( + name=event_name, + ) + + result = self.execute(conflict) + if result is None: + self.logger.warning(f"Failed to insert event! event_id {event_id}") return None return result.lastrowid def get_event(self, version: int, event_id: int) -> Optional[List[Dict]]: - sql = select(events).where(and_(events.c.version == version, events.c.eventId == event_id)) - + sql = select(events).where( + and_(events.c.version == version, events.c.eventId == event_id) + ) + result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() def get_events(self, version: int) -> Optional[List[Dict]]: sql = select(events).where(events.c.version == version) - + result = self.execute(sql) - if result is None: return None - return result.fetchall() - - def get_enabled_events(self, version: int) -> Optional[List[Dict]]: - sql = select(events).where(and_(events.c.version == version, events.c.enabled == True)) - - result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - def put_chart(self, version: int, song_id: int, chart_id: int, title: str, artist: str, genre: str, level: float) -> Optional[int]: + def get_enabled_events(self, version: int) -> Optional[List[Dict]]: + sql = select(events).where( + and_(events.c.version == version, events.c.enabled == True) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def put_chart( + self, + version: int, + song_id: int, + chart_id: int, + title: str, + artist: str, + genre: str, + level: float, + ) -> Optional[int]: sql = insert(music).values( - version = version, - songId = song_id, - chartId = chart_id, - title = title, - artist = artist, - genre = genre, - level = level, + version=version, + songId=song_id, + chartId=chart_id, + title=title, + artist=artist, + genre=genre, + level=level, ) conflict = sql.on_duplicate_key_update( - title = title, - artist = artist, - genre = genre, - level = level, + title=title, + artist=artist, + genre=genre, + level=level, ) result = self.execute(conflict) if result is None: - self.logger.warn(f"Failed to insert chart! song_id: {song_id}, chart_id: {chart_id}") + self.logger.warning( + f"Failed to insert chart! song_id: {song_id}, chart_id: {chart_id}" + ) return None return result.lastrowid - def get_chart(self, version: int, song_id: int, chart_id: int = None) -> Optional[List[Dict]]: + def get_chart( + self, version: int, song_id: int, chart_id: int = None + ) -> Optional[List[Dict]]: pass def get_music(self, version: int) -> Optional[List[Dict]]: pass - def get_music_chart(self, version: int, song_id: int, chart_id: int) -> Optional[List[Row]]: - sql = select(music).where(and_( - music.c.version == version, - music.c.songId == song_id, - music.c.chartId == chart_id - )) + def get_music_chart( + self, version: int, song_id: int, chart_id: int + ) -> Optional[List[Row]]: + sql = select(music).where( + and_( + music.c.version == version, + music.c.songId == song_id, + music.c.chartId == chart_id, + ) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() + + def get_ranking_list(self, version: int) -> Optional[List[Dict]]: + sql = select(music_ranking.c.musicId.label('id'), music_ranking.c.point, music_ranking.c.userName).where(music_ranking.c.version == version) + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def put_reward(self, version: int, rewardId: int, rewardname: str, itemKind: int, itemId: int) -> Optional[int]: + sql = insert(rewards).values( + version=version, + rewardId=rewardId, + rewardname=rewardname, + itemKind=itemKind, + itemId=itemId, + ) + conflict = sql.on_duplicate_key_update( + rewardname=rewardname, + ) + result = self.execute(conflict) + if result is None: + self.logger.warning(f"Failed to insert reward! reward_id: {rewardId}") + return None + return result.lastrowid + + def get_reward_list(self, version: int) -> Optional[List[Dict]]: + sql = select(rewards).where(rewards.c.version == version) + + result = self.execute(sql) + if result is None: + self.logger.warning(f"Failed to load reward list") + return None + return result.fetchall() + + def get_present_list(self, version: int) -> Optional[List[Dict]]: + sql = select(present).where(present.c.version == version) + + result = self.execute(sql) + if result is None: + self.logger.warning(f"Failed to load present list") + return None + return result.fetchall() + + def get_tech_music(self, version: int) -> Optional[List[Dict]]: + sql = select(tech_music).where(tech_music.c.version == version) + + result = self.execute(sql) + + if result is None: + return None + return result.fetchall() + + def put_client_testmode_data(self, region_id: int, client_testmode_data: Dict) -> Optional[List[Dict]]: + sql = insert(client_testmode).values(regionId=region_id, **client_testmode_data) + conflict = sql.on_duplicate_key_update(regionId=region_id, **client_testmode_data) + + result = self.execute(conflict) + if result is None: + self.logger.warning(f"region_id: {region_id} Failed to update ClientTestMode data"), + return None + return result.lastrowid + + def put_client_setting_data(self, machine_id: int, client_setting_data: Dict) -> Optional[List[Dict]]: + sql = machine.update(machine.c.id == machine_id).values(data=client_setting_data) + + result = self.execute(sql) + if result is None: + self.logger.warning(f"machine_id: {machine_id} Failed to update ClientSetting data"), + return None + return result.lastrowid + + def put_static_game_point_defaults(self) -> Optional[List[Dict]]: + game_point_defaults = [{"type": 0, "cost": 100},{"type": 1, "cost": 230},{"type": 2, "cost": 370},{"type": 3, "cost": 120},{"type": 4, "cost": 240},{"type": 5, "cost": 360}] + sql = insert(game_point).values(game_point_defaults) + result = self.execute(sql) + if result is None: + self.logger.warning(f"Failed to insert default GP table!") + return None + return result.lastrowid + + def get_static_game_point(self) -> Optional[List[Dict]]: + sql = select(game_point.c.type, game_point.c.cost, game_point.c.startDate, game_point.c.endDate) + result = self.execute(sql) + if result is None: + return None + return result.fetchall() diff --git a/titles/ongeki/summer.py b/titles/ongeki/summer.py index 24ed290..adc8c0f 100644 --- a/titles/ongeki/summer.py +++ b/titles/ongeki/summer.py @@ -5,11 +5,12 @@ from titles.ongeki.base import OngekiBase from titles.ongeki.const import OngekiConstants from titles.ongeki.config import OngekiConfig + class OngekiSummer(OngekiBase): def __init__(self, core_cfg: CoreConfig, game_cfg: OngekiConfig) -> None: super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_SUMMER - + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: ret = super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.10.00" diff --git a/titles/ongeki/summerplus.py b/titles/ongeki/summerplus.py index 188e618..8b2cd03 100644 --- a/titles/ongeki/summerplus.py +++ b/titles/ongeki/summerplus.py @@ -5,11 +5,12 @@ from titles.ongeki.base import OngekiBase from titles.ongeki.const import OngekiConstants from titles.ongeki.config import OngekiConfig + class OngekiSummerPlus(OngekiBase): def __init__(self, core_cfg: CoreConfig, game_cfg: OngekiConfig) -> None: super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_SUMMER_PLUS - + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: ret = super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.15.00" diff --git a/titles/pokken/__init__.py b/titles/pokken/__init__.py index 5be8ac6..94237c4 100644 --- a/titles/pokken/__init__.py +++ b/titles/pokken/__init__.py @@ -1,17 +1,10 @@ -from titles.pokken.index import PokkenServlet -from titles.pokken.const import PokkenConstants +from .index import PokkenServlet +from .const import PokkenConstants +from .database import PokkenData +from .frontend import PokkenFrontend index = PokkenServlet - -use_default_title = True -include_protocol = True -title_secure = True +database = PokkenData game_codes = [PokkenConstants.GAME_CODE] -trailing_slash = True -use_default_host = False - -include_port = True -uri="https://$h:$p/" -host="$h:$p/" - -current_schema_version = 1 \ No newline at end of file +current_schema_version = 1 +frontend = PokkenFrontend diff --git a/titles/pokken/base.py b/titles/pokken/base.py index bd95295..9663e81 100644 --- a/titles/pokken/base.py +++ b/titles/pokken/base.py @@ -1,17 +1,25 @@ from datetime import datetime, timedelta -import json -from typing import Any +import json, logging +from typing import Any, Dict, List +import random -from core.config import CoreConfig -from titles.pokken.config import PokkenConfig -from titles.pokken.proto import jackal_pb2 +from core.data import Data +from core import CoreConfig +from .config import PokkenConfig +from .proto import jackal_pb2 +from .database import PokkenData +from .const import PokkenConstants -class PokkenBase(): + +class PokkenBase: def __init__(self, core_cfg: CoreConfig, game_cfg: PokkenConfig) -> None: self.core_cfg = core_cfg self.game_cfg = game_cfg self.version = 0 - + self.logger = logging.getLogger("pokken") + self.data = PokkenData(core_cfg) + self.SUPPORT_SET_NONE = 4294967295 + def handle_noop(self, request: Any) -> bytes: res = jackal_pb2.Response() res.result = 1 @@ -19,38 +27,39 @@ class PokkenBase(): return res.SerializeToString() - def handle_ping(self, request: jackal_pb2.PingRequestData) -> bytes: + def handle_ping(self, request: jackal_pb2.Request) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.PING return res.SerializeToString() - - def handle_register_pcb(self, request: jackal_pb2.RegisterPcbRequestData) -> bytes: + + def handle_register_pcb(self, request: jackal_pb2.Request) -> bytes: 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}") regist_pcb = jackal_pb2.RegisterPcbResponseData() - regist_pcb.server_time = int(datetime.now().timestamp() / 1000) + regist_pcb.server_time = int(datetime.now().timestamp()) biwa_setting = { "MatchingServer": { - "host": f"https://{self.core_cfg.title.hostname}", - "port": 9000, - "url": "/matching" + "host": f"https://{self.game_cfg.server.hostname}", + "port": self.game_cfg.ports.game, + "url": "/SDAK/100/matching", }, "StunServer": { - "addr": self.core_cfg.title.hostname, - "port": 3333 + "addr": self.game_cfg.server.stun_server_host, + "port": self.game_cfg.server.stun_server_port, }, "TurnServer": { - "addr": self.core_cfg.title.hostname, - "port": 4444 + "addr": self.game_cfg.server.stun_server_host, + "port": self.game_cfg.server.stun_server_port, }, - "AdmissionUrl": f"ws://{self.core_cfg.title.hostname}:1111", - "locationId": 123, + "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" + "biwalogfilename": "./biwa.log", } regist_pcb.bnp_baseuri = f"{self.core_cfg.title.hostname}/bna" regist_pcb.biwa_setting = json.dumps(biwa_setting) @@ -59,34 +68,35 @@ class PokkenBase(): return res.SerializeToString() - def handle_save_ads(self, request: jackal_pb2.SaveAdsRequestData) -> bytes: + def handle_save_ads(self, request: jackal_pb2.Request) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.SAVE_ADS return res.SerializeToString() - def handle_save_client_log(self, request: jackal_pb2.SaveClientLogRequestData) -> bytes: + def handle_save_client_log(self, request: jackal_pb2.Request) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.SAVE_CLIENT_LOG return res.SerializeToString() - def handle_check_diagnosis(self, request: jackal_pb2.CheckDiagnosisRequestData) -> bytes: + def handle_check_diagnosis(self, request: jackal_pb2.Request) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.CHECK_DIAGNOSIS return res.SerializeToString() - def handle_load_client_settings(self, request: jackal_pb2.CheckDiagnosisRequestData) -> bytes: + def handle_load_client_settings(self, request: jackal_pb2.Request) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.LOAD_CLIENT_SETTINGS settings = jackal_pb2.LoadClientSettingsResponseData() - settings.money_magnification = 0 + # TODO: Make configurable + settings.money_magnification = 1 settings.continue_bonus_exp = 100 settings.continue_fight_money = 100 settings.event_bonus_exp = 100 @@ -101,3 +111,344 @@ class PokkenBase(): res.load_client_settings.CopyFrom(settings) return res.SerializeToString() + + def handle_load_ranking(self, request: jackal_pb2.Request) -> bytes: + res = jackal_pb2.Response() + res.result = 1 + res.type = jackal_pb2.MessageType.LOAD_RANKING + ranking = jackal_pb2.LoadRankingResponseData() + + ranking.ranking_id = 1 + ranking.ranking_start = 0 + ranking.ranking_end = 1 + ranking.event_end = True + ranking.modify_date = int(datetime.now().timestamp() / 1000) + res.load_ranking.CopyFrom(ranking) + return res.SerializeToString() + + def handle_load_user(self, request: jackal_pb2.Request) -> bytes: + res = jackal_pb2.Response() + res.result = 1 + res.type = jackal_pb2.MessageType.LOAD_USER + access_code = request.load_user.access_code + load_usr = jackal_pb2.LoadUserResponseData() + user_id = self.data.card.get_user_id_from_card(access_code) + + if user_id is None and self.game_cfg.server.auto_register: + user_id = self.data.user.create_user() + card_id = self.data.card.create_card(user_id, access_code) + + self.logger.info( + f"Register new card {access_code} (UserId {user_id}, CardId {card_id})" + ) + + elif user_id is None: + self.logger.info(f"Registration of card {access_code} blocked!") + res.load_user.CopyFrom(load_usr) + return res.SerializeToString() + + """ + TODO: Add repeated values + tutorial_progress_flag + rankmatch_progress + support_pokemon_list + support_set_1 + support_set_2 + support_set_3 + aid_skill_list + achievement_flag + event_achievement_flag + event_achievement_param + """ + profile = 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: + profile_id = self.data.profile.create_profile(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', '')})" + ) + pokemon_data = 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) + load_usr.navi_enable_flag = profile_dict.get("navi_enable_flag", True) + load_usr.pad_vibrate_flag = profile_dict.get("pad_vibrate_flag", True) + load_usr.home_region_code = profile_dict.get("home_region_code", 0) + load_usr.home_loc_name = profile_dict.get("home_loc_name", "") + load_usr.pref_code = profile_dict.get("pref_code", 0) + 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.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) + load_usr.total_play_days = profile_dict.get("total_play_days", 0) + load_usr.play_date_time = profile_dict.get("play_date_time", 0) + load_usr.lucky_box_fail_num = profile_dict.get("lucky_box_fail_num", 0) + load_usr.event_reward_get_flag = profile_dict.get("event_reward_get_flag", 0) + load_usr.rank_pvp_all = profile_dict.get("rank_pvp_all", 0) + load_usr.rank_pvp_loc = profile_dict.get("rank_pvp_loc", 0) + load_usr.rank_cpu_all = profile_dict.get("rank_cpu_all", 0) + load_usr.rank_cpu_loc = profile_dict.get("rank_cpu_loc", 0) + 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_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.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.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) + load_usr.timeup_win_num = profile_dict.get("timeup_win_num", 0) + 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.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_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) + load_usr.avatar_face0 = profile_dict.get("avatar_face0", 0) + load_usr.avatar_face1 = profile_dict.get("avatar_face1", 0) + load_usr.avatar_face2 = profile_dict.get("avatar_face2", 0) + load_usr.avatar_bodyall = profile_dict.get("avatar_bodyall", 0) + load_usr.avatar_wear = profile_dict.get("avatar_wear", 0) + load_usr.avatar_accessory = profile_dict.get("avatar_accessory", 0) + load_usr.avatar_stamp = profile_dict.get("avatar_stamp", 0) + + load_usr.event_state = profile_dict.get("event_state", 0) + load_usr.event_id = profile_dict.get("event_id", 0) + load_usr.sp_bonus_category_id_1 = profile_dict.get("sp_bonus_category_id_1", 0) + load_usr.sp_bonus_key_value_1 = profile_dict.get("sp_bonus_key_value_1", 0) + load_usr.sp_bonus_category_id_2 = profile_dict.get("sp_bonus_category_id_2", 0) + load_usr.sp_bonus_key_value_2 = profile_dict.get("sp_bonus_key_value_2", 0) + load_usr.last_play_event_id = profile_dict.get("last_play_event_id", 0) + + if pokemon_data is not None: + for pkmn in pokemon_data: + pkmn_d = pkmn._asdict() + pkm = jackal_pb2.LoadUserResponseData.PokemonData() + + pkm.char_id = pkmn_d.get('char_id', 0) + pkm.illustration_book_no = pkmn_d.get('illustration_book_no', 0) + pkm.pokemon_exp = pkmn_d.get('pokemon_exp', 0) + pkm.battle_num_vs_wan = pkmn_d.get('battle_num_vs_wan', 0) + pkm.win_vs_wan = pkmn_d.get('win_vs_wan', 0) + pkm.battle_num_vs_lan = pkmn_d.get('battle_num_vs_lan', 0) + pkm.win_vs_lan = pkmn_d.get('win_vs_lan', 0) + pkm.battle_num_vs_cpu = pkmn_d.get('battle_num_vs_cpu', 0) + pkm.win_cpu = pkmn_d.get('win_cpu', 0) + pkm.battle_all_num_tutorial = pkmn_d.get('battle_all_num_tutorial', 0) + pkm.battle_num_tutorial = pkmn_d.get('battle_num_tutorial', 0) + pkm.bp_point_atk = pkmn_d.get('bp_point_atk', 0) + pkm.bp_point_res = pkmn_d.get('bp_point_res', 0) + pkm.bp_point_def = pkmn_d.get('bp_point_def', 0) + pkm.bp_point_sp = pkmn_d.get('bp_point_sp', 0) + + load_usr.pokemon_data.append(pkm) + + res.load_user.CopyFrom(load_usr) + return res.SerializeToString() + + def handle_set_bnpassid_lock(self, data: jackal_pb2.Request) -> bytes: + res = jackal_pb2.Response() + res.result = 1 + res.type = jackal_pb2.MessageType.SET_BNPASSID_LOCK + return res.SerializeToString() + + def handle_save_user(self, request: jackal_pb2.Request) -> bytes: + res = jackal_pb2.Response() + res.result = 1 + res.type = jackal_pb2.MessageType.SAVE_USER + + req = request.save_user + user_id = req.banapass_id + + tut_flgs: List[int] = [] + ach_flgs: List[int] = [] + evt_flgs: List[int] = [] + evt_params: List[int] = [] + + get_rank_pts: int = req.get_trainer_rank_point if req.get_trainer_rank_point else 0 + get_money: int = req.get_money + get_score_pts: int = req.get_score_point if req.get_score_point else 0 + grade_max: int = req.grade_max_num + extra_counter: int = req.extra_counter + evt_reward_get_flg: int = req.event_reward_get_flag + num_continues: int = req.continue_num + total_play_days: int = req.total_play_days + awake_num: int = req.awake_num # ? + use_support_ct: int = req.use_support_num + beat_num: int = req.beat_num # ? + evt_state: int = req.event_state + aid_skill: int = req.aid_skill + last_evt: int = req.last_play_event_id + + battle = req.battle_data + mon = req.pokemon_data + + p = self.data.profile.touch_profile(user_id) + if p is None or not p: + self.data.profile.create_profile(user_id) + + if req.trainer_name_pending is not None and req.trainer_name_pending: # we're saving for the first time + self.data.profile.set_profile_name(user_id, req.trainer_name_pending, req.avatar_gender if req.avatar_gender else None) + + for tut_flg in req.tutorial_progress_flag: + tut_flgs.append(tut_flg) + + self.data.profile.update_profile_tutorial_flags(user_id, tut_flgs) + + for ach_flg in req.achievement_flag: + ach_flgs.append(ach_flg) + + self.data.profile.update_profile_tutorial_flags(user_id, ach_flg) + + for evt_flg in req.event_achievement_flag: + evt_flgs.append(evt_flg) + + for evt_param in req.event_achievement_param: + evt_params.append(evt_param) + + self.data.profile.update_profile_event(user_id, evt_state, evt_flgs, evt_params, req.last_play_event_id) + + for reward in req.reward_data: + self.data.item.add_reward(user_id, reward.get_category_id, reward.get_content_id, reward.get_type_id) + + self.data.profile.add_profile_points(user_id, get_rank_pts, get_money, get_score_pts, grade_max) + + self.data.profile.update_support_team(user_id, 1, req.support_set_1[0], req.support_set_1[1]) + self.data.profile.update_support_team(user_id, 2, req.support_set_2[0], req.support_set_2[1]) + self.data.profile.update_support_team(user_id, 3, req.support_set_3[0], req.support_set_3[1]) + + 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) + self.data.profile.add_pokemon_xp(user_id, mon.char_id, mon.get_pokemon_exp) + + for x in range(len(battle.play_mode)): + self.data.profile.put_pokemon_battle_result( + user_id, + mon.char_id, + PokkenConstants.BATTLE_TYPE(battle.play_mode[x]), + PokkenConstants.BATTLE_RESULT(battle.result[x]) + ) + + self.data.profile.put_stats( + user_id, + battle.ex_ko_num, + battle.wko_num, + battle.timeup_win_num, + battle.cool_ko_num, + battle.perfect_ko_num, + num_continues + ) + + self.data.profile.put_extra( + user_id, + extra_counter, + evt_reward_get_flg, + total_play_days, + awake_num, + use_support_ct, + beat_num, + aid_skill, + last_evt + ) + + + return res.SerializeToString() + + def handle_save_ingame_log(self, data: jackal_pb2.Request) -> bytes: + res = jackal_pb2.Response() + res.result = 1 + res.type = jackal_pb2.MessageType.SAVE_INGAME_LOG + return res.SerializeToString() + + def handle_save_charge(self, data: jackal_pb2.Request) -> bytes: + res = jackal_pb2.Response() + res.result = 1 + res.type = jackal_pb2.MessageType.SAVE_CHARGE + return res.SerializeToString() + + def handle_matching_noop( + self, data: Dict = {}, client_ip: str = "127.0.0.1" + ) -> Dict: + return {} + + def handle_matching_start_matching( + self, data: Dict = {}, client_ip: str = "127.0.0.1" + ) -> Dict: + return {} + + 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", + "A":{ + "pcb_id": data["data"]["must"]["pcb_id"], + "gip": client_ip + }, + "list":[] + } + } + + def handle_matching_stop_matching( + self, data: Dict = {}, client_ip: str = "127.0.0.1" + ) -> Dict: + return {} + + def handle_admission_noop(self, data: Dict, req_ip: str = "127.0.0.1") -> Dict: + return {} + + def handle_admission_joinsession(self, data: Dict, req_ip: str = "127.0.0.1") -> Dict: + self.logger.info(f"Admission: JoinSession from {req_ip}") + return { + 'data': { + "id": 12345678 + } + } diff --git a/titles/pokken/config.py b/titles/pokken/config.py index 1221c08..d3741af 100644 --- a/titles/pokken/config.py +++ b/titles/pokken/config.py @@ -1,37 +1,85 @@ from core.config import CoreConfig -class PokkenServerConfig(): + +class PokkenServerConfig: + def __init__(self, parent_config: "PokkenConfig"): + self.__config = parent_config + + @property + def hostname(self) -> str: + return CoreConfig.get_config_field( + self.__config, "pokken", "server", "hostname", default="localhost" + ) + + @property + def enable(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "pokken", "server", "enable", default=True + ) + + @property + def loglevel(self) -> int: + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "pokken", "server", "loglevel", default="info" + ) + ) + + @property + def auto_register(self) -> bool: + """ + Automatically register users in `aime_user` on first carding in with pokken + if they don't exist already. Set to false to display an error instead. + """ + return CoreConfig.get_config_field( + self.__config, "pokken", "server", "auto_register", default=True + ) + + @property + def enable_matching(self) -> bool: + """ + If global matching should happen + """ + return CoreConfig.get_config_field( + self.__config, "pokken", "server", "enable_matching", default=False + ) + + @property + def stun_server_host(self) -> str: + """ + Hostname of the EXTERNAL stun server the game should connect to. This is not handled by artemis. + """ + return CoreConfig.get_config_field( + self.__config, "pokken", "server", "stun_server_host", default="stunserver.stunprotocol.org" + ) + + @property + def stun_server_port(self) -> int: + """ + Port of the EXTERNAL stun server the game should connect to. This is not handled by artemis. + """ + return CoreConfig.get_config_field( + self.__config, "pokken", "server", "stun_server_port", default=3478 + ) + +class PokkenPortsConfig: def __init__(self, parent_config: "PokkenConfig"): self.__config = parent_config @property - def enable(self) -> bool: - return CoreConfig.get_config_field(self.__config, 'pokken', 'server', 'enable', default=True) + def game(self) -> int: + return CoreConfig.get_config_field( + self.__config, "pokken", "ports", "game", default=9000 + ) @property - def loglevel(self) -> int: - return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'pokken', 'server', 'loglevel', default="info")) - - @property - def hostname(self) -> str: - return CoreConfig.get_config_field(self.__config, 'pokken', 'server', 'hostname', default="localhost") - - @property - def port(self) -> int: - return CoreConfig.get_config_field(self.__config, 'pokken', 'server', 'port', default=9000) - - @property - def port_matching(self) -> int: - return CoreConfig.get_config_field(self.__config, 'pokken', 'server', 'port', default=9001) + def admission(self) -> int: + return CoreConfig.get_config_field( + self.__config, "pokken", "ports", "admission", default=9001 + ) - @property - def ssl_cert(self) -> str: - return CoreConfig.get_config_field(self.__config, 'pokken', 'server', 'ssl_cert', default="cert/pokken.crt") - - @property - def ssl_key(self) -> str: - return CoreConfig.get_config_field(self.__config, 'pokken', 'server', 'ssl_key', default="cert/pokken.key") class PokkenConfig(dict): def __init__(self) -> None: self.server = PokkenServerConfig(self) + self.ports = PokkenPortsConfig(self) diff --git a/titles/pokken/const.py b/titles/pokken/const.py index 203925c..9fa3b06 100644 --- a/titles/pokken/const.py +++ b/titles/pokken/const.py @@ -1,12 +1,26 @@ -class PokkenConstants(): +from enum import Enum + + +class PokkenConstants: GAME_CODE = "SDAK" CONFIG_NAME = "pokken.yaml" VER_POKKEN = 0 - VERSION_NAMES = ("Pokken Tournament") + VERSION_NAMES = "Pokken Tournament" + + class BATTLE_TYPE(Enum): + TUTORIAL = 1 + AI = 2 + LAN = 3 + WAN = 4 + TUTORIAL_3 = 7 + + class BATTLE_RESULT(Enum): + WIN = 1 + LOSS = 2 @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] diff --git a/titles/pokken/database.py b/titles/pokken/database.py new file mode 100644 index 0000000..272cfd8 --- /dev/null +++ b/titles/pokken/database.py @@ -0,0 +1,14 @@ +from core.data import Data +from core.config import CoreConfig + +from .schema import * + + +class PokkenData(Data): + def __init__(self, cfg: CoreConfig) -> None: + super().__init__(cfg) + + self.profile = PokkenProfileData(cfg, self.session) + self.match = PokkenMatchData(cfg, self.session) + self.item = PokkenItemData(cfg, self.session) + self.static = PokkenStaticData(cfg, self.session) diff --git a/titles/pokken/frontend.py b/titles/pokken/frontend.py new file mode 100644 index 0000000..af344dc --- /dev/null +++ b/titles/pokken/frontend.py @@ -0,0 +1,39 @@ +import yaml +import jinja2 +from twisted.web.http import Request +from os import path +from twisted.web.server import Session + +from core.frontend import FE_Base, IUserSession +from core.config import CoreConfig +from .database import PokkenData +from .config import PokkenConfig +from .const import PokkenConstants + + +class PokkenFrontend(FE_Base): + def __init__( + self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str + ) -> None: + super().__init__(cfg, environment) + self.data = PokkenData(cfg) + self.game_cfg = PokkenConfig() + if path.exists(f"{cfg_dir}/{PokkenConstants.CONFIG_NAME}"): + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{PokkenConstants.CONFIG_NAME}")) + ) + self.nav_name = "Pokken" + + def render_GET(self, request: Request) -> bytes: + template = self.environment.get_template( + "titles/pokken/frontend/pokken_index.jinja" + ) + + sesh: Session = request.getSession() + usr_sesh = IUserSession(sesh) + + return template.render( + title=f"{self.core_config.server.name} | {self.nav_name}", + game_list=self.environment.globals["game_list"], + sesh=vars(usr_sesh) + ).encode("utf-16") diff --git a/titles/pokken/frontend/pokken_index.jinja b/titles/pokken/frontend/pokken_index.jinja new file mode 100644 index 0000000..446893a --- /dev/null +++ b/titles/pokken/frontend/pokken_index.jinja @@ -0,0 +1,4 @@ +{% extends "core/frontend/index.jinja" %} +{% block content %} +

Pokken

+{% endblock content %} \ No newline at end of file diff --git a/titles/pokken/index.py b/titles/pokken/index.py index d0b3efd..30b6617 100644 --- a/titles/pokken/index.py +++ b/titles/pokken/index.py @@ -1,73 +1,184 @@ +from typing import Tuple, List, Dict from twisted.web.http import Request -from twisted.web import resource, server -from twisted.internet import reactor, endpoints +from twisted.web import resource +from twisted.internet import reactor +import json, ast +from datetime import datetime import yaml import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler -from titles.pokken.proto import jackal_pb2 -from google.protobuf import text_format +import inflection +from os import path +from google.protobuf.message import DecodeError -from core.config import CoreConfig -from titles.pokken.config import PokkenConfig -from titles.pokken.base import PokkenBase +from core import CoreConfig, Utils +from core.title import BaseServlet +from .config import PokkenConfig +from .base import PokkenBase +from .const import PokkenConstants +from .proto import jackal_pb2 +from .services import PokkenAdmissionFactory -class PokkenServlet(resource.Resource): + +class PokkenServlet(BaseServlet): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: - self.isLeaf = True - self.core_cfg = core_cfg + super().__init__(core_cfg, cfg_dir) self.config_dir = cfg_dir self.game_cfg = PokkenConfig() - self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/pokken.yaml"))) + if path.exists(f"{cfg_dir}/pokken.yaml"): + self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/pokken.yaml"))) self.logger = logging.getLogger("pokken") if not hasattr(self.logger, "inited"): log_fmt_str = "[%(asctime)s] Pokken | %(levelname)s | %(message)s" log_fmt = logging.Formatter(log_fmt_str) - fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.core_cfg.server.log_dir, "pokken"), encoding='utf8', - when="d", backupCount=10) + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.core_cfg.server.log_dir, "pokken"), + encoding="utf8", + when="d", + backupCount=10, + ) fileHandler.setFormatter(log_fmt) - + consoleHandler = logging.StreamHandler() consoleHandler.setFormatter(log_fmt) self.logger.addHandler(fileHandler) self.logger.addHandler(consoleHandler) - + self.logger.setLevel(self.game_cfg.server.loglevel) - coloredlogs.install(level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str) + coloredlogs.install( + level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str + ) self.logger.inited = True self.base = PokkenBase(core_cfg, self.game_cfg) - - def setup(self): - if self.game_cfg.server.enable: - if self.core_cfg.server.is_develop: - endpoints.serverFromString(reactor, f"ssl:{self.game_cfg.server.port}"\ - f":interface={self.game_cfg.server.hostname}:privateKey={self.game_cfg.server.ssl_key}:"\ - f"certKey={self.game_cfg.server.ssl_cert}")\ - .listen(server.Site(PokkenServlet(self.core_cfg, self.config_dir))) - else: - endpoints.serverFromString(reactor, f"tcp:{self.game_cfg.server.port}"\ - f":interface={self.game_cfg.server.hostname}")\ - .listen(server.Site(PokkenServlet(self.core_cfg, self.config_dir))) + @classmethod + def is_game_enabled(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> bool: + game_cfg = PokkenConfig() - self.logger.info(f"Pokken title server ready on port {self.game_cfg.server.port}") + if path.exists(f"{cfg_dir}/{PokkenConstants.CONFIG_NAME}"): + game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{PokkenConstants.CONFIG_NAME}")) + ) + + if not game_cfg.server.enable: + return False + + return True - def render_POST(self, request: Request, version: int, endpoints: str) -> bytes: - req_url = request.uri.decode() - if req_url == "/matching": - self.logger.info("Matching request") + def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: + return ( + [], + [ + ("render_POST", "/pokken/", {}), + ("handle_matching", "/pokken/matching", {}), + ] + ) + + def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: + if self.game_cfg.ports.game != 443: + return ( + f"https://{self.game_cfg.server.hostname}:{self.game_cfg.ports.game}/pokken/", + f"{self.game_cfg.server.hostname}/pokken/", + ) + return ( + f"https://{self.game_cfg.server.hostname}/pokken/", + f"{self.game_cfg.server.hostname}/pokken/", + ) + + def get_mucha_info(self, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str]: + game_cfg = PokkenConfig() + + if path.exists(f"{cfg_dir}/{PokkenConstants.CONFIG_NAME}"): + game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{PokkenConstants.CONFIG_NAME}")) + ) + + if not game_cfg.server.enable: + return (False, "") + + return (True, "PKF1") + + def setup(self) -> None: + if self.game_cfg.server.enable_matching: + reactor.listenTCP( + self.game_cfg.ports.admission, PokkenAdmissionFactory(self.core_cfg, self.game_cfg) + ) + + def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes: + content = request.content.getvalue() + if content == b"": + self.logger.info("Empty request") + return b"" pokken_request = jackal_pb2.Request() - pokken_request.ParseFromString(request.content.getvalue()) - endpoint = jackal_pb2.MessageType(pokken_request.type).name.lower() - - self.logger.info(f"{endpoint} request") + try: + pokken_request.ParseFromString(content) + except DecodeError as e: + self.logger.warning(f"{e} {content}") + return b"" + + endpoint = jackal_pb2.MessageType.DESCRIPTOR.values_by_number[ + pokken_request.type + ].name.lower() + + self.logger.debug(pokken_request) handler = getattr(self.base, f"handle_{endpoint}", None) if handler is None: - self.logger.warn(f"No handler found for message type {endpoint}") + self.logger.warning(f"No handler found for message type {endpoint}") return self.base.handle_noop(pokken_request) - return handler(pokken_request) + + self.logger.info(f"{endpoint} request from {Utils.get_ip_addr(request)}") + + ret = handler(pokken_request) + return ret + + def handle_matching(self, request: Request, game_code: str, matchers: Dict) -> bytes: + if not self.game_cfg.server.enable_matching: + return b"" + + content = request.content.getvalue() + client_ip = Utils.get_ip_addr(request) + + if content is None or content == b"": + self.logger.info("Empty matching request") + return json.dumps(self.base.handle_matching_noop()).encode() + + json_content = ast.literal_eval( + content.decode() + .replace("null", "None") + .replace("true", "True") + .replace("false", "False") + ) + self.logger.info(f"Matching {json_content['call']} request") + self.logger.debug(json_content) + + handler = getattr( + self.base, + f"handle_matching_{inflection.underscore(json_content['call'])}", + None, + ) + if handler is None: + self.logger.warning( + f"No handler found for message type {json_content['call']}" + ) + return json.dumps(self.base.handle_matching_noop()).encode() + + ret = handler(json_content, client_ip) + + if ret is None: + ret = {} + if "result" not in ret: + ret["result"] = "true" + if "data" not in ret: + ret["data"] = {} + if "timestamp" not in ret: + ret["timestamp"] = int(datetime.now().timestamp() * 1000) + + self.logger.debug(f"Response {ret}") + + return json.dumps(ret).encode() diff --git a/titles/pokken/proto/jackal_pb2.py b/titles/pokken/proto/jackal_pb2.py index 782403d..9a2e30e 100644 --- a/titles/pokken/proto/jackal_pb2.py +++ b/titles/pokken/proto/jackal_pb2.py @@ -6,134 +6,134 @@ from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database + # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0cjackal.proto\x12\x0fjackal.protobuf\"\xae\x0b\n\x07Request\x12*\n\x04type\x18\x01 \x02(\x0e\x32\x1c.jackal.protobuf.MessageType\x12.\n\x04noop\x18\x02 \x01(\x0b\x32 .jackal.protobuf.NoopRequestData\x12.\n\x04ping\x18\x03 \x01(\x0b\x32 .jackal.protobuf.PingRequestData\x12=\n\x0cregister_pcb\x18\x04 \x01(\x0b\x32\'.jackal.protobuf.RegisterPcbRequestData\x12\x35\n\x08save_ads\x18\x05 \x01(\x0b\x32#.jackal.protobuf.SaveAdsRequestData\x12\x46\n\x11\x63heck_access_code\x18\x06 \x01(\x0b\x32+.jackal.protobuf.CheckAccessCodeRequestData\x12\x46\n\x11set_bnpassid_lock\x18\x07 \x01(\x0b\x32+.jackal.protobuf.SetBnpassIdLockRequestData\x12\x37\n\tload_user\x18\x08 \x01(\x0b\x32$.jackal.protobuf.LoadUserRequestData\x12\x37\n\tsave_user\x18\n \x01(\x0b\x32$.jackal.protobuf.SaveUserRequestData\x12\x43\n\x0f\x63heck_diagnosis\x18\x0b \x01(\x0b\x32*.jackal.protobuf.CheckDiagnosisRequestData\x12\x42\n\x0fsave_client_log\x18\x0c \x01(\x0b\x32).jackal.protobuf.SaveClientLogRequestData\x12L\n\x14pre_load_information\x18\r \x01(\x0b\x32..jackal.protobuf.PreLoadInformationRequestData\x12\x45\n\x10load_information\x18\x0e \x01(\x0b\x32+.jackal.protobuf.LoadInformationRequestData\x12\x42\n\x0fpre_save_replay\x18\x0f \x01(\x0b\x32).jackal.protobuf.PreSaveReplayRequestData\x12;\n\x0bsave_replay\x18\x10 \x01(\x0b\x32&.jackal.protobuf.SaveReplayRequestData\x12;\n\x0bsave_charge\x18\x11 \x01(\x0b\x32&.jackal.protobuf.SaveChargeRequestData\x12?\n\rcheck_ranking\x18\x12 \x01(\x0b\x32(.jackal.protobuf.CheckRankingRequestData\x12=\n\x0cload_ranking\x18\x13 \x01(\x0b\x32\'.jackal.protobuf.LoadRankingRequestData\x12\x42\n\x0fsave_ingame_log\x18\x14 \x01(\x0b\x32).jackal.protobuf.SaveInGameLogRequestData\x12[\n\x1cpre_load_information_attract\x18\x15 \x01(\x0b\x32\x35.jackal.protobuf.PreLoadInformationAttractRequestData\x12T\n\x18load_information_attract\x18\x16 \x01(\x0b\x32\x32.jackal.protobuf.LoadInformationAttractRequestData\x12L\n\x14load_client_settings\x18\x17 \x01(\x0b\x32..jackal.protobuf.LoadClientSettingsRequestData\"\xd4\x0b\n\x08Response\x12*\n\x04type\x18\x01 \x02(\x0e\x32\x1c.jackal.protobuf.MessageType\x12\x0e\n\x06result\x18\x02 \x02(\r\x12/\n\x04noop\x18\x03 \x01(\x0b\x32!.jackal.protobuf.NoopResponseData\x12/\n\x04ping\x18\x04 \x01(\x0b\x32!.jackal.protobuf.PingResponseData\x12>\n\x0cregister_pcb\x18\x05 \x01(\x0b\x32(.jackal.protobuf.RegisterPcbResponseData\x12\x36\n\x08save_ads\x18\x06 \x01(\x0b\x32$.jackal.protobuf.SaveAdsResponseData\x12G\n\x11\x63heck_access_code\x18\x07 \x01(\x0b\x32,.jackal.protobuf.CheckAccessCodeResponseData\x12G\n\x11set_bnpassid_lock\x18\x08 \x01(\x0b\x32,.jackal.protobuf.SetBnpassIdLockResponseData\x12\x38\n\tload_user\x18\t \x01(\x0b\x32%.jackal.protobuf.LoadUserResponseData\x12\x38\n\tsave_user\x18\x0b \x01(\x0b\x32%.jackal.protobuf.SaveUserResponseData\x12\x44\n\x0f\x63heck_diagnosis\x18\x0c \x01(\x0b\x32+.jackal.protobuf.CheckDiagnosisResponseData\x12\x43\n\x0fsave_client_log\x18\r \x01(\x0b\x32*.jackal.protobuf.SaveClientLogResponseData\x12M\n\x14pre_load_information\x18\x0e \x01(\x0b\x32/.jackal.protobuf.PreLoadInformationResponseData\x12\x46\n\x10load_information\x18\x0f \x01(\x0b\x32,.jackal.protobuf.LoadInformationResponseData\x12\x43\n\x0fpre_save_replay\x18\x10 \x01(\x0b\x32*.jackal.protobuf.PreSaveReplayResponseData\x12<\n\x0bsave_replay\x18\x11 \x01(\x0b\x32\'.jackal.protobuf.SaveReplayResponseData\x12<\n\x0bsave_charge\x18\x12 \x01(\x0b\x32\'.jackal.protobuf.SaveChargeResponseData\x12@\n\rcheck_ranking\x18\x13 \x01(\x0b\x32).jackal.protobuf.CheckRankingResponseData\x12>\n\x0cload_ranking\x18\x14 \x01(\x0b\x32(.jackal.protobuf.LoadRankingResponseData\x12\x43\n\x0fsave_ingame_log\x18\x15 \x01(\x0b\x32*.jackal.protobuf.SaveInGameLogResponseData\x12\\\n\x1cpre_load_information_attract\x18\x16 \x01(\x0b\x32\x36.jackal.protobuf.PreLoadInformationAttractResponseData\x12U\n\x18load_information_attract\x18\x17 \x01(\x0b\x32\x33.jackal.protobuf.LoadInformationAttractResponseData\x12M\n\x14load_client_settings\x18\x18 \x01(\x0b\x32/.jackal.protobuf.LoadClientSettingsResponseData\"1\n\x0fNoopRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\"\x12\n\x10NoopResponseData\"1\n\x0fPingRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\"\x12\n\x10PingResponseData\"\xce\x02\n\x16RegisterPcbRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x10\n\x08pcb_type\x18\x02 \x02(\r\x12\x15\n\rlocation_name\x18\x03 \x02(\t\x12\x19\n\x11location_nickname\x18\x04 \x02(\t\x12\x11\n\tpref_code\x18\x05 \x01(\r\x12\r\n\x05\x61\x64\x64r0\x18\x06 \x01(\t\x12\r\n\x05\x61\x64\x64r1\x18\x07 \x01(\t\x12\r\n\x05\x61\x64\x64r2\x18\x08 \x01(\t\x12\r\n\x05\x61\x64\x64r3\x18\t \x01(\t\x12\x0e\n\x06loc_id\x18\n \x02(\t\x12\x14\n\x0c\x63ountry_code\x18\x0b \x02(\t\x12\x13\n\x0bregion_code\x18\x0c \x02(\r\x12\r\n\x05karma\x18\r \x02(\x05\x12\x0f\n\x07game_id\x18\x0e \x02(\t\x12\x10\n\x08game_ver\x18\x0f \x02(\t\x12\x10\n\x08\x64isk_ver\x18\x10 \x02(\t\x12\x12\n\nutc_offset\x18\x11 \x02(\x05\"\x85\x01\n\x17RegisterPcbResponseData\x12\x13\n\x0bserver_time\x18\x01 \x02(\r\x12\x15\n\roperate_start\x18\x02 \x01(\t\x12\x13\n\x0boperate_end\x18\x03 \x01(\t\x12\x13\n\x0b\x62np_baseuri\x18\x04 \x01(\t\x12\x14\n\x0c\x62iwa_setting\x18\x05 \x02(\t\"\xd7\x02\n\x12SaveAdsRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x17\n\x0f\x61\x64s_start_count\x18\x03 \x02(\r\x12\x16\n\x0e\x61\x64s_coin_count\x18\x04 \x02(\r\x12\x19\n\x11\x61\x64s_service_count\x18\x05 \x02(\r\x12\x1a\n\x12\x61\x64s_freeplay_count\x18\x06 \x02(\r\x12\x1a\n\x12\x61\x64s_operation_days\x18\x07 \x02(\r\x12\x1a\n\x12\x61\x64s_power_on_count\x18\x08 \x02(\r\x12\x46\n\rads_play_time\x18\t \x03(\x0b\x32/.jackal.protobuf.SaveAdsRequestData.AdsPlayTime\x1a\x39\n\x0b\x41\x64sPlayTime\x12\x12\n\npokemon_id\x18\x65 \x02(\r\x12\x16\n\x0e\x63harplay_count\x18\x66 \x02(\r\"\x15\n\x13SaveAdsResponseData\"M\n\x1a\x43heckAccessCodeRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x0f\n\x07\x63hip_id\x18\x03 \x02(\t\"M\n\x1b\x43heckAccessCodeResponseData\x12\x19\n\x11\x63ommidserv_result\x18\x01 \x02(\r\x12\x13\n\x0b\x61\x63\x63\x65ss_code\x18\x02 \x02(\t\"\xa4\x01\n\x1aSetBnpassIdLockRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x13\n\x0b\x62\x61napass_id\x18\x03 \x02(\r\x12\x13\n\x0b\x64\x65vice_type\x18\x04 \x02(\r\x12\x0f\n\x07\x63hip_id\x18\x05 \x02(\t\x12\x13\n\x0b\x61\x63\x63\x65ss_code\x18\x06 \x01(\t\x12\x16\n\x0e\x63\x61rd_lock_time\x18\x07 \x02(\r\"\x1d\n\x1bSetBnpassIdLockResponseData\"\x85\x01\n\x13LoadUserRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x13\n\x0b\x64\x65vice_type\x18\x03 \x02(\r\x12\x0f\n\x07\x63hip_id\x18\x04 \x02(\t\x12\x13\n\x0b\x61\x63\x63\x65ss_code\x18\x05 \x01(\t\x12\x13\n\x0b\x63\x61rd_status\x18\x06 \x02(\x08\"\xfc\x12\n\x14LoadUserResponseData\x12\x19\n\x11\x63ommidserv_result\x18\x01 \x02(\r\x12\x11\n\tload_hash\x18\x02 \x02(\r\x12\x17\n\x0f\x63\x61rdlock_status\x18\x03 \x02(\x08\x12\x13\n\x0b\x62\x61napass_id\x18\x04 \x02(\r\x12\x13\n\x0b\x61\x63\x63\x65ss_code\x18\x05 \x01(\t\x12\x15\n\rnew_card_flag\x18\x06 \x02(\x08\x12\x1e\n\x16precedent_release_flag\x18\x07 \x02(\r\x12\x18\n\x10navi_newbie_flag\x18\x08 \x02(\x08\x12\x18\n\x10navi_enable_flag\x18\t \x02(\x08\x12\x18\n\x10pad_vibrate_flag\x18\n \x02(\x08\x12\x18\n\x10home_region_code\x18\x0b \x02(\r\x12\x15\n\rhome_loc_name\x18\x0c \x02(\t\x12\x11\n\tpref_code\x18\r \x02(\r\x12\x14\n\x0ctrainer_name\x18\x0e \x01(\t\x12\x1a\n\x12trainer_rank_point\x18\x0f \x02(\r\x12\x0e\n\x06wallet\x18\x10 \x02(\r\x12\x13\n\x0b\x66ight_money\x18\x11 \x02(\r\x12\x13\n\x0bscore_point\x18\x12 \x02(\r\x12\x15\n\rgrade_max_num\x18\x13 \x02(\r\x12\x15\n\rextra_counter\x18\x14 \x01(\r\x12\x1e\n\x16tutorial_progress_flag\x18\x15 \x03(\r\x12\x17\n\x0ftotal_play_days\x18\x16 \x02(\r\x12\x16\n\x0eplay_date_time\x18\x17 \x02(\r\x12\x1a\n\x12lucky_box_fail_num\x18\x18 \x02(\r\x12\x1d\n\x15\x65vent_reward_get_flag\x18\x19 \x02(\r\x12\x14\n\x0crank_pvp_all\x18\x1a \x02(\r\x12\x14\n\x0crank_pvp_loc\x18\x1b \x02(\r\x12\x14\n\x0crank_cpu_all\x18\x1c \x02(\r\x12\x14\n\x0crank_cpu_loc\x18\x1d \x02(\r\x12\x12\n\nrank_event\x18\x1e \x02(\r\x12\x11\n\tawake_num\x18\x1f \x02(\r\x12\x17\n\x0fuse_support_num\x18 \x02(\r\x12\x16\n\x0erankmatch_flag\x18! \x02(\r\x12\x1a\n\x12rankmatch_progress\x18\" \x03(\r\x12\x15\n\rrankmatch_max\x18# \x01(\r\x12\x19\n\x11rankmatch_success\x18$ \x01(\r\x12\x10\n\x08\x62\x65\x61t_num\x18% \x01(\x05\x12\x15\n\rtitle_text_id\x18& \x02(\r\x12\x16\n\x0etitle_plate_id\x18\' \x02(\r\x12\x1b\n\x13title_decoration_id\x18( \x02(\r\x12\x1c\n\x14support_pokemon_list\x18) \x03(\r\x12\x15\n\rsupport_set_1\x18* \x03(\r\x12\x15\n\rsupport_set_2\x18+ \x03(\r\x12\x15\n\rsupport_set_3\x18, \x03(\r\x12\x14\n\x0cnavi_trainer\x18- \x02(\r\x12\x17\n\x0fnavi_version_id\x18. \x02(\r\x12\x16\n\x0e\x61id_skill_list\x18/ \x03(\r\x12\x11\n\taid_skill\x18\x30 \x02(\r\x12\x17\n\x0f\x63omment_text_id\x18\x31 \x02(\r\x12\x17\n\x0f\x63omment_word_id\x18\x32 \x02(\r\x12\x1a\n\x12latest_use_pokemon\x18\x33 \x02(\r\x12\x11\n\tex_ko_num\x18\x34 \x02(\r\x12\x0f\n\x07wko_num\x18\x35 \x02(\r\x12\x16\n\x0etimeup_win_num\x18\x36 \x02(\r\x12\x13\n\x0b\x63ool_ko_num\x18\x37 \x02(\r\x12\x16\n\x0eperfect_ko_num\x18\x38 \x02(\r\x12\x13\n\x0brecord_flag\x18\x39 \x02(\r\x12\x1c\n\x14site_register_status\x18: \x02(\r\x12\x14\n\x0c\x63ontinue_num\x18; \x02(\r\x12\x18\n\x10\x61\x63hievement_flag\x18< \x03(\r\x12\x13\n\x0b\x61vatar_body\x18= \x01(\r\x12\x15\n\ravatar_gender\x18> \x01(\r\x12\x19\n\x11\x61vatar_background\x18? \x01(\r\x12\x13\n\x0b\x61vatar_head\x18@ \x01(\r\x12\x1a\n\x12\x61vatar_battleglass\x18\x41 \x01(\r\x12\x14\n\x0c\x61vatar_face0\x18\x42 \x01(\r\x12\x14\n\x0c\x61vatar_face1\x18\x43 \x01(\r\x12\x14\n\x0c\x61vatar_face2\x18\x44 \x01(\r\x12\x16\n\x0e\x61vatar_bodyall\x18\x45 \x01(\r\x12\x13\n\x0b\x61vatar_wear\x18\x46 \x01(\r\x12\x18\n\x10\x61vatar_accessory\x18G \x01(\r\x12\x14\n\x0c\x61vatar_stamp\x18H \x01(\r\x12G\n\x0cpokemon_data\x18I \x03(\x0b\x32\x31.jackal.protobuf.LoadUserResponseData.PokemonData\x12\x13\n\x0b\x65vent_state\x18J \x02(\r\x12\x10\n\x08\x65vent_id\x18K \x02(\r\x12\x1e\n\x16sp_bonus_category_id_1\x18L \x02(\r\x12\x1c\n\x14sp_bonus_key_value_1\x18M \x02(\r\x12\x1e\n\x16sp_bonus_category_id_2\x18N \x02(\r\x12\x1c\n\x14sp_bonus_key_value_2\x18O \x02(\r\x12\x1a\n\x12last_play_event_id\x18P \x01(\r\x12\x1e\n\x16\x65vent_achievement_flag\x18Q \x03(\r\x12\x1f\n\x17\x65vent_achievement_param\x18R \x03(\r\x1a\xf0\x02\n\x0bPokemonData\x12\x0f\n\x07\x63har_id\x18\x65 \x02(\r\x12\x1c\n\x14illustration_book_no\x18\x66 \x02(\r\x12\x13\n\x0bpokemon_exp\x18g \x02(\r\x12\x19\n\x11\x62\x61ttle_num_vs_wan\x18h \x02(\r\x12\x12\n\nwin_vs_wan\x18i \x02(\r\x12\x19\n\x11\x62\x61ttle_num_vs_lan\x18j \x02(\r\x12\x12\n\nwin_vs_lan\x18k \x02(\r\x12\x19\n\x11\x62\x61ttle_num_vs_cpu\x18l \x02(\r\x12\x0f\n\x07win_cpu\x18m \x02(\r\x12\x1f\n\x17\x62\x61ttle_all_num_tutorial\x18n \x02(\r\x12\x1b\n\x13\x62\x61ttle_num_tutorial\x18o \x02(\r\x12\x14\n\x0c\x62p_point_atk\x18p \x02(\r\x12\x14\n\x0c\x62p_point_res\x18q \x02(\r\x12\x14\n\x0c\x62p_point_def\x18r \x02(\r\x12\x13\n\x0b\x62p_point_sp\x18s \x02(\r\"\x98\x0c\n\x13SaveUserRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x13\n\x0b\x62\x61napass_id\x18\x03 \x02(\r\x12\x1e\n\x16get_trainer_rank_point\x18\x04 \x01(\x05\x12\x11\n\tget_money\x18\x05 \x02(\r\x12\x17\n\x0fget_score_point\x18\x06 \x01(\r\x12\x15\n\rgrade_max_num\x18\x07 \x01(\r\x12\x15\n\rextra_counter\x18\x08 \x01(\r\x12\x1e\n\x16tutorial_progress_flag\x18\t \x03(\r\x12\x1d\n\x15\x65vent_reward_get_flag\x18\n \x01(\r\x12\x14\n\x0c\x63ontinue_num\x18\x0b \x02(\r\x12\x17\n\x0ftotal_play_days\x18\x0c \x02(\r\x12\x18\n\x10\x61\x63hievement_flag\x18\r \x03(\r\x12\x11\n\tawake_num\x18\x0e \x02(\r\x12\x17\n\x0fuse_support_num\x18\x0f \x02(\r\x12\x16\n\x0erankmatch_flag\x18\x10 \x02(\r\x12\x1a\n\x12rank_match_process\x18\x11 \x03(\r\x12\x16\n\x0erank_match_max\x18\x12 \x01(\r\x12\x1a\n\x12rank_match_success\x18\x13 \x01(\r\x12\x10\n\x08\x62\x65\x61t_num\x18\x14 \x01(\x05\x12\x15\n\rsupport_set_1\x18\x15 \x03(\r\x12\x15\n\rsupport_set_2\x18\x16 \x03(\r\x12\x15\n\rsupport_set_3\x18\x17 \x03(\r\x12\x44\n\x0b\x62\x61ttle_data\x18\x18 \x02(\x0b\x32/.jackal.protobuf.SaveUserRequestData.BattleData\x12\x46\n\x0cpokemon_data\x18\x19 \x02(\x0b\x32\x30.jackal.protobuf.SaveUserRequestData.PokemonData\x12\x1c\n\x14trainer_name_pending\x18\x1a \x01(\t\x12\x15\n\ravatar_gender\x18\x1b \x01(\r\x12\x15\n\rcontinue_flag\x18\x1c \x02(\x08\x12\x14\n\x0creq_sendtime\x18\x1d \x02(\r\x12\x15\n\rplay_all_time\x18\x1e \x02(\r\x12\x11\n\tload_hash\x18\x1f \x02(\r\x12\x44\n\x0breward_data\x18 \x03(\x0b\x32/.jackal.protobuf.SaveUserRequestData.RewardData\x12\x13\n\x0b\x65vent_state\x18! \x01(\r\x12\x11\n\taid_skill\x18\" \x01(\r\x12\x1a\n\x12last_play_event_id\x18# \x01(\r\x12\x1e\n\x16\x65vent_achievement_flag\x18$ \x03(\r\x12\x1f\n\x17\x65vent_achievement_param\x18% \x03(\r\x1a\xec\x01\n\nBattleData\x12\x11\n\tplay_mode\x18\x65 \x03(\r\x12\x0e\n\x06result\x18\x66 \x03(\r\x12\x11\n\tex_ko_num\x18g \x02(\r\x12\x0f\n\x07wko_num\x18h \x02(\r\x12\x16\n\x0etimeup_win_num\x18i \x02(\r\x12\x13\n\x0b\x63ool_ko_num\x18j \x02(\r\x12\x16\n\x0eperfect_ko_num\x18k \x02(\r\x12\x10\n\x08use_navi\x18l \x02(\r\x12\x16\n\x0euse_navi_cloth\x18m \x02(\r\x12\x15\n\ruse_aid_skill\x18n \x02(\r\x12\x11\n\tplay_date\x18o \x02(\r\x1a\xb3\x01\n\x0bPokemonData\x12\x10\n\x07\x63har_id\x18\xc9\x01 \x02(\r\x12\x1d\n\x14illustration_book_no\x18\xca\x01 \x02(\r\x12\x18\n\x0fget_pokemon_exp\x18\xcb\x01 \x02(\r\x12\x15\n\x0c\x62p_point_atk\x18\xcc\x01 \x02(\r\x12\x15\n\x0c\x62p_point_res\x18\xcd\x01 \x02(\r\x12\x15\n\x0c\x62p_point_def\x18\xce\x01 \x02(\r\x12\x14\n\x0b\x62p_point_sp\x18\xcf\x01 \x02(\r\x1aU\n\nRewardData\x12\x18\n\x0fget_category_id\x18\xad\x02 \x02(\r\x12\x17\n\x0eget_content_id\x18\xae\x02 \x02(\r\x12\x14\n\x0bget_type_id\x18\xaf\x02 \x02(\r\"\x16\n\x14SaveUserResponseData\";\n\x19\x43heckDiagnosisRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\"\x95\x02\n\x1a\x43heckDiagnosisResponseData\x12Q\n\x0e\x64iagnosis_data\x18\x01 \x03(\x0b\x32\x39.jackal.protobuf.CheckDiagnosisResponseData.DiagnosisData\x1a\xa3\x01\n\rDiagnosisData\x12\x14\n\x0crequest_type\x18\x65 \x02(\r\x12\x17\n\x0f\x63onnect_timeout\x18\x66 \x02(\r\x12\x14\n\x0csend_timeout\x18g \x02(\r\x12\x17\n\x0freceive_timeout\x18h \x02(\r\x12\x1c\n\x14retry_time_of_number\x18i \x02(\r\x12\x16\n\x0eretry_interval\x18j \x02(\r\"\xf4\x01\n\x18SaveClientLogRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x11\n\tserial_id\x18\x03 \x02(\r\x12\x14\n\x0creq_sendtime\x18\x04 \x02(\r\x12\r\n\x05karma\x18\x05 \x02(\x05\x12\x14\n\x0crequest_type\x18\x06 \x02(\r\x12\x1f\n\x17request_number_of_times\x18\x07 \x02(\r\x12\x1f\n\x17timeout_number_of_times\x18\x08 \x02(\r\x12\x11\n\tretry_max\x18\t \x02(\r\x12\x15\n\rresponse_time\x18\n \x02(\r\"\x1b\n\x19SaveClientLogResponseData\"V\n\x1dPreLoadInformationRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x15\n\rinfo_small_id\x18\x03 \x02(\r\"\xc9\x01\n\x1ePreLoadInformationResponseData\x12\x15\n\rinfo_small_id\x18\x01 \x02(\r\x12\x13\n\x0bregion_code\x18\x02 \x02(\r\x12\x12\n\nsession_id\x18\x03 \x02(\r\x12\x13\n\x0b\x62lock_total\x18\x04 \x02(\r\x12\x12\n\nblock_size\x18\x05 \x02(\r\x12\x10\n\x08interval\x18\x06 \x02(\r\x12\x16\n\x0einfo_data_size\x18\x07 \x02(\r\x12\x14\n\x0cinfo_data_id\x18\x08 \x02(\r\"v\n\x1aLoadInformationRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x15\n\rinfo_small_id\x18\x03 \x02(\r\x12\x12\n\nsession_id\x18\x04 \x02(\r\x12\r\n\x05\x62lock\x18\x05 \x02(\r\"\x96\x01\n\x1bLoadInformationResponseData\x12\x15\n\rinfo_small_id\x18\x01 \x02(\r\x12\x12\n\nstart_date\x18\x02 \x02(\r\x12\x10\n\x08\x65nd_date\x18\x03 \x02(\r\x12\r\n\x05\x62lock\x18\x04 \x02(\r\x12\x13\n\x0b\x62lock_total\x18\x05 \x02(\r\x12\x16\n\x0einfo_data_body\x18\x06 \x02(\x0c\"\x80\x01\n\x18PreSaveReplayRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x13\n\x0bregion_code\x18\x03 \x02(\r\x12\x15\n\rcategory_code\x18\x04 \x02(\r\x12\x18\n\x10replay_data_size\x18\x05 \x02(\r\"j\n\x19PreSaveReplayResponseData\x12\x12\n\nsession_id\x18\x01 \x02(\r\x12\x13\n\x0b\x62lock_total\x18\x02 \x02(\r\x12\x12\n\nblock_size\x18\x03 \x02(\r\x12\x10\n\x08interval\x18\x04 \x02(\r\"\x82\x02\n\x15SaveReplayRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x13\n\x0b\x62\x61napass_id\x18\x03 \x02(\r\x12\x12\n\npokemon_id\x18\x04 \x02(\r\x12\x17\n\x0ftrainer_rank_id\x18\x05 \x02(\r\x12\x13\n\x0bregion_code\x18\x06 \x02(\r\x12\x12\n\nsession_id\x18\x07 \x02(\r\x12\r\n\x05\x62lock\x18\x08 \x02(\r\x12\x1b\n\x13transfer_completion\x18\t \x02(\r\x12\x18\n\x10replay_data_size\x18\n \x02(\r\x12\x18\n\x10replay_data_body\x18\x0b \x02(\x0c\"\x18\n\x16SaveReplayResponseData\"\x8d\x01\n\x15SaveChargeRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x0f\n\x07game_id\x18\x03 \x02(\t\x12\x19\n\x11\x63harge_data_index\x18\x04 \x02(\t\x12\x13\n\x0b\x63harge_type\x18\x05 \x02(\r\x12\x13\n\x0b\x63harge_time\x18\x06 \x02(\r\"N\n\x16SaveChargeResponseData\x12\x19\n\x11\x63harge_error_code\x18\x01 \x02(\r\x12\x19\n\x11\x63harge_data_index\x18\x02 \x02(\t\"u\n\x17\x43heckRankingRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x13\n\x0bregion_code\x18\x03 \x02(\r\x12\x12\n\nranking_id\x18\x04 \x02(\r\x12\x11\n\ttimestamp\x18\x05 \x02(\r\".\n\x18\x43heckRankingResponseData\x12\x12\n\nranking_id\x18\x01 \x02(\r\"a\n\x16LoadRankingRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x13\n\x0bregion_code\x18\x03 \x02(\r\x12\x12\n\nranking_id\x18\x04 \x02(\r\"\xbd\x08\n\x17LoadRankingResponseData\x12\x12\n\nranking_id\x18\x01 \x02(\r\x12\x15\n\rranking_start\x18\x02 \x02(\r\x12\x13\n\x0branking_end\x18\x03 \x02(\r\x12\x11\n\tevent_end\x18\x04 \x02(\x08\x12J\n\x0ctrainer_data\x18\x05 \x03(\x0b\x32\x34.jackal.protobuf.LoadRankingResponseData.TrainerData\x12\x13\n\x0bmodify_date\x18\x06 \x02(\r\x12\x13\n\x0b\x65vent_state\x18\x07 \x01(\r\x1a\xd8\x06\n\x0bTrainerData\x12\x14\n\x0ctrainer_name\x18\x65 \x02(\t\x12\x1a\n\x12trainer_rank_point\x18\x66 \x02(\r\x12\r\n\x05point\x18g \x02(\r\x12\x13\n\x0brecord_flag\x18h \x02(\r\x12\x18\n\x10\x66\x61vorite_pokemon\x18i \x02(\r\x12\x12\n\nwin_vs_wan\x18j \x02(\r\x12\x19\n\x11\x62\x61ttle_num_vs_wan\x18k \x02(\r\x12\x12\n\nwin_vs_cpu\x18l \x02(\r\x12\x19\n\x11\x62\x61ttle_num_vs_cpu\x18m \x02(\r\x12\x15\n\rtitle_text_id\x18n \x02(\r\x12\x16\n\x0etitle_plate_id\x18o \x02(\r\x12\x1b\n\x13title_decoration_id\x18p \x02(\r\x12\x17\n\x0f\x63omment_text_id\x18q \x02(\r\x12\x17\n\x0f\x63omment_word_id\x18r \x02(\r\x12\x10\n\x08loc_name\x18s \x02(\t\x12\x11\n\tpref_code\x18t \x02(\r\x12\x10\n\x08rank_num\x18u \x02(\r\x12\x15\n\rlast_rank_num\x18v \x02(\r\x12\x0e\n\x06updown\x18w \x02(\r\x12\x12\n\npokemon_id\x18x \x02(\r\x12\x13\n\x0bpokemon_exp\x18y \x02(\r\x12\x14\n\x0c\x62p_point_atk\x18z \x02(\r\x12\x14\n\x0c\x62p_point_res\x18{ \x02(\r\x12\x14\n\x0c\x62p_point_def\x18| \x02(\r\x12\x13\n\x0b\x62p_point_sp\x18} \x02(\r\x12\x13\n\x0b\x61vatar_body\x18~ \x02(\r\x12\x15\n\ravatar_gender\x18\x7f \x02(\r\x12\x1a\n\x11\x61vatar_background\x18\x80\x01 \x02(\r\x12\x14\n\x0b\x61vatar_head\x18\x81\x01 \x02(\r\x12\x1b\n\x12\x61vatar_battleglass\x18\x82\x01 \x02(\r\x12\x15\n\x0c\x61vatar_face0\x18\x83\x01 \x02(\r\x12\x15\n\x0c\x61vatar_face1\x18\x84\x01 \x02(\r\x12\x15\n\x0c\x61vatar_face2\x18\x85\x01 \x02(\r\x12\x17\n\x0e\x61vatar_bodyall\x18\x86\x01 \x02(\r\x12\x14\n\x0b\x61vatar_wear\x18\x87\x01 \x02(\r\x12\x19\n\x10\x61vatar_accessory\x18\x88\x01 \x02(\r\x12\x15\n\x0c\x61vatar_stamp\x18\x89\x01 \x02(\r\"h\n\x18SaveInGameLogRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x13\n\x0bin_game_log\x18\x03 \x02(\x0c\x12\x17\n\x0flog_change_time\x18\x04 \x02(\r\"\x1b\n\x19SaveInGameLogResponseData\"]\n$PreLoadInformationAttractRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x15\n\rinfo_large_id\x18\x03 \x02(\r\"\xd0\x01\n%PreLoadInformationAttractResponseData\x12\x15\n\rinfo_large_id\x18\x01 \x02(\r\x12\x13\n\x0bregion_code\x18\x02 \x02(\r\x12\x12\n\nsession_id\x18\x03 \x02(\r\x12\x13\n\x0b\x62lock_total\x18\x04 \x02(\r\x12\x12\n\nblock_size\x18\x05 \x02(\r\x12\x10\n\x08interval\x18\x06 \x02(\r\x12\x16\n\x0einfo_data_size\x18\x07 \x02(\r\x12\x14\n\x0cinfo_data_id\x18\x08 \x02(\r\"}\n!LoadInformationAttractRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x15\n\rinfo_large_id\x18\x03 \x02(\r\x12\x12\n\nsession_id\x18\x04 \x02(\r\x12\r\n\x05\x62lock\x18\x05 \x02(\r\"\x9d\x01\n\"LoadInformationAttractResponseData\x12\x15\n\rinfo_large_id\x18\x01 \x02(\r\x12\x12\n\nstart_date\x18\x02 \x02(\r\x12\x10\n\x08\x65nd_date\x18\x03 \x02(\r\x12\r\n\x05\x62lock\x18\x04 \x02(\r\x12\x13\n\x0b\x62lock_total\x18\x05 \x02(\r\x12\x16\n\x0einfo_data_body\x18\x06 \x02(\x0c\"?\n\x1dLoadClientSettingsRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\"\x8d\x10\n\x1eLoadClientSettingsResponseData\x12\x1b\n\x13money_magnification\x18\x01 \x02(\r\x12!\n\x19\x64m2_probability_single100\x18\x02 \x03(\r\x12\x1a\n\x12\x63ontinue_bonus_exp\x18\x03 \x02(\r\x12\x1c\n\x14\x63ontinue_fight_money\x18\x04 \x02(\r\x12\x17\n\x0f\x65vent_bonus_exp\x18\x05 \x02(\r\x12\x11\n\tlevel_cap\x18\x06 \x02(\r\x12\x15\n\rop_movie_flag\x18\x07 \x02(\r\x12M\n\nevent_info\x18\x08 \x03(\x0b\x32\x39.jackal.protobuf.LoadClientSettingsResponseData.EventInfo\x12O\n\x0b\x62\x61nner_info\x18\t \x03(\x0b\x32:.jackal.protobuf.LoadClientSettingsResponseData.BannerInfo\x12Q\n\x0c\x61ttract_info\x18\n \x03(\x0b\x32;.jackal.protobuf.LoadClientSettingsResponseData.AttractInfo\x12O\n\x0binfo_window\x18\x0b \x03(\x0b\x32:.jackal.protobuf.LoadClientSettingsResponseData.InfoWindow\x12\x18\n\x10lucky_bonus_rate\x18\x0c \x02(\r\x12\x18\n\x10\x66\x61il_support_num\x18\r \x02(\r\x12O\n\x0blucky_bonus\x18\x0e \x03(\x0b\x32:.jackal.protobuf.LoadClientSettingsResponseData.LuckyBonus\x12S\n\rspecial_bonus\x18\x0f \x03(\x0b\x32<.jackal.protobuf.LoadClientSettingsResponseData.SpecialBonus\x12\x17\n\x0f\x63hara_open_flag\x18\x10 \x02(\r\x12\x17\n\x0f\x63hara_open_date\x18\x11 \x02(\r\x12\x1b\n\x13\x63hara_pre_open_date\x18\x12 \x02(\r\x12\x11\n\tsearch_id\x18\x13 \x02(\r\x12\x16\n\x0e\x63lient_version\x18\x14 \x01(\t\x12!\n\x19\x63lient_version_start_date\x18\x15 \x01(\r\x1a\xe0\x01\n\tEventInfo\x12\x13\n\x0b\x65vent_state\x18\x65 \x02(\r\x12\x10\n\x08\x65vent_id\x18\x66 \x02(\r\x12\x1e\n\x16sp_bonus_category_id_1\x18g \x02(\r\x12\x1c\n\x14sp_bonus_key_value_1\x18h \x02(\r\x12\x1e\n\x16sp_bonus_category_id_2\x18i \x02(\r\x12\x1c\n\x14sp_bonus_key_value_2\x18j \x02(\r\x12\x18\n\x10\x65vent_start_date\x18k \x02(\r\x12\x16\n\x0e\x65vent_end_date\x18l \x02(\r\x1a\xa2\x01\n\nBannerInfo\x12\x1a\n\x11\x62\x61nner_start_date\x18\xc9\x01 \x02(\r\x12\x18\n\x0f\x62\x61nner_end_date\x18\xca\x01 \x02(\r\x12\x12\n\tbanner_id\x18\xcb\x01 \x02(\r\x12\x15\n\x0c\x62\x61nner_title\x18\xcc\x01 \x02(\t\x12\x18\n\x0f\x62\x61nner_sub_info\x18\xcd\x01 \x02(\t\x12\x19\n\x10\x62\x61nner_term_info\x18\xce\x01 \x02(\t\x1a\x84\x02\n\x0b\x41ttractInfo\x12 \n\x17\x61ttract_info_start_date\x18\xad\x02 \x02(\r\x12\x1e\n\x15\x61ttract_info_end_date\x18\xae\x02 \x02(\r\x12\x18\n\x0f\x61ttract_info_id\x18\xaf\x02 \x02(\r\x12\x1b\n\x12\x61ttract_info_title\x18\xb0\x02 \x02(\t\x12\x1e\n\x15\x61ttract_info_sub_info\x18\xb1\x02 \x02(\t\x12 \n\x17\x61ttract_info_start_info\x18\xb2\x02 \x02(\t\x12\x1e\n\x15\x61ttract_info_end_info\x18\xb3\x02 \x02(\t\x12\x1a\n\x11\x61ttract_info_text\x18\xb4\x02 \x02(\t\x1a\xfb\x01\n\nInfoWindow\x12\x1f\n\x16info_window_start_date\x18\x91\x03 \x02(\r\x12\x1d\n\x14info_window_end_date\x18\x92\x03 \x02(\r\x12\x17\n\x0einfo_window_id\x18\x93\x03 \x02(\r\x12\x1a\n\x11info_window_title\x18\x94\x03 \x02(\t\x12\x1d\n\x14info_window_sub_info\x18\x95\x03 \x02(\t\x12\x1f\n\x16info_window_start_info\x18\x96\x03 \x02(\t\x12\x1d\n\x14info_window_end_info\x18\x97\x03 \x02(\t\x12\x19\n\x10info_window_text\x18\x98\x03 \x02(\t\x1an\n\nLuckyBonus\x12 \n\x17lucky_bonus_category_id\x18\xf5\x03 \x02(\r\x12\x1c\n\x13lucky_bonus_data_id\x18\xf6\x03 \x02(\r\x12 \n\x17lucky_bonus_probability\x18\xf7\x03 \x02(\r\x1av\n\x0cSpecialBonus\x12\"\n\x19special_bonus_category_id\x18\xd9\x04 \x02(\r\x12\x1e\n\x15special_bonus_data_id\x18\xda\x04 \x02(\r\x12\"\n\x19special_bonus_probability\x18\xdb\x04 \x02(\r*\xb2\x03\n\x0bMessageType\x12\x08\n\x04NOOP\x10\x00\x12\x08\n\x04PING\x10\x01\x12\x10\n\x0cREGISTER_PCB\x10\x02\x12\x0c\n\x08SAVE_ADS\x10\x03\x12\x15\n\x11\x43HECK_ACCESS_CODE\x10\x04\x12\x15\n\x11SET_BNPASSID_LOCK\x10\x05\x12\r\n\tLOAD_USER\x10\x06\x12\r\n\tSAVE_USER\x10\t\x12\x13\n\x0f\x43HECK_DIAGNOSIS\x10\n\x12\x13\n\x0fSAVE_CLIENT_LOG\x10\x0b\x12\x18\n\x14PRE_LOAD_INFORMATION\x10\x0c\x12\x14\n\x10LOAD_INFORMATION\x10\r\x12\x13\n\x0fPRE_SAVE_REPLAY\x10\x0e\x12\x0f\n\x0bSAVE_REPLAY\x10\x0f\x12\x0f\n\x0bSAVE_CHARGE\x10\x10\x12\x11\n\rCHECK_RANKING\x10\x11\x12\x10\n\x0cLOAD_RANKING\x10\x12\x12\x13\n\x0fSAVE_INGAME_LOG\x10\x13\x12 \n\x1cPRE_LOAD_INFORMATION_ATTRACT\x10\x14\x12\x1c\n\x18LOAD_INFORMATION_ATTRACT\x10\x15\x12\x18\n\x14LOAD_CLIENT_SETTINGS\x10\x16') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x0cjackal.proto\x12\x0fjackal.protobuf"\xae\x0b\n\x07Request\x12*\n\x04type\x18\x01 \x02(\x0e\x32\x1c.jackal.protobuf.MessageType\x12.\n\x04noop\x18\x02 \x01(\x0b\x32 .jackal.protobuf.NoopRequestData\x12.\n\x04ping\x18\x03 \x01(\x0b\x32 .jackal.protobuf.PingRequestData\x12=\n\x0cregister_pcb\x18\x04 \x01(\x0b\x32\'.jackal.protobuf.RegisterPcbRequestData\x12\x35\n\x08save_ads\x18\x05 \x01(\x0b\x32#.jackal.protobuf.SaveAdsRequestData\x12\x46\n\x11\x63heck_access_code\x18\x06 \x01(\x0b\x32+.jackal.protobuf.CheckAccessCodeRequestData\x12\x46\n\x11set_bnpassid_lock\x18\x07 \x01(\x0b\x32+.jackal.protobuf.SetBnpassIdLockRequestData\x12\x37\n\tload_user\x18\x08 \x01(\x0b\x32$.jackal.protobuf.LoadUserRequestData\x12\x37\n\tsave_user\x18\n \x01(\x0b\x32$.jackal.protobuf.SaveUserRequestData\x12\x43\n\x0f\x63heck_diagnosis\x18\x0b \x01(\x0b\x32*.jackal.protobuf.CheckDiagnosisRequestData\x12\x42\n\x0fsave_client_log\x18\x0c \x01(\x0b\x32).jackal.protobuf.SaveClientLogRequestData\x12L\n\x14pre_load_information\x18\r \x01(\x0b\x32..jackal.protobuf.PreLoadInformationRequestData\x12\x45\n\x10load_information\x18\x0e \x01(\x0b\x32+.jackal.protobuf.LoadInformationRequestData\x12\x42\n\x0fpre_save_replay\x18\x0f \x01(\x0b\x32).jackal.protobuf.PreSaveReplayRequestData\x12;\n\x0bsave_replay\x18\x10 \x01(\x0b\x32&.jackal.protobuf.SaveReplayRequestData\x12;\n\x0bsave_charge\x18\x11 \x01(\x0b\x32&.jackal.protobuf.SaveChargeRequestData\x12?\n\rcheck_ranking\x18\x12 \x01(\x0b\x32(.jackal.protobuf.CheckRankingRequestData\x12=\n\x0cload_ranking\x18\x13 \x01(\x0b\x32\'.jackal.protobuf.LoadRankingRequestData\x12\x42\n\x0fsave_ingame_log\x18\x14 \x01(\x0b\x32).jackal.protobuf.SaveInGameLogRequestData\x12[\n\x1cpre_load_information_attract\x18\x15 \x01(\x0b\x32\x35.jackal.protobuf.PreLoadInformationAttractRequestData\x12T\n\x18load_information_attract\x18\x16 \x01(\x0b\x32\x32.jackal.protobuf.LoadInformationAttractRequestData\x12L\n\x14load_client_settings\x18\x17 \x01(\x0b\x32..jackal.protobuf.LoadClientSettingsRequestData"\xd4\x0b\n\x08Response\x12*\n\x04type\x18\x01 \x02(\x0e\x32\x1c.jackal.protobuf.MessageType\x12\x0e\n\x06result\x18\x02 \x02(\r\x12/\n\x04noop\x18\x03 \x01(\x0b\x32!.jackal.protobuf.NoopResponseData\x12/\n\x04ping\x18\x04 \x01(\x0b\x32!.jackal.protobuf.PingResponseData\x12>\n\x0cregister_pcb\x18\x05 \x01(\x0b\x32(.jackal.protobuf.RegisterPcbResponseData\x12\x36\n\x08save_ads\x18\x06 \x01(\x0b\x32$.jackal.protobuf.SaveAdsResponseData\x12G\n\x11\x63heck_access_code\x18\x07 \x01(\x0b\x32,.jackal.protobuf.CheckAccessCodeResponseData\x12G\n\x11set_bnpassid_lock\x18\x08 \x01(\x0b\x32,.jackal.protobuf.SetBnpassIdLockResponseData\x12\x38\n\tload_user\x18\t \x01(\x0b\x32%.jackal.protobuf.LoadUserResponseData\x12\x38\n\tsave_user\x18\x0b \x01(\x0b\x32%.jackal.protobuf.SaveUserResponseData\x12\x44\n\x0f\x63heck_diagnosis\x18\x0c \x01(\x0b\x32+.jackal.protobuf.CheckDiagnosisResponseData\x12\x43\n\x0fsave_client_log\x18\r \x01(\x0b\x32*.jackal.protobuf.SaveClientLogResponseData\x12M\n\x14pre_load_information\x18\x0e \x01(\x0b\x32/.jackal.protobuf.PreLoadInformationResponseData\x12\x46\n\x10load_information\x18\x0f \x01(\x0b\x32,.jackal.protobuf.LoadInformationResponseData\x12\x43\n\x0fpre_save_replay\x18\x10 \x01(\x0b\x32*.jackal.protobuf.PreSaveReplayResponseData\x12<\n\x0bsave_replay\x18\x11 \x01(\x0b\x32\'.jackal.protobuf.SaveReplayResponseData\x12<\n\x0bsave_charge\x18\x12 \x01(\x0b\x32\'.jackal.protobuf.SaveChargeResponseData\x12@\n\rcheck_ranking\x18\x13 \x01(\x0b\x32).jackal.protobuf.CheckRankingResponseData\x12>\n\x0cload_ranking\x18\x14 \x01(\x0b\x32(.jackal.protobuf.LoadRankingResponseData\x12\x43\n\x0fsave_ingame_log\x18\x15 \x01(\x0b\x32*.jackal.protobuf.SaveInGameLogResponseData\x12\\\n\x1cpre_load_information_attract\x18\x16 \x01(\x0b\x32\x36.jackal.protobuf.PreLoadInformationAttractResponseData\x12U\n\x18load_information_attract\x18\x17 \x01(\x0b\x32\x33.jackal.protobuf.LoadInformationAttractResponseData\x12M\n\x14load_client_settings\x18\x18 \x01(\x0b\x32/.jackal.protobuf.LoadClientSettingsResponseData"1\n\x0fNoopRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t"\x12\n\x10NoopResponseData"1\n\x0fPingRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t"\x12\n\x10PingResponseData"\xce\x02\n\x16RegisterPcbRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x10\n\x08pcb_type\x18\x02 \x02(\r\x12\x15\n\rlocation_name\x18\x03 \x02(\t\x12\x19\n\x11location_nickname\x18\x04 \x02(\t\x12\x11\n\tpref_code\x18\x05 \x01(\r\x12\r\n\x05\x61\x64\x64r0\x18\x06 \x01(\t\x12\r\n\x05\x61\x64\x64r1\x18\x07 \x01(\t\x12\r\n\x05\x61\x64\x64r2\x18\x08 \x01(\t\x12\r\n\x05\x61\x64\x64r3\x18\t \x01(\t\x12\x0e\n\x06loc_id\x18\n \x02(\t\x12\x14\n\x0c\x63ountry_code\x18\x0b \x02(\t\x12\x13\n\x0bregion_code\x18\x0c \x02(\r\x12\r\n\x05karma\x18\r \x02(\x05\x12\x0f\n\x07game_id\x18\x0e \x02(\t\x12\x10\n\x08game_ver\x18\x0f \x02(\t\x12\x10\n\x08\x64isk_ver\x18\x10 \x02(\t\x12\x12\n\nutc_offset\x18\x11 \x02(\x05"\x85\x01\n\x17RegisterPcbResponseData\x12\x13\n\x0bserver_time\x18\x01 \x02(\r\x12\x15\n\roperate_start\x18\x02 \x01(\t\x12\x13\n\x0boperate_end\x18\x03 \x01(\t\x12\x13\n\x0b\x62np_baseuri\x18\x04 \x01(\t\x12\x14\n\x0c\x62iwa_setting\x18\x05 \x02(\t"\xd7\x02\n\x12SaveAdsRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x17\n\x0f\x61\x64s_start_count\x18\x03 \x02(\r\x12\x16\n\x0e\x61\x64s_coin_count\x18\x04 \x02(\r\x12\x19\n\x11\x61\x64s_service_count\x18\x05 \x02(\r\x12\x1a\n\x12\x61\x64s_freeplay_count\x18\x06 \x02(\r\x12\x1a\n\x12\x61\x64s_operation_days\x18\x07 \x02(\r\x12\x1a\n\x12\x61\x64s_power_on_count\x18\x08 \x02(\r\x12\x46\n\rads_play_time\x18\t \x03(\x0b\x32/.jackal.protobuf.SaveAdsRequestData.AdsPlayTime\x1a\x39\n\x0b\x41\x64sPlayTime\x12\x12\n\npokemon_id\x18\x65 \x02(\r\x12\x16\n\x0e\x63harplay_count\x18\x66 \x02(\r"\x15\n\x13SaveAdsResponseData"M\n\x1a\x43heckAccessCodeRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x0f\n\x07\x63hip_id\x18\x03 \x02(\t"M\n\x1b\x43heckAccessCodeResponseData\x12\x19\n\x11\x63ommidserv_result\x18\x01 \x02(\r\x12\x13\n\x0b\x61\x63\x63\x65ss_code\x18\x02 \x02(\t"\xa4\x01\n\x1aSetBnpassIdLockRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x13\n\x0b\x62\x61napass_id\x18\x03 \x02(\r\x12\x13\n\x0b\x64\x65vice_type\x18\x04 \x02(\r\x12\x0f\n\x07\x63hip_id\x18\x05 \x02(\t\x12\x13\n\x0b\x61\x63\x63\x65ss_code\x18\x06 \x01(\t\x12\x16\n\x0e\x63\x61rd_lock_time\x18\x07 \x02(\r"\x1d\n\x1bSetBnpassIdLockResponseData"\x85\x01\n\x13LoadUserRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x13\n\x0b\x64\x65vice_type\x18\x03 \x02(\r\x12\x0f\n\x07\x63hip_id\x18\x04 \x02(\t\x12\x13\n\x0b\x61\x63\x63\x65ss_code\x18\x05 \x01(\t\x12\x13\n\x0b\x63\x61rd_status\x18\x06 \x02(\x08"\xfc\x12\n\x14LoadUserResponseData\x12\x19\n\x11\x63ommidserv_result\x18\x01 \x02(\r\x12\x11\n\tload_hash\x18\x02 \x02(\r\x12\x17\n\x0f\x63\x61rdlock_status\x18\x03 \x02(\x08\x12\x13\n\x0b\x62\x61napass_id\x18\x04 \x02(\r\x12\x13\n\x0b\x61\x63\x63\x65ss_code\x18\x05 \x01(\t\x12\x15\n\rnew_card_flag\x18\x06 \x02(\x08\x12\x1e\n\x16precedent_release_flag\x18\x07 \x02(\r\x12\x18\n\x10navi_newbie_flag\x18\x08 \x02(\x08\x12\x18\n\x10navi_enable_flag\x18\t \x02(\x08\x12\x18\n\x10pad_vibrate_flag\x18\n \x02(\x08\x12\x18\n\x10home_region_code\x18\x0b \x02(\r\x12\x15\n\rhome_loc_name\x18\x0c \x02(\t\x12\x11\n\tpref_code\x18\r \x02(\r\x12\x14\n\x0ctrainer_name\x18\x0e \x01(\t\x12\x1a\n\x12trainer_rank_point\x18\x0f \x02(\r\x12\x0e\n\x06wallet\x18\x10 \x02(\r\x12\x13\n\x0b\x66ight_money\x18\x11 \x02(\r\x12\x13\n\x0bscore_point\x18\x12 \x02(\r\x12\x15\n\rgrade_max_num\x18\x13 \x02(\r\x12\x15\n\rextra_counter\x18\x14 \x01(\r\x12\x1e\n\x16tutorial_progress_flag\x18\x15 \x03(\r\x12\x17\n\x0ftotal_play_days\x18\x16 \x02(\r\x12\x16\n\x0eplay_date_time\x18\x17 \x02(\r\x12\x1a\n\x12lucky_box_fail_num\x18\x18 \x02(\r\x12\x1d\n\x15\x65vent_reward_get_flag\x18\x19 \x02(\r\x12\x14\n\x0crank_pvp_all\x18\x1a \x02(\r\x12\x14\n\x0crank_pvp_loc\x18\x1b \x02(\r\x12\x14\n\x0crank_cpu_all\x18\x1c \x02(\r\x12\x14\n\x0crank_cpu_loc\x18\x1d \x02(\r\x12\x12\n\nrank_event\x18\x1e \x02(\r\x12\x11\n\tawake_num\x18\x1f \x02(\r\x12\x17\n\x0fuse_support_num\x18 \x02(\r\x12\x16\n\x0erankmatch_flag\x18! \x02(\r\x12\x1a\n\x12rankmatch_progress\x18" \x03(\r\x12\x15\n\rrankmatch_max\x18# \x01(\r\x12\x19\n\x11rankmatch_success\x18$ \x01(\r\x12\x10\n\x08\x62\x65\x61t_num\x18% \x01(\x05\x12\x15\n\rtitle_text_id\x18& \x02(\r\x12\x16\n\x0etitle_plate_id\x18\' \x02(\r\x12\x1b\n\x13title_decoration_id\x18( \x02(\r\x12\x1c\n\x14support_pokemon_list\x18) \x03(\r\x12\x15\n\rsupport_set_1\x18* \x03(\r\x12\x15\n\rsupport_set_2\x18+ \x03(\r\x12\x15\n\rsupport_set_3\x18, \x03(\r\x12\x14\n\x0cnavi_trainer\x18- \x02(\r\x12\x17\n\x0fnavi_version_id\x18. \x02(\r\x12\x16\n\x0e\x61id_skill_list\x18/ \x03(\r\x12\x11\n\taid_skill\x18\x30 \x02(\r\x12\x17\n\x0f\x63omment_text_id\x18\x31 \x02(\r\x12\x17\n\x0f\x63omment_word_id\x18\x32 \x02(\r\x12\x1a\n\x12latest_use_pokemon\x18\x33 \x02(\r\x12\x11\n\tex_ko_num\x18\x34 \x02(\r\x12\x0f\n\x07wko_num\x18\x35 \x02(\r\x12\x16\n\x0etimeup_win_num\x18\x36 \x02(\r\x12\x13\n\x0b\x63ool_ko_num\x18\x37 \x02(\r\x12\x16\n\x0eperfect_ko_num\x18\x38 \x02(\r\x12\x13\n\x0brecord_flag\x18\x39 \x02(\r\x12\x1c\n\x14site_register_status\x18: \x02(\r\x12\x14\n\x0c\x63ontinue_num\x18; \x02(\r\x12\x18\n\x10\x61\x63hievement_flag\x18< \x03(\r\x12\x13\n\x0b\x61vatar_body\x18= \x01(\r\x12\x15\n\ravatar_gender\x18> \x01(\r\x12\x19\n\x11\x61vatar_background\x18? \x01(\r\x12\x13\n\x0b\x61vatar_head\x18@ \x01(\r\x12\x1a\n\x12\x61vatar_battleglass\x18\x41 \x01(\r\x12\x14\n\x0c\x61vatar_face0\x18\x42 \x01(\r\x12\x14\n\x0c\x61vatar_face1\x18\x43 \x01(\r\x12\x14\n\x0c\x61vatar_face2\x18\x44 \x01(\r\x12\x16\n\x0e\x61vatar_bodyall\x18\x45 \x01(\r\x12\x13\n\x0b\x61vatar_wear\x18\x46 \x01(\r\x12\x18\n\x10\x61vatar_accessory\x18G \x01(\r\x12\x14\n\x0c\x61vatar_stamp\x18H \x01(\r\x12G\n\x0cpokemon_data\x18I \x03(\x0b\x32\x31.jackal.protobuf.LoadUserResponseData.PokemonData\x12\x13\n\x0b\x65vent_state\x18J \x02(\r\x12\x10\n\x08\x65vent_id\x18K \x02(\r\x12\x1e\n\x16sp_bonus_category_id_1\x18L \x02(\r\x12\x1c\n\x14sp_bonus_key_value_1\x18M \x02(\r\x12\x1e\n\x16sp_bonus_category_id_2\x18N \x02(\r\x12\x1c\n\x14sp_bonus_key_value_2\x18O \x02(\r\x12\x1a\n\x12last_play_event_id\x18P \x01(\r\x12\x1e\n\x16\x65vent_achievement_flag\x18Q \x03(\r\x12\x1f\n\x17\x65vent_achievement_param\x18R \x03(\r\x1a\xf0\x02\n\x0bPokemonData\x12\x0f\n\x07\x63har_id\x18\x65 \x02(\r\x12\x1c\n\x14illustration_book_no\x18\x66 \x02(\r\x12\x13\n\x0bpokemon_exp\x18g \x02(\r\x12\x19\n\x11\x62\x61ttle_num_vs_wan\x18h \x02(\r\x12\x12\n\nwin_vs_wan\x18i \x02(\r\x12\x19\n\x11\x62\x61ttle_num_vs_lan\x18j \x02(\r\x12\x12\n\nwin_vs_lan\x18k \x02(\r\x12\x19\n\x11\x62\x61ttle_num_vs_cpu\x18l \x02(\r\x12\x0f\n\x07win_cpu\x18m \x02(\r\x12\x1f\n\x17\x62\x61ttle_all_num_tutorial\x18n \x02(\r\x12\x1b\n\x13\x62\x61ttle_num_tutorial\x18o \x02(\r\x12\x14\n\x0c\x62p_point_atk\x18p \x02(\r\x12\x14\n\x0c\x62p_point_res\x18q \x02(\r\x12\x14\n\x0c\x62p_point_def\x18r \x02(\r\x12\x13\n\x0b\x62p_point_sp\x18s \x02(\r"\x98\x0c\n\x13SaveUserRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x13\n\x0b\x62\x61napass_id\x18\x03 \x02(\r\x12\x1e\n\x16get_trainer_rank_point\x18\x04 \x01(\x05\x12\x11\n\tget_money\x18\x05 \x02(\r\x12\x17\n\x0fget_score_point\x18\x06 \x01(\r\x12\x15\n\rgrade_max_num\x18\x07 \x01(\r\x12\x15\n\rextra_counter\x18\x08 \x01(\r\x12\x1e\n\x16tutorial_progress_flag\x18\t \x03(\r\x12\x1d\n\x15\x65vent_reward_get_flag\x18\n \x01(\r\x12\x14\n\x0c\x63ontinue_num\x18\x0b \x02(\r\x12\x17\n\x0ftotal_play_days\x18\x0c \x02(\r\x12\x18\n\x10\x61\x63hievement_flag\x18\r \x03(\r\x12\x11\n\tawake_num\x18\x0e \x02(\r\x12\x17\n\x0fuse_support_num\x18\x0f \x02(\r\x12\x16\n\x0erankmatch_flag\x18\x10 \x02(\r\x12\x1a\n\x12rank_match_process\x18\x11 \x03(\r\x12\x16\n\x0erank_match_max\x18\x12 \x01(\r\x12\x1a\n\x12rank_match_success\x18\x13 \x01(\r\x12\x10\n\x08\x62\x65\x61t_num\x18\x14 \x01(\x05\x12\x15\n\rsupport_set_1\x18\x15 \x03(\r\x12\x15\n\rsupport_set_2\x18\x16 \x03(\r\x12\x15\n\rsupport_set_3\x18\x17 \x03(\r\x12\x44\n\x0b\x62\x61ttle_data\x18\x18 \x02(\x0b\x32/.jackal.protobuf.SaveUserRequestData.BattleData\x12\x46\n\x0cpokemon_data\x18\x19 \x02(\x0b\x32\x30.jackal.protobuf.SaveUserRequestData.PokemonData\x12\x1c\n\x14trainer_name_pending\x18\x1a \x01(\t\x12\x15\n\ravatar_gender\x18\x1b \x01(\r\x12\x15\n\rcontinue_flag\x18\x1c \x02(\x08\x12\x14\n\x0creq_sendtime\x18\x1d \x02(\r\x12\x15\n\rplay_all_time\x18\x1e \x02(\r\x12\x11\n\tload_hash\x18\x1f \x02(\r\x12\x44\n\x0breward_data\x18 \x03(\x0b\x32/.jackal.protobuf.SaveUserRequestData.RewardData\x12\x13\n\x0b\x65vent_state\x18! \x01(\r\x12\x11\n\taid_skill\x18" \x01(\r\x12\x1a\n\x12last_play_event_id\x18# \x01(\r\x12\x1e\n\x16\x65vent_achievement_flag\x18$ \x03(\r\x12\x1f\n\x17\x65vent_achievement_param\x18% \x03(\r\x1a\xec\x01\n\nBattleData\x12\x11\n\tplay_mode\x18\x65 \x03(\r\x12\x0e\n\x06result\x18\x66 \x03(\r\x12\x11\n\tex_ko_num\x18g \x02(\r\x12\x0f\n\x07wko_num\x18h \x02(\r\x12\x16\n\x0etimeup_win_num\x18i \x02(\r\x12\x13\n\x0b\x63ool_ko_num\x18j \x02(\r\x12\x16\n\x0eperfect_ko_num\x18k \x02(\r\x12\x10\n\x08use_navi\x18l \x02(\r\x12\x16\n\x0euse_navi_cloth\x18m \x02(\r\x12\x15\n\ruse_aid_skill\x18n \x02(\r\x12\x11\n\tplay_date\x18o \x02(\r\x1a\xb3\x01\n\x0bPokemonData\x12\x10\n\x07\x63har_id\x18\xc9\x01 \x02(\r\x12\x1d\n\x14illustration_book_no\x18\xca\x01 \x02(\r\x12\x18\n\x0fget_pokemon_exp\x18\xcb\x01 \x02(\r\x12\x15\n\x0c\x62p_point_atk\x18\xcc\x01 \x02(\r\x12\x15\n\x0c\x62p_point_res\x18\xcd\x01 \x02(\r\x12\x15\n\x0c\x62p_point_def\x18\xce\x01 \x02(\r\x12\x14\n\x0b\x62p_point_sp\x18\xcf\x01 \x02(\r\x1aU\n\nRewardData\x12\x18\n\x0fget_category_id\x18\xad\x02 \x02(\r\x12\x17\n\x0eget_content_id\x18\xae\x02 \x02(\r\x12\x14\n\x0bget_type_id\x18\xaf\x02 \x02(\r"\x16\n\x14SaveUserResponseData";\n\x19\x43heckDiagnosisRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t"\x95\x02\n\x1a\x43heckDiagnosisResponseData\x12Q\n\x0e\x64iagnosis_data\x18\x01 \x03(\x0b\x32\x39.jackal.protobuf.CheckDiagnosisResponseData.DiagnosisData\x1a\xa3\x01\n\rDiagnosisData\x12\x14\n\x0crequest_type\x18\x65 \x02(\r\x12\x17\n\x0f\x63onnect_timeout\x18\x66 \x02(\r\x12\x14\n\x0csend_timeout\x18g \x02(\r\x12\x17\n\x0freceive_timeout\x18h \x02(\r\x12\x1c\n\x14retry_time_of_number\x18i \x02(\r\x12\x16\n\x0eretry_interval\x18j \x02(\r"\xf4\x01\n\x18SaveClientLogRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x11\n\tserial_id\x18\x03 \x02(\r\x12\x14\n\x0creq_sendtime\x18\x04 \x02(\r\x12\r\n\x05karma\x18\x05 \x02(\x05\x12\x14\n\x0crequest_type\x18\x06 \x02(\r\x12\x1f\n\x17request_number_of_times\x18\x07 \x02(\r\x12\x1f\n\x17timeout_number_of_times\x18\x08 \x02(\r\x12\x11\n\tretry_max\x18\t \x02(\r\x12\x15\n\rresponse_time\x18\n \x02(\r"\x1b\n\x19SaveClientLogResponseData"V\n\x1dPreLoadInformationRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x15\n\rinfo_small_id\x18\x03 \x02(\r"\xc9\x01\n\x1ePreLoadInformationResponseData\x12\x15\n\rinfo_small_id\x18\x01 \x02(\r\x12\x13\n\x0bregion_code\x18\x02 \x02(\r\x12\x12\n\nsession_id\x18\x03 \x02(\r\x12\x13\n\x0b\x62lock_total\x18\x04 \x02(\r\x12\x12\n\nblock_size\x18\x05 \x02(\r\x12\x10\n\x08interval\x18\x06 \x02(\r\x12\x16\n\x0einfo_data_size\x18\x07 \x02(\r\x12\x14\n\x0cinfo_data_id\x18\x08 \x02(\r"v\n\x1aLoadInformationRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x15\n\rinfo_small_id\x18\x03 \x02(\r\x12\x12\n\nsession_id\x18\x04 \x02(\r\x12\r\n\x05\x62lock\x18\x05 \x02(\r"\x96\x01\n\x1bLoadInformationResponseData\x12\x15\n\rinfo_small_id\x18\x01 \x02(\r\x12\x12\n\nstart_date\x18\x02 \x02(\r\x12\x10\n\x08\x65nd_date\x18\x03 \x02(\r\x12\r\n\x05\x62lock\x18\x04 \x02(\r\x12\x13\n\x0b\x62lock_total\x18\x05 \x02(\r\x12\x16\n\x0einfo_data_body\x18\x06 \x02(\x0c"\x80\x01\n\x18PreSaveReplayRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x13\n\x0bregion_code\x18\x03 \x02(\r\x12\x15\n\rcategory_code\x18\x04 \x02(\r\x12\x18\n\x10replay_data_size\x18\x05 \x02(\r"j\n\x19PreSaveReplayResponseData\x12\x12\n\nsession_id\x18\x01 \x02(\r\x12\x13\n\x0b\x62lock_total\x18\x02 \x02(\r\x12\x12\n\nblock_size\x18\x03 \x02(\r\x12\x10\n\x08interval\x18\x04 \x02(\r"\x82\x02\n\x15SaveReplayRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x13\n\x0b\x62\x61napass_id\x18\x03 \x02(\r\x12\x12\n\npokemon_id\x18\x04 \x02(\r\x12\x17\n\x0ftrainer_rank_id\x18\x05 \x02(\r\x12\x13\n\x0bregion_code\x18\x06 \x02(\r\x12\x12\n\nsession_id\x18\x07 \x02(\r\x12\r\n\x05\x62lock\x18\x08 \x02(\r\x12\x1b\n\x13transfer_completion\x18\t \x02(\r\x12\x18\n\x10replay_data_size\x18\n \x02(\r\x12\x18\n\x10replay_data_body\x18\x0b \x02(\x0c"\x18\n\x16SaveReplayResponseData"\x8d\x01\n\x15SaveChargeRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x0f\n\x07game_id\x18\x03 \x02(\t\x12\x19\n\x11\x63harge_data_index\x18\x04 \x02(\t\x12\x13\n\x0b\x63harge_type\x18\x05 \x02(\r\x12\x13\n\x0b\x63harge_time\x18\x06 \x02(\r"N\n\x16SaveChargeResponseData\x12\x19\n\x11\x63harge_error_code\x18\x01 \x02(\r\x12\x19\n\x11\x63harge_data_index\x18\x02 \x02(\t"u\n\x17\x43heckRankingRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x13\n\x0bregion_code\x18\x03 \x02(\r\x12\x12\n\nranking_id\x18\x04 \x02(\r\x12\x11\n\ttimestamp\x18\x05 \x02(\r".\n\x18\x43heckRankingResponseData\x12\x12\n\nranking_id\x18\x01 \x02(\r"a\n\x16LoadRankingRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x13\n\x0bregion_code\x18\x03 \x02(\r\x12\x12\n\nranking_id\x18\x04 \x02(\r"\xbd\x08\n\x17LoadRankingResponseData\x12\x12\n\nranking_id\x18\x01 \x02(\r\x12\x15\n\rranking_start\x18\x02 \x02(\r\x12\x13\n\x0branking_end\x18\x03 \x02(\r\x12\x11\n\tevent_end\x18\x04 \x02(\x08\x12J\n\x0ctrainer_data\x18\x05 \x03(\x0b\x32\x34.jackal.protobuf.LoadRankingResponseData.TrainerData\x12\x13\n\x0bmodify_date\x18\x06 \x02(\r\x12\x13\n\x0b\x65vent_state\x18\x07 \x01(\r\x1a\xd8\x06\n\x0bTrainerData\x12\x14\n\x0ctrainer_name\x18\x65 \x02(\t\x12\x1a\n\x12trainer_rank_point\x18\x66 \x02(\r\x12\r\n\x05point\x18g \x02(\r\x12\x13\n\x0brecord_flag\x18h \x02(\r\x12\x18\n\x10\x66\x61vorite_pokemon\x18i \x02(\r\x12\x12\n\nwin_vs_wan\x18j \x02(\r\x12\x19\n\x11\x62\x61ttle_num_vs_wan\x18k \x02(\r\x12\x12\n\nwin_vs_cpu\x18l \x02(\r\x12\x19\n\x11\x62\x61ttle_num_vs_cpu\x18m \x02(\r\x12\x15\n\rtitle_text_id\x18n \x02(\r\x12\x16\n\x0etitle_plate_id\x18o \x02(\r\x12\x1b\n\x13title_decoration_id\x18p \x02(\r\x12\x17\n\x0f\x63omment_text_id\x18q \x02(\r\x12\x17\n\x0f\x63omment_word_id\x18r \x02(\r\x12\x10\n\x08loc_name\x18s \x02(\t\x12\x11\n\tpref_code\x18t \x02(\r\x12\x10\n\x08rank_num\x18u \x02(\r\x12\x15\n\rlast_rank_num\x18v \x02(\r\x12\x0e\n\x06updown\x18w \x02(\r\x12\x12\n\npokemon_id\x18x \x02(\r\x12\x13\n\x0bpokemon_exp\x18y \x02(\r\x12\x14\n\x0c\x62p_point_atk\x18z \x02(\r\x12\x14\n\x0c\x62p_point_res\x18{ \x02(\r\x12\x14\n\x0c\x62p_point_def\x18| \x02(\r\x12\x13\n\x0b\x62p_point_sp\x18} \x02(\r\x12\x13\n\x0b\x61vatar_body\x18~ \x02(\r\x12\x15\n\ravatar_gender\x18\x7f \x02(\r\x12\x1a\n\x11\x61vatar_background\x18\x80\x01 \x02(\r\x12\x14\n\x0b\x61vatar_head\x18\x81\x01 \x02(\r\x12\x1b\n\x12\x61vatar_battleglass\x18\x82\x01 \x02(\r\x12\x15\n\x0c\x61vatar_face0\x18\x83\x01 \x02(\r\x12\x15\n\x0c\x61vatar_face1\x18\x84\x01 \x02(\r\x12\x15\n\x0c\x61vatar_face2\x18\x85\x01 \x02(\r\x12\x17\n\x0e\x61vatar_bodyall\x18\x86\x01 \x02(\r\x12\x14\n\x0b\x61vatar_wear\x18\x87\x01 \x02(\r\x12\x19\n\x10\x61vatar_accessory\x18\x88\x01 \x02(\r\x12\x15\n\x0c\x61vatar_stamp\x18\x89\x01 \x02(\r"h\n\x18SaveInGameLogRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x13\n\x0bin_game_log\x18\x03 \x02(\x0c\x12\x17\n\x0flog_change_time\x18\x04 \x02(\r"\x1b\n\x19SaveInGameLogResponseData"]\n$PreLoadInformationAttractRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x15\n\rinfo_large_id\x18\x03 \x02(\r"\xd0\x01\n%PreLoadInformationAttractResponseData\x12\x15\n\rinfo_large_id\x18\x01 \x02(\r\x12\x13\n\x0bregion_code\x18\x02 \x02(\r\x12\x12\n\nsession_id\x18\x03 \x02(\r\x12\x13\n\x0b\x62lock_total\x18\x04 \x02(\r\x12\x12\n\nblock_size\x18\x05 \x02(\r\x12\x10\n\x08interval\x18\x06 \x02(\r\x12\x16\n\x0einfo_data_size\x18\x07 \x02(\r\x12\x14\n\x0cinfo_data_id\x18\x08 \x02(\r"}\n!LoadInformationAttractRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x15\n\rinfo_large_id\x18\x03 \x02(\r\x12\x12\n\nsession_id\x18\x04 \x02(\r\x12\r\n\x05\x62lock\x18\x05 \x02(\r"\x9d\x01\n"LoadInformationAttractResponseData\x12\x15\n\rinfo_large_id\x18\x01 \x02(\r\x12\x12\n\nstart_date\x18\x02 \x02(\r\x12\x10\n\x08\x65nd_date\x18\x03 \x02(\r\x12\r\n\x05\x62lock\x18\x04 \x02(\r\x12\x13\n\x0b\x62lock_total\x18\x05 \x02(\r\x12\x16\n\x0einfo_data_body\x18\x06 \x02(\x0c"?\n\x1dLoadClientSettingsRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t"\x8d\x10\n\x1eLoadClientSettingsResponseData\x12\x1b\n\x13money_magnification\x18\x01 \x02(\r\x12!\n\x19\x64m2_probability_single100\x18\x02 \x03(\r\x12\x1a\n\x12\x63ontinue_bonus_exp\x18\x03 \x02(\r\x12\x1c\n\x14\x63ontinue_fight_money\x18\x04 \x02(\r\x12\x17\n\x0f\x65vent_bonus_exp\x18\x05 \x02(\r\x12\x11\n\tlevel_cap\x18\x06 \x02(\r\x12\x15\n\rop_movie_flag\x18\x07 \x02(\r\x12M\n\nevent_info\x18\x08 \x03(\x0b\x32\x39.jackal.protobuf.LoadClientSettingsResponseData.EventInfo\x12O\n\x0b\x62\x61nner_info\x18\t \x03(\x0b\x32:.jackal.protobuf.LoadClientSettingsResponseData.BannerInfo\x12Q\n\x0c\x61ttract_info\x18\n \x03(\x0b\x32;.jackal.protobuf.LoadClientSettingsResponseData.AttractInfo\x12O\n\x0binfo_window\x18\x0b \x03(\x0b\x32:.jackal.protobuf.LoadClientSettingsResponseData.InfoWindow\x12\x18\n\x10lucky_bonus_rate\x18\x0c \x02(\r\x12\x18\n\x10\x66\x61il_support_num\x18\r \x02(\r\x12O\n\x0blucky_bonus\x18\x0e \x03(\x0b\x32:.jackal.protobuf.LoadClientSettingsResponseData.LuckyBonus\x12S\n\rspecial_bonus\x18\x0f \x03(\x0b\x32<.jackal.protobuf.LoadClientSettingsResponseData.SpecialBonus\x12\x17\n\x0f\x63hara_open_flag\x18\x10 \x02(\r\x12\x17\n\x0f\x63hara_open_date\x18\x11 \x02(\r\x12\x1b\n\x13\x63hara_pre_open_date\x18\x12 \x02(\r\x12\x11\n\tsearch_id\x18\x13 \x02(\r\x12\x16\n\x0e\x63lient_version\x18\x14 \x01(\t\x12!\n\x19\x63lient_version_start_date\x18\x15 \x01(\r\x1a\xe0\x01\n\tEventInfo\x12\x13\n\x0b\x65vent_state\x18\x65 \x02(\r\x12\x10\n\x08\x65vent_id\x18\x66 \x02(\r\x12\x1e\n\x16sp_bonus_category_id_1\x18g \x02(\r\x12\x1c\n\x14sp_bonus_key_value_1\x18h \x02(\r\x12\x1e\n\x16sp_bonus_category_id_2\x18i \x02(\r\x12\x1c\n\x14sp_bonus_key_value_2\x18j \x02(\r\x12\x18\n\x10\x65vent_start_date\x18k \x02(\r\x12\x16\n\x0e\x65vent_end_date\x18l \x02(\r\x1a\xa2\x01\n\nBannerInfo\x12\x1a\n\x11\x62\x61nner_start_date\x18\xc9\x01 \x02(\r\x12\x18\n\x0f\x62\x61nner_end_date\x18\xca\x01 \x02(\r\x12\x12\n\tbanner_id\x18\xcb\x01 \x02(\r\x12\x15\n\x0c\x62\x61nner_title\x18\xcc\x01 \x02(\t\x12\x18\n\x0f\x62\x61nner_sub_info\x18\xcd\x01 \x02(\t\x12\x19\n\x10\x62\x61nner_term_info\x18\xce\x01 \x02(\t\x1a\x84\x02\n\x0b\x41ttractInfo\x12 \n\x17\x61ttract_info_start_date\x18\xad\x02 \x02(\r\x12\x1e\n\x15\x61ttract_info_end_date\x18\xae\x02 \x02(\r\x12\x18\n\x0f\x61ttract_info_id\x18\xaf\x02 \x02(\r\x12\x1b\n\x12\x61ttract_info_title\x18\xb0\x02 \x02(\t\x12\x1e\n\x15\x61ttract_info_sub_info\x18\xb1\x02 \x02(\t\x12 \n\x17\x61ttract_info_start_info\x18\xb2\x02 \x02(\t\x12\x1e\n\x15\x61ttract_info_end_info\x18\xb3\x02 \x02(\t\x12\x1a\n\x11\x61ttract_info_text\x18\xb4\x02 \x02(\t\x1a\xfb\x01\n\nInfoWindow\x12\x1f\n\x16info_window_start_date\x18\x91\x03 \x02(\r\x12\x1d\n\x14info_window_end_date\x18\x92\x03 \x02(\r\x12\x17\n\x0einfo_window_id\x18\x93\x03 \x02(\r\x12\x1a\n\x11info_window_title\x18\x94\x03 \x02(\t\x12\x1d\n\x14info_window_sub_info\x18\x95\x03 \x02(\t\x12\x1f\n\x16info_window_start_info\x18\x96\x03 \x02(\t\x12\x1d\n\x14info_window_end_info\x18\x97\x03 \x02(\t\x12\x19\n\x10info_window_text\x18\x98\x03 \x02(\t\x1an\n\nLuckyBonus\x12 \n\x17lucky_bonus_category_id\x18\xf5\x03 \x02(\r\x12\x1c\n\x13lucky_bonus_data_id\x18\xf6\x03 \x02(\r\x12 \n\x17lucky_bonus_probability\x18\xf7\x03 \x02(\r\x1av\n\x0cSpecialBonus\x12"\n\x19special_bonus_category_id\x18\xd9\x04 \x02(\r\x12\x1e\n\x15special_bonus_data_id\x18\xda\x04 \x02(\r\x12"\n\x19special_bonus_probability\x18\xdb\x04 \x02(\r*\xb2\x03\n\x0bMessageType\x12\x08\n\x04NOOP\x10\x00\x12\x08\n\x04PING\x10\x01\x12\x10\n\x0cREGISTER_PCB\x10\x02\x12\x0c\n\x08SAVE_ADS\x10\x03\x12\x15\n\x11\x43HECK_ACCESS_CODE\x10\x04\x12\x15\n\x11SET_BNPASSID_LOCK\x10\x05\x12\r\n\tLOAD_USER\x10\x06\x12\r\n\tSAVE_USER\x10\t\x12\x13\n\x0f\x43HECK_DIAGNOSIS\x10\n\x12\x13\n\x0fSAVE_CLIENT_LOG\x10\x0b\x12\x18\n\x14PRE_LOAD_INFORMATION\x10\x0c\x12\x14\n\x10LOAD_INFORMATION\x10\r\x12\x13\n\x0fPRE_SAVE_REPLAY\x10\x0e\x12\x0f\n\x0bSAVE_REPLAY\x10\x0f\x12\x0f\n\x0bSAVE_CHARGE\x10\x10\x12\x11\n\rCHECK_RANKING\x10\x11\x12\x10\n\x0cLOAD_RANKING\x10\x12\x12\x13\n\x0fSAVE_INGAME_LOG\x10\x13\x12 \n\x1cPRE_LOAD_INFORMATION_ATTRACT\x10\x14\x12\x1c\n\x18LOAD_INFORMATION_ATTRACT\x10\x15\x12\x18\n\x14LOAD_CLIENT_SETTINGS\x10\x16' +) _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'jackal_pb2', globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "jackal_pb2", globals()) if _descriptor._USE_C_DESCRIPTORS == False: - - DESCRIPTOR._options = None - _MESSAGETYPE._serialized_start=14623 - _MESSAGETYPE._serialized_end=15057 - _REQUEST._serialized_start=34 - _REQUEST._serialized_end=1488 - _RESPONSE._serialized_start=1491 - _RESPONSE._serialized_end=2983 - _NOOPREQUESTDATA._serialized_start=2985 - _NOOPREQUESTDATA._serialized_end=3034 - _NOOPRESPONSEDATA._serialized_start=3036 - _NOOPRESPONSEDATA._serialized_end=3054 - _PINGREQUESTDATA._serialized_start=3056 - _PINGREQUESTDATA._serialized_end=3105 - _PINGRESPONSEDATA._serialized_start=3107 - _PINGRESPONSEDATA._serialized_end=3125 - _REGISTERPCBREQUESTDATA._serialized_start=3128 - _REGISTERPCBREQUESTDATA._serialized_end=3462 - _REGISTERPCBRESPONSEDATA._serialized_start=3465 - _REGISTERPCBRESPONSEDATA._serialized_end=3598 - _SAVEADSREQUESTDATA._serialized_start=3601 - _SAVEADSREQUESTDATA._serialized_end=3944 - _SAVEADSREQUESTDATA_ADSPLAYTIME._serialized_start=3887 - _SAVEADSREQUESTDATA_ADSPLAYTIME._serialized_end=3944 - _SAVEADSRESPONSEDATA._serialized_start=3946 - _SAVEADSRESPONSEDATA._serialized_end=3967 - _CHECKACCESSCODEREQUESTDATA._serialized_start=3969 - _CHECKACCESSCODEREQUESTDATA._serialized_end=4046 - _CHECKACCESSCODERESPONSEDATA._serialized_start=4048 - _CHECKACCESSCODERESPONSEDATA._serialized_end=4125 - _SETBNPASSIDLOCKREQUESTDATA._serialized_start=4128 - _SETBNPASSIDLOCKREQUESTDATA._serialized_end=4292 - _SETBNPASSIDLOCKRESPONSEDATA._serialized_start=4294 - _SETBNPASSIDLOCKRESPONSEDATA._serialized_end=4323 - _LOADUSERREQUESTDATA._serialized_start=4326 - _LOADUSERREQUESTDATA._serialized_end=4459 - _LOADUSERRESPONSEDATA._serialized_start=4462 - _LOADUSERRESPONSEDATA._serialized_end=6890 - _LOADUSERRESPONSEDATA_POKEMONDATA._serialized_start=6522 - _LOADUSERRESPONSEDATA_POKEMONDATA._serialized_end=6890 - _SAVEUSERREQUESTDATA._serialized_start=6893 - _SAVEUSERREQUESTDATA._serialized_end=8453 - _SAVEUSERREQUESTDATA_BATTLEDATA._serialized_start=7948 - _SAVEUSERREQUESTDATA_BATTLEDATA._serialized_end=8184 - _SAVEUSERREQUESTDATA_POKEMONDATA._serialized_start=8187 - _SAVEUSERREQUESTDATA_POKEMONDATA._serialized_end=8366 - _SAVEUSERREQUESTDATA_REWARDDATA._serialized_start=8368 - _SAVEUSERREQUESTDATA_REWARDDATA._serialized_end=8453 - _SAVEUSERRESPONSEDATA._serialized_start=8455 - _SAVEUSERRESPONSEDATA._serialized_end=8477 - _CHECKDIAGNOSISREQUESTDATA._serialized_start=8479 - _CHECKDIAGNOSISREQUESTDATA._serialized_end=8538 - _CHECKDIAGNOSISRESPONSEDATA._serialized_start=8541 - _CHECKDIAGNOSISRESPONSEDATA._serialized_end=8818 - _CHECKDIAGNOSISRESPONSEDATA_DIAGNOSISDATA._serialized_start=8655 - _CHECKDIAGNOSISRESPONSEDATA_DIAGNOSISDATA._serialized_end=8818 - _SAVECLIENTLOGREQUESTDATA._serialized_start=8821 - _SAVECLIENTLOGREQUESTDATA._serialized_end=9065 - _SAVECLIENTLOGRESPONSEDATA._serialized_start=9067 - _SAVECLIENTLOGRESPONSEDATA._serialized_end=9094 - _PRELOADINFORMATIONREQUESTDATA._serialized_start=9096 - _PRELOADINFORMATIONREQUESTDATA._serialized_end=9182 - _PRELOADINFORMATIONRESPONSEDATA._serialized_start=9185 - _PRELOADINFORMATIONRESPONSEDATA._serialized_end=9386 - _LOADINFORMATIONREQUESTDATA._serialized_start=9388 - _LOADINFORMATIONREQUESTDATA._serialized_end=9506 - _LOADINFORMATIONRESPONSEDATA._serialized_start=9509 - _LOADINFORMATIONRESPONSEDATA._serialized_end=9659 - _PRESAVEREPLAYREQUESTDATA._serialized_start=9662 - _PRESAVEREPLAYREQUESTDATA._serialized_end=9790 - _PRESAVEREPLAYRESPONSEDATA._serialized_start=9792 - _PRESAVEREPLAYRESPONSEDATA._serialized_end=9898 - _SAVEREPLAYREQUESTDATA._serialized_start=9901 - _SAVEREPLAYREQUESTDATA._serialized_end=10159 - _SAVEREPLAYRESPONSEDATA._serialized_start=10161 - _SAVEREPLAYRESPONSEDATA._serialized_end=10185 - _SAVECHARGEREQUESTDATA._serialized_start=10188 - _SAVECHARGEREQUESTDATA._serialized_end=10329 - _SAVECHARGERESPONSEDATA._serialized_start=10331 - _SAVECHARGERESPONSEDATA._serialized_end=10409 - _CHECKRANKINGREQUESTDATA._serialized_start=10411 - _CHECKRANKINGREQUESTDATA._serialized_end=10528 - _CHECKRANKINGRESPONSEDATA._serialized_start=10530 - _CHECKRANKINGRESPONSEDATA._serialized_end=10576 - _LOADRANKINGREQUESTDATA._serialized_start=10578 - _LOADRANKINGREQUESTDATA._serialized_end=10675 - _LOADRANKINGRESPONSEDATA._serialized_start=10678 - _LOADRANKINGRESPONSEDATA._serialized_end=11763 - _LOADRANKINGRESPONSEDATA_TRAINERDATA._serialized_start=10907 - _LOADRANKINGRESPONSEDATA_TRAINERDATA._serialized_end=11763 - _SAVEINGAMELOGREQUESTDATA._serialized_start=11765 - _SAVEINGAMELOGREQUESTDATA._serialized_end=11869 - _SAVEINGAMELOGRESPONSEDATA._serialized_start=11871 - _SAVEINGAMELOGRESPONSEDATA._serialized_end=11898 - _PRELOADINFORMATIONATTRACTREQUESTDATA._serialized_start=11900 - _PRELOADINFORMATIONATTRACTREQUESTDATA._serialized_end=11993 - _PRELOADINFORMATIONATTRACTRESPONSEDATA._serialized_start=11996 - _PRELOADINFORMATIONATTRACTRESPONSEDATA._serialized_end=12204 - _LOADINFORMATIONATTRACTREQUESTDATA._serialized_start=12206 - _LOADINFORMATIONATTRACTREQUESTDATA._serialized_end=12331 - _LOADINFORMATIONATTRACTRESPONSEDATA._serialized_start=12334 - _LOADINFORMATIONATTRACTRESPONSEDATA._serialized_end=12491 - _LOADCLIENTSETTINGSREQUESTDATA._serialized_start=12493 - _LOADCLIENTSETTINGSREQUESTDATA._serialized_end=12556 - _LOADCLIENTSETTINGSRESPONSEDATA._serialized_start=12559 - _LOADCLIENTSETTINGSRESPONSEDATA._serialized_end=14620 - _LOADCLIENTSETTINGSRESPONSEDATA_EVENTINFO._serialized_start=13482 - _LOADCLIENTSETTINGSRESPONSEDATA_EVENTINFO._serialized_end=13706 - _LOADCLIENTSETTINGSRESPONSEDATA_BANNERINFO._serialized_start=13709 - _LOADCLIENTSETTINGSRESPONSEDATA_BANNERINFO._serialized_end=13871 - _LOADCLIENTSETTINGSRESPONSEDATA_ATTRACTINFO._serialized_start=13874 - _LOADCLIENTSETTINGSRESPONSEDATA_ATTRACTINFO._serialized_end=14134 - _LOADCLIENTSETTINGSRESPONSEDATA_INFOWINDOW._serialized_start=14137 - _LOADCLIENTSETTINGSRESPONSEDATA_INFOWINDOW._serialized_end=14388 - _LOADCLIENTSETTINGSRESPONSEDATA_LUCKYBONUS._serialized_start=14390 - _LOADCLIENTSETTINGSRESPONSEDATA_LUCKYBONUS._serialized_end=14500 - _LOADCLIENTSETTINGSRESPONSEDATA_SPECIALBONUS._serialized_start=14502 - _LOADCLIENTSETTINGSRESPONSEDATA_SPECIALBONUS._serialized_end=14620 + DESCRIPTOR._options = None + _MESSAGETYPE._serialized_start = 14623 + _MESSAGETYPE._serialized_end = 15057 + _REQUEST._serialized_start = 34 + _REQUEST._serialized_end = 1488 + _RESPONSE._serialized_start = 1491 + _RESPONSE._serialized_end = 2983 + _NOOPREQUESTDATA._serialized_start = 2985 + _NOOPREQUESTDATA._serialized_end = 3034 + _NOOPRESPONSEDATA._serialized_start = 3036 + _NOOPRESPONSEDATA._serialized_end = 3054 + _PINGREQUESTDATA._serialized_start = 3056 + _PINGREQUESTDATA._serialized_end = 3105 + _PINGRESPONSEDATA._serialized_start = 3107 + _PINGRESPONSEDATA._serialized_end = 3125 + _REGISTERPCBREQUESTDATA._serialized_start = 3128 + _REGISTERPCBREQUESTDATA._serialized_end = 3462 + _REGISTERPCBRESPONSEDATA._serialized_start = 3465 + _REGISTERPCBRESPONSEDATA._serialized_end = 3598 + _SAVEADSREQUESTDATA._serialized_start = 3601 + _SAVEADSREQUESTDATA._serialized_end = 3944 + _SAVEADSREQUESTDATA_ADSPLAYTIME._serialized_start = 3887 + _SAVEADSREQUESTDATA_ADSPLAYTIME._serialized_end = 3944 + _SAVEADSRESPONSEDATA._serialized_start = 3946 + _SAVEADSRESPONSEDATA._serialized_end = 3967 + _CHECKACCESSCODEREQUESTDATA._serialized_start = 3969 + _CHECKACCESSCODEREQUESTDATA._serialized_end = 4046 + _CHECKACCESSCODERESPONSEDATA._serialized_start = 4048 + _CHECKACCESSCODERESPONSEDATA._serialized_end = 4125 + _SETBNPASSIDLOCKREQUESTDATA._serialized_start = 4128 + _SETBNPASSIDLOCKREQUESTDATA._serialized_end = 4292 + _SETBNPASSIDLOCKRESPONSEDATA._serialized_start = 4294 + _SETBNPASSIDLOCKRESPONSEDATA._serialized_end = 4323 + _LOADUSERREQUESTDATA._serialized_start = 4326 + _LOADUSERREQUESTDATA._serialized_end = 4459 + _LOADUSERRESPONSEDATA._serialized_start = 4462 + _LOADUSERRESPONSEDATA._serialized_end = 6890 + _LOADUSERRESPONSEDATA_POKEMONDATA._serialized_start = 6522 + _LOADUSERRESPONSEDATA_POKEMONDATA._serialized_end = 6890 + _SAVEUSERREQUESTDATA._serialized_start = 6893 + _SAVEUSERREQUESTDATA._serialized_end = 8453 + _SAVEUSERREQUESTDATA_BATTLEDATA._serialized_start = 7948 + _SAVEUSERREQUESTDATA_BATTLEDATA._serialized_end = 8184 + _SAVEUSERREQUESTDATA_POKEMONDATA._serialized_start = 8187 + _SAVEUSERREQUESTDATA_POKEMONDATA._serialized_end = 8366 + _SAVEUSERREQUESTDATA_REWARDDATA._serialized_start = 8368 + _SAVEUSERREQUESTDATA_REWARDDATA._serialized_end = 8453 + _SAVEUSERRESPONSEDATA._serialized_start = 8455 + _SAVEUSERRESPONSEDATA._serialized_end = 8477 + _CHECKDIAGNOSISREQUESTDATA._serialized_start = 8479 + _CHECKDIAGNOSISREQUESTDATA._serialized_end = 8538 + _CHECKDIAGNOSISRESPONSEDATA._serialized_start = 8541 + _CHECKDIAGNOSISRESPONSEDATA._serialized_end = 8818 + _CHECKDIAGNOSISRESPONSEDATA_DIAGNOSISDATA._serialized_start = 8655 + _CHECKDIAGNOSISRESPONSEDATA_DIAGNOSISDATA._serialized_end = 8818 + _SAVECLIENTLOGREQUESTDATA._serialized_start = 8821 + _SAVECLIENTLOGREQUESTDATA._serialized_end = 9065 + _SAVECLIENTLOGRESPONSEDATA._serialized_start = 9067 + _SAVECLIENTLOGRESPONSEDATA._serialized_end = 9094 + _PRELOADINFORMATIONREQUESTDATA._serialized_start = 9096 + _PRELOADINFORMATIONREQUESTDATA._serialized_end = 9182 + _PRELOADINFORMATIONRESPONSEDATA._serialized_start = 9185 + _PRELOADINFORMATIONRESPONSEDATA._serialized_end = 9386 + _LOADINFORMATIONREQUESTDATA._serialized_start = 9388 + _LOADINFORMATIONREQUESTDATA._serialized_end = 9506 + _LOADINFORMATIONRESPONSEDATA._serialized_start = 9509 + _LOADINFORMATIONRESPONSEDATA._serialized_end = 9659 + _PRESAVEREPLAYREQUESTDATA._serialized_start = 9662 + _PRESAVEREPLAYREQUESTDATA._serialized_end = 9790 + _PRESAVEREPLAYRESPONSEDATA._serialized_start = 9792 + _PRESAVEREPLAYRESPONSEDATA._serialized_end = 9898 + _SAVEREPLAYREQUESTDATA._serialized_start = 9901 + _SAVEREPLAYREQUESTDATA._serialized_end = 10159 + _SAVEREPLAYRESPONSEDATA._serialized_start = 10161 + _SAVEREPLAYRESPONSEDATA._serialized_end = 10185 + _SAVECHARGEREQUESTDATA._serialized_start = 10188 + _SAVECHARGEREQUESTDATA._serialized_end = 10329 + _SAVECHARGERESPONSEDATA._serialized_start = 10331 + _SAVECHARGERESPONSEDATA._serialized_end = 10409 + _CHECKRANKINGREQUESTDATA._serialized_start = 10411 + _CHECKRANKINGREQUESTDATA._serialized_end = 10528 + _CHECKRANKINGRESPONSEDATA._serialized_start = 10530 + _CHECKRANKINGRESPONSEDATA._serialized_end = 10576 + _LOADRANKINGREQUESTDATA._serialized_start = 10578 + _LOADRANKINGREQUESTDATA._serialized_end = 10675 + _LOADRANKINGRESPONSEDATA._serialized_start = 10678 + _LOADRANKINGRESPONSEDATA._serialized_end = 11763 + _LOADRANKINGRESPONSEDATA_TRAINERDATA._serialized_start = 10907 + _LOADRANKINGRESPONSEDATA_TRAINERDATA._serialized_end = 11763 + _SAVEINGAMELOGREQUESTDATA._serialized_start = 11765 + _SAVEINGAMELOGREQUESTDATA._serialized_end = 11869 + _SAVEINGAMELOGRESPONSEDATA._serialized_start = 11871 + _SAVEINGAMELOGRESPONSEDATA._serialized_end = 11898 + _PRELOADINFORMATIONATTRACTREQUESTDATA._serialized_start = 11900 + _PRELOADINFORMATIONATTRACTREQUESTDATA._serialized_end = 11993 + _PRELOADINFORMATIONATTRACTRESPONSEDATA._serialized_start = 11996 + _PRELOADINFORMATIONATTRACTRESPONSEDATA._serialized_end = 12204 + _LOADINFORMATIONATTRACTREQUESTDATA._serialized_start = 12206 + _LOADINFORMATIONATTRACTREQUESTDATA._serialized_end = 12331 + _LOADINFORMATIONATTRACTRESPONSEDATA._serialized_start = 12334 + _LOADINFORMATIONATTRACTRESPONSEDATA._serialized_end = 12491 + _LOADCLIENTSETTINGSREQUESTDATA._serialized_start = 12493 + _LOADCLIENTSETTINGSREQUESTDATA._serialized_end = 12556 + _LOADCLIENTSETTINGSRESPONSEDATA._serialized_start = 12559 + _LOADCLIENTSETTINGSRESPONSEDATA._serialized_end = 14620 + _LOADCLIENTSETTINGSRESPONSEDATA_EVENTINFO._serialized_start = 13482 + _LOADCLIENTSETTINGSRESPONSEDATA_EVENTINFO._serialized_end = 13706 + _LOADCLIENTSETTINGSRESPONSEDATA_BANNERINFO._serialized_start = 13709 + _LOADCLIENTSETTINGSRESPONSEDATA_BANNERINFO._serialized_end = 13871 + _LOADCLIENTSETTINGSRESPONSEDATA_ATTRACTINFO._serialized_start = 13874 + _LOADCLIENTSETTINGSRESPONSEDATA_ATTRACTINFO._serialized_end = 14134 + _LOADCLIENTSETTINGSRESPONSEDATA_INFOWINDOW._serialized_start = 14137 + _LOADCLIENTSETTINGSRESPONSEDATA_INFOWINDOW._serialized_end = 14388 + _LOADCLIENTSETTINGSRESPONSEDATA_LUCKYBONUS._serialized_start = 14390 + _LOADCLIENTSETTINGSRESPONSEDATA_LUCKYBONUS._serialized_end = 14500 + _LOADCLIENTSETTINGSRESPONSEDATA_SPECIALBONUS._serialized_start = 14502 + _LOADCLIENTSETTINGSRESPONSEDATA_SPECIALBONUS._serialized_end = 14620 # @@protoc_insertion_point(module_scope) diff --git a/titles/pokken/schema/__init__.py b/titles/pokken/schema/__init__.py new file mode 100644 index 0000000..81b8132 --- /dev/null +++ b/titles/pokken/schema/__init__.py @@ -0,0 +1,4 @@ +from .profile import PokkenProfileData +from .match import PokkenMatchData +from .item import PokkenItemData +from .static import PokkenStaticData diff --git a/titles/pokken/schema/item.py b/titles/pokken/schema/item.py new file mode 100644 index 0000000..f68d6d9 --- /dev/null +++ b/titles/pokken/schema/item.py @@ -0,0 +1,50 @@ +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON +from sqlalchemy.schema import ForeignKey +from sqlalchemy.sql import func, select, update, delete +from sqlalchemy.engine import Row +from sqlalchemy.dialects.mysql import insert + +from core.data.schema import BaseData, metadata + +item = Table( + "pokken_item", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + unique=True, + ), + Column("category", Integer), + Column("content", Integer), + Column("type", Integer), + UniqueConstraint("user", "category", "content", "type", name="pokken_item_uk"), + mysql_charset="utf8mb4", +) + + +class PokkenItemData(BaseData): + """ + Items obtained as rewards + """ + + def add_reward(self, user_id: int, category: int, content: int, item_type: int) -> Optional[int]: + sql = insert(item).values( + user=user_id, + category=category, + content=content, + type=item_type, + ) + + conflict = sql.on_duplicate_key_update( + content=content, + ) + + result = self.execute(conflict) + if result is None: + self.logger.warning(f"Failed to insert reward for user {user_id}: {category}-{content}-{item_type}") + return None + return result.lastrowid diff --git a/titles/pokken/schema/match.py b/titles/pokken/schema/match.py new file mode 100644 index 0000000..c84ec63 --- /dev/null +++ b/titles/pokken/schema/match.py @@ -0,0 +1,52 @@ +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON +from sqlalchemy.schema import ForeignKey +from sqlalchemy.sql import func, select, update, delete +from sqlalchemy.engine import Row +from sqlalchemy.dialects.mysql import insert + +from core.data.schema import BaseData, metadata + +# Pokken sends depressingly little match data... +match_data = Table( + "pokken_match_data", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("num_games", Integer), + Column("play_modes", JSON), + Column("results", JSON), + Column("ex_ko_num", Integer), + Column("wko_num", Integer), + Column("timeup_win_num", Integer), + Column("cool_ko_num", Integer), + Column("perfect_ko_num", Integer), + Column("use_navi", Integer), + Column("use_navi_cloth", Integer), + Column("use_aid_skill", Integer), + Column("play_date", TIMESTAMP), + mysql_charset="utf8mb4", +) + + +class PokkenMatchData(BaseData): + """ + Match logs + """ + + def save_match(self, user_id: int, match_data: Dict) -> Optional[int]: + pass + + def get_match(self, match_id: int) -> Optional[Row]: + pass + + def get_matches_by_user(self, user_id: int) -> Optional[List[Row]]: + pass + + def get_matches(self, limit: int = 20) -> Optional[List[Row]]: + pass diff --git a/titles/pokken/schema/profile.py b/titles/pokken/schema/profile.py new file mode 100644 index 0000000..ab81d77 --- /dev/null +++ b/titles/pokken/schema/profile.py @@ -0,0 +1,381 @@ +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.schema import ForeignKey +from sqlalchemy.sql import func, select, update, delete +from sqlalchemy.sql.functions import coalesce +from sqlalchemy.engine import Row +from sqlalchemy.dialects.mysql import insert + +from core.data.schema import BaseData, metadata +from ..const import PokkenConstants + +# Some more of the repeated fields could probably be their own tables, for now I just did the ones that made sense to me +# Having the profile table be this massive kinda blows for updates but w/e, **kwargs to the rescue +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("home_region_code", Integer), + Column("home_loc_name", String(255)), + Column("pref_code", Integer), + Column("navi_newbie_flag", Boolean), + Column("navi_enable_flag", Boolean), + Column("pad_vibrate_flag", Boolean), + Column("trainer_rank_point", Integer), + Column("wallet", Integer), + Column("fight_money", Integer), + Column("score_point", Integer), + Column("grade_max_num", Integer), + Column("extra_counter", Integer), # Optional + Column("tutorial_progress_flag", JSON), # Repeated, Integer + Column("total_play_days", Integer), + Column("play_date_time", Integer), + Column("achievement_flag", JSON), # Repeated, Integer + Column("lucky_box_fail_num", Integer), + Column("event_reward_get_flag", Integer), + Column("rank_pvp_all", Integer), + Column("rank_pvp_loc", Integer), + Column("rank_cpu_all", Integer), + Column("rank_cpu_loc", Integer), + Column("rank_event", Integer), + Column("awake_num", Integer), + Column("use_support_num", Integer), + Column("rankmatch_flag", Integer), + Column("rankmatch_max", Integer), # Optional + Column("rankmatch_progress", JSON), # Repeated, Integer + Column("rankmatch_success", Integer), # Optional + Column("beat_num", Integer), # Optional + Column("title_text_id", Integer), + Column("title_plate_id", Integer), + Column("title_decoration_id", Integer), + Column("support_pokemon_list", JSON), # Repeated, Integer + Column("support_set_1_1", Integer), # Repeated, Integer + Column("support_set_1_2", Integer), + Column("support_set_2_1", Integer), # Repeated, Integer + Column("support_set_2_2", Integer), + Column("support_set_3_1", Integer), # Repeated, Integer + Column("support_set_3_2", Integer), + Column("navi_trainer", Integer), + Column("navi_version_id", Integer), + Column("aid_skill_list", JSON), # Repeated, Integer + Column("aid_skill", Integer), + Column("comment_text_id", Integer), + Column("comment_word_id", Integer), + Column("latest_use_pokemon", Integer), + Column("ex_ko_num", Integer), + Column("wko_num", Integer), + Column("timeup_win_num", Integer), + Column("cool_ko_num", Integer), + Column("perfect_ko_num", Integer), + Column("record_flag", Integer), + Column("continue_num", Integer), + Column("avatar_body", Integer), # Optional + Column("avatar_gender", Integer), # Optional + Column("avatar_background", Integer), # Optional + Column("avatar_head", Integer), # Optional + Column("avatar_battleglass", Integer), # Optional + Column("avatar_face0", Integer), # Optional + Column("avatar_face1", Integer), # Optional + Column("avatar_face2", Integer), # Optional + Column("avatar_bodyall", Integer), # Optional + Column("avatar_wear", Integer), # Optional + Column("avatar_accessory", Integer), # Optional + Column("avatar_stamp", Integer), # Optional + Column("event_state", Integer), + Column("event_id", Integer), + Column("sp_bonus_category_id_1", Integer), + Column("sp_bonus_key_value_1", Integer), + Column("sp_bonus_category_id_2", Integer), + Column("sp_bonus_key_value_2", Integer), + Column("last_play_event_id", Integer), # Optional + Column("event_achievement_flag", JSON), # Repeated, Integer + Column("event_achievement_param", JSON), # Repeated, Integer + Column("battle_num_vs_wan", Integer), # 4? + Column("win_vs_wan", Integer), + Column("battle_num_vs_lan", Integer), # 3? + Column("win_vs_lan", Integer), + Column("battle_num_vs_cpu", Integer), # 2 + Column("win_cpu", Integer), + Column("battle_num_tutorial", Integer), # 1? + 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("pokemon_exp", Integer), + Column("battle_num_vs_wan", Integer), # 4? + Column("win_vs_wan", Integer), + Column("battle_num_vs_lan", Integer), # 3? + Column("win_vs_lan", Integer), + Column("battle_num_vs_cpu", Integer), # 2 + Column("win_cpu", Integer), + Column("battle_all_num_tutorial", Integer), # ??? + Column("battle_num_tutorial", Integer), # 1? + Column("bp_point_atk", Integer), + 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", +) + + +class PokkenProfileData(BaseData): + def touch_profile(self, user_id: int) -> Optional[int]: + sql = select([profile.c.id]).where(profile.c.user == user_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone()['id'] + + def create_profile(self, user_id: int) -> Optional[int]: + sql = insert(profile).values(user=user_id) + conflict = sql.on_duplicate_key_update(user=user_id) + + result = self.execute(conflict) + if result is None: + self.logger.error(f"Failed to create pokken profile for user {user_id}!") + return None + return result.lastrowid + + 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, + avatar_gender=gender if gender is not None else profile.c.avatar_gender + ) + result = self.execute(sql) + if result is None: + self.logger.error( + f"Failed to update pokken profile name for user {user_id}!" + ) + + def put_extra( + self, + user_id: int, + extra_counter: int, + evt_reward_get_flg: int, + total_play_days: int, + awake_num: int, + use_support_ct: int, + beat_num: int, + aid_skill: int, + last_evt: int + ) -> None: + sql = update(profile).where(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, + beat_num=beat_num, + aid_skill=aid_skill, + last_play_event_id=last_evt + ) + + result = self.execute(sql) + if result is None: + self.logger.error(f"Failed to put extra data for user {user_id}") + + def update_profile_tutorial_flags(self, user_id: int, tutorial_flags: List) -> None: + sql = update(profile).where(profile.c.user == user_id).values( + tutorial_progress_flag=tutorial_flags, + ) + result = self.execute(sql) + if result is None: + self.logger.error( + f"Failed to update pokken profile tutorial flags for user {user_id}!" + ) + + def update_profile_achievement_flags(self, user_id: int, achievement_flags: List) -> None: + sql = update(profile).where(profile.c.user == user_id).values( + achievement_flag=achievement_flags, + ) + result = self.execute(sql) + if result is None: + self.logger.error( + f"Failed to update pokken profile achievement flags for user {user_id}!" + ) + + 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( + event_state=event_state, + event_achievement_flag=event_flags, + event_achievement_param=event_param, + last_play_event_id=last_evt if last_evt is not None else profile.c.last_play_event_id, + ) + result = self.execute(sql) + if result is None: + self.logger.error( + f"Failed to update pokken profile event state for user {user_id}!" + ) + + 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, + grade_max_num = grade_max + ) + + def get_profile(self, user_id: int) -> Optional[Row]: + sql = profile.select(profile.c.user == user_id) + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def put_pokemon( + self, + user_id: int, + pokemon_id: int, + illust_no: int, + atk: int, + res: int, + defe: int, + sp: int + ) -> Optional[int]: + sql = insert(pokemon_data).values( + user=user_id, + char_id=pokemon_id, + illustration_book_no=illust_no, + pokemon_exp=0, + battle_num_vs_wan=0, + win_vs_wan=0, + battle_num_vs_lan=0, + win_vs_lan=0, + battle_num_vs_cpu=0, + win_cpu=0, + battle_all_num_tutorial=0, + battle_num_tutorial=0, + bp_point_atk=1+atk, + bp_point_res=1+res, + bp_point_def=1+defe, + bp_point_sp=1+sp, + ) + + conflict = sql.on_duplicate_key_update( + illustration_book_no=illust_no, + 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, + bp_point_sp=pokemon_data.c.bp_point_sp + sp, + ) + + result = self.execute(conflict) + if result is None: + self.logger.warning(f"Failed to insert pokemon ID {pokemon_id} for user {user_id}") + return None + return result.lastrowid + + def add_pokemon_xp( + self, + user_id: int, + 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( + pokemon_exp=coalesce(pokemon_data.c.pokemon_exp, 0) + xp + ) + + result = self.execute(sql) + if result is None: + self.logger.warning(f"Failed to add {xp} XP to pokemon ID {pokemon_id} for user {user_id}") + + 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)) + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_all_pokemon_data(self, user_id: int) -> Optional[List[Row]]: + sql = pokemon_data.select(pokemon_data.c.user == user_id) + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def put_pokemon_battle_result( + self, user_id: int, pokemon_id: int, match_type: PokkenConstants.BATTLE_TYPE, match_result: PokkenConstants.BATTLE_RESULT + ) -> None: + """ + 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( + 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), + + battle_num_vs_cpu=coalesce(pokemon_data.c.battle_num_vs_cpu, 0) + 1 if match_type==PokkenConstants.BATTLE_TYPE.AI else coalesce(pokemon_data.c.battle_num_vs_cpu, 0), + win_cpu=coalesce(pokemon_data.c.win_cpu, 0) + 1 if match_type==PokkenConstants.BATTLE_TYPE.AI and match_result==PokkenConstants.BATTLE_RESULT.WIN else coalesce(pokemon_data.c.win_cpu, 0), + + battle_num_vs_lan=coalesce(pokemon_data.c.battle_num_vs_lan, 0) + 1 if match_type==PokkenConstants.BATTLE_TYPE.LAN else coalesce(pokemon_data.c.battle_num_vs_lan, 0), + win_vs_lan=coalesce(pokemon_data.c.win_vs_lan, 0) + 1 if match_type==PokkenConstants.BATTLE_TYPE.LAN and match_result==PokkenConstants.BATTLE_RESULT.WIN else coalesce(pokemon_data.c.win_vs_lan, 0), + + battle_num_vs_wan=coalesce(pokemon_data.c.battle_num_vs_wan, 0) + 1 if match_type==PokkenConstants.BATTLE_TYPE.WAN else coalesce(pokemon_data.c.battle_num_vs_wan, 0), + win_vs_wan=coalesce(pokemon_data.c.win_vs_wan, 0) + 1 if match_type==PokkenConstants.BATTLE_TYPE.WAN and match_result==PokkenConstants.BATTLE_RESULT.WIN else coalesce(pokemon_data.c.win_vs_wan, 0), + ) + + result = self.execute(sql) + if result is None: + self.logger.warning(f"Failed to record match stats for user {user_id}'s pokemon {pokemon_id} (type {match_type.name} | result {match_result.name})") + + def put_stats( + self, + user_id: int, + exkos: int, + wkos: int, + timeout_wins: int, + cool_kos: int, + perfects: int, + continues: int, + ) -> None: + """ + Records profile stats + """ + sql = update(profile).where(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, + cool_ko_num=coalesce(profile.c.cool_ko_num, 0) + cool_kos, + perfect_ko_num=coalesce(profile.c.perfect_ko_num, 0) + perfects, + continue_num=continues, + ) + + result = self.execute(sql) + if result is None: + self.logger.warning(f"Failed to update stats for user {user_id}") + + 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( + 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, + support_set_2_2=support2 if support_id == 2 else profile.c.support_set_2_2, + support_set_3_1=support1 if support_id == 3 else profile.c.support_set_3_1, + support_set_3_2=support2 if support_id == 3 else profile.c.support_set_3_2, + ) + + result = self.execute(sql) + if result is None: + self.logger.warning(f"Failed to update support team {support_id} for user {user_id}") diff --git a/titles/pokken/schema/static.py b/titles/pokken/schema/static.py new file mode 100644 index 0000000..121ebc4 --- /dev/null +++ b/titles/pokken/schema/static.py @@ -0,0 +1,13 @@ +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON +from sqlalchemy.schema import ForeignKey +from sqlalchemy.sql import func, select, update, delete +from sqlalchemy.engine import Row +from sqlalchemy.dialects.mysql import insert + +from core.data.schema import BaseData, metadata + + +class PokkenStaticData(BaseData): + pass diff --git a/titles/pokken/services.py b/titles/pokken/services.py new file mode 100644 index 0000000..952c232 --- /dev/null +++ b/titles/pokken/services.py @@ -0,0 +1,66 @@ +from twisted.internet.interfaces import IAddress +from twisted.internet.protocol import Protocol +from autobahn.twisted.websocket import WebSocketServerProtocol, WebSocketServerFactory +from autobahn.websocket.types import ConnectionRequest +from typing import Dict +import logging +import json + +from core.config import CoreConfig +from .config import PokkenConfig +from .base import PokkenBase + +class PokkenAdmissionProtocol(WebSocketServerProtocol): + def __init__(self, cfg: CoreConfig, game_cfg: PokkenConfig): + super().__init__() + self.core_config = cfg + self.game_config = game_cfg + self.logger = logging.getLogger("pokken") + + self.base = PokkenBase(cfg, game_cfg) + + def onConnect(self, request: ConnectionRequest) -> None: + self.logger.debug(f"Admission: Connection from {request.peer}") + + def onClose(self, wasClean: bool, code: int, reason: str) -> None: + self.logger.debug(f"Admission: Connection with {self.transport.getPeer().host} closed {'cleanly ' if wasClean else ''}with code {code} - {reason}") + + def onMessage(self, payload, isBinary: bool) -> None: + msg: Dict = json.loads(payload) + self.logger.debug(f"Admission: Message from {self.transport.getPeer().host}:{self.transport.getPeer().port} - {msg}") + + api = msg.get("api", "noop") + handler = getattr(self.base, f"handle_admission_{api.lower()}") + resp = handler(msg, self.transport.getPeer().host) + + if resp is None: + resp = {} + + if "type" not in resp: + resp['type'] = "res" + if "data" not in resp: + resp['data'] = {} + if "api" not in resp: + resp['api'] = api + if "result" not in resp: + resp['result'] = 'true' + + self.logger.debug(f"Websocket response: {resp}") + self.sendMessage(json.dumps(resp).encode(), isBinary) + +class PokkenAdmissionFactory(WebSocketServerFactory): + protocol = PokkenAdmissionProtocol + + def __init__( + self, + cfg: CoreConfig, + game_cfg: PokkenConfig + ) -> None: + self.core_config = cfg + self.game_config = game_cfg + super().__init__(f"ws://{self.game_config.server.hostname}:{self.game_config.ports.admission}") + + def buildProtocol(self, addr: IAddress) -> Protocol: + p = self.protocol(self.core_config, self.game_config) + p.factory = self + return p diff --git a/titles/sao/__init__.py b/titles/sao/__init__.py new file mode 100644 index 0000000..15a46f9 --- /dev/null +++ b/titles/sao/__init__.py @@ -0,0 +1,10 @@ +from .index import SaoServlet +from .const import SaoConstants +from .database import SaoData +from .read import SaoReader + +index = SaoServlet +database = SaoData +reader = SaoReader +game_codes = [SaoConstants.GAME_CODE] +current_schema_version = 1 diff --git a/titles/sao/base.py b/titles/sao/base.py new file mode 100644 index 0000000..09a9f88 --- /dev/null +++ b/titles/sao/base.py @@ -0,0 +1,973 @@ +import logging +from csv import * +from random import choice +from typing import Dict, List +from os import path + +from core.data import Data +from core import CoreConfig +from .config import SaoConfig +from .database import SaoData +from titles.sao.handlers.base import * + +class SaoBase: + def __init__(self, core_cfg: CoreConfig, game_cfg: SaoConfig) -> None: + self.core_cfg = core_cfg + self.game_cfg = game_cfg + self.core_data = Data(core_cfg) + self.game_data = SaoData(core_cfg) + self.version = 0 + self.logger = logging.getLogger("sao") + + def 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) + + return ret + + self.logger.warning(f"Failed to find csv file {file}.csv") + return ret + + 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() + + def handle_c122(self, header: SaoRequestHeader, request: bytes) -> bytes: + #common/get_maintenance_info + resp = SaoGetMaintResponse(header.cmd +1) + return resp.make() + + def handle_c12e(self, header: SaoRequestHeader, request: bytes) -> bytes: + #common/ac_cabinet_boot_notification + resp = SaoCommonAcCabinetBootNotificationResponse(header.cmd +1) + return resp.make() + + def handle_c100(self, header: SaoRequestHeader, request: bytes) -> bytes: + #common/get_app_versions + resp = SaoCommonGetAppVersionsRequest(header.cmd +1) + return resp.make() + + def handle_c102(self, header: SaoRequestHeader, request: bytes) -> bytes: + #common/master_data_version_check + resp = SaoMasterDataVersionCheckResponse(header.cmd +1) + return resp.make() + + def handle_c10a(self, header: SaoRequestHeader, request: bytes) -> bytes: + #common/paying_play_start + resp = SaoCommonPayingPlayStartRequest(header.cmd +1) + return resp.make() + + 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() + + def handle_c11e(self, header: SaoRequestHeader, request: bytes) -> bytes: + #common/get_auth_card_data + req = SaoGetAuthCardDataRequest(header, request) + + #Check authentication + user_id = self.core_data.card.get_user_id_from_card( req.access_code ) + + if not user_id: + user_id = self.core_data.user.create_user() #works + card_id = self.core_data.card.create_card(user_id, 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 = self.game_data.profile.create_profile(user_id) + self.game_data.item.put_hero_log(user_id, 101000010, 1, 0, 101000016, 0, 30086, 1001, 1002, 1003, 1005) + self.game_data.item.put_hero_log(user_id, 102000010, 1, 0, 103000006, 0, 30086, 1001, 1002, 1003, 1005) + self.game_data.item.put_hero_log(user_id, 103000010, 1, 0, 112000009, 0, 30086, 1001, 1002, 1003, 1005) + self.game_data.item.put_hero_party(user_id, 0, 101000010, 102000010, 103000010) + self.game_data.item.put_equipment_data(user_id, 101000016, 1, 200, 0, 0, 0) + self.game_data.item.put_equipment_data(user_id, 103000006, 1, 200, 0, 0, 0) + self.game_data.item.put_equipment_data(user_id, 112000009, 1, 200, 0, 0, 0) + self.game_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"User Authenticated: { req.access_code } | { user_id }") + + #Grab values from profile + profile_data = self.game_data.profile.get_profile(user_id) + + if user_id and not profile_data: + profile_id = self.game_data.profile.create_profile(user_id) + self.game_data.item.put_hero_log(user_id, 101000010, 1, 0, 101000016, 0, 30086, 1001, 1002, 1003, 1005) + self.game_data.item.put_hero_log(user_id, 102000010, 1, 0, 103000006, 0, 30086, 1001, 1002, 1003, 1005) + self.game_data.item.put_hero_log(user_id, 103000010, 1, 0, 112000009, 0, 30086, 1001, 1002, 1003, 1005) + self.game_data.item.put_hero_party(user_id, 0, 101000010, 102000010, 103000010) + self.game_data.item.put_equipment_data(user_id, 101000016, 1, 200, 0, 0, 0) + self.game_data.item.put_equipment_data(user_id, 103000006, 1, 200, 0, 0, 0) + self.game_data.item.put_equipment_data(user_id, 112000009, 1, 200, 0, 0, 0) + self.game_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 + + + profile_data = self.game_data.profile.get_profile(user_id) + + resp = SaoGetAuthCardDataResponse(header.cmd +1, profile_data) + return resp.make() + + def handle_c40c(self, header: SaoRequestHeader, request: bytes) -> bytes: + #home/check_ac_login_bonus + resp = SaoHomeCheckAcLoginBonusResponse(header.cmd +1) + return resp.make() + + def handle_c104(self, header: SaoRequestHeader, request: bytes) -> bytes: + #common/login + req = SaoCommonLoginRequest(header, request) + + user_id = self.core_data.card.get_user_id_from_card( req.access_code ) + profile_data = self.game_data.profile.get_profile(user_id) + + resp = SaoCommonLoginResponse(header.cmd +1, profile_data) + return resp.make() + + def handle_c404(self, header: SaoRequestHeader, request: bytes) -> bytes: + #home/check_comeback_event + resp = SaoCheckComebackEventRequest(header.cmd +1) + return resp.make() + + def handle_c000(self, header: SaoRequestHeader, request: bytes) -> bytes: + #ticket/ticket + resp = SaoTicketResponse(header.cmd +1) + return resp.make() + + def handle_c500(self, header: SaoRequestHeader, request: bytes) -> bytes: + #user_info/get_user_basic_data + req = SaoGetUserBasicDataRequest(header, request) + + profile_data = self.game_data.profile.get_profile(req.user_id) + + resp = SaoGetUserBasicDataResponse(header.cmd +1, profile_data) + return resp.make() + + def handle_c600(self, header: SaoRequestHeader, request: bytes) -> bytes: + #have_object/get_hero_log_user_data_list + req = SaoGetHeroLogUserDataListRequest(header, request) + + hero_data = self.game_data.item.get_hero_logs(req.user_id) + + resp = SaoGetHeroLogUserDataListResponse(header.cmd +1, hero_data) + return resp.make() + + def handle_c602(self, header: SaoRequestHeader, request: bytes) -> bytes: + #have_object/get_equipment_user_data_list + req = SaoGetEquipmentUserDataListRequest(header, request) + + equipment_data = self.game_data.item.get_user_equipments(req.user_id) + + resp = SaoGetEquipmentUserDataListResponse(header.cmd +1, equipment_data) + return resp.make() + + def handle_c604(self, header: SaoRequestHeader, request: bytes) -> bytes: + #have_object/get_item_user_data_list + req = SaoGetItemUserDataListRequest(header, request) + + item_data = self.game_data.item.get_user_items(req.user_id) + + resp = SaoGetItemUserDataListResponse(header.cmd +1, item_data) + return resp.make() + + def handle_c606(self, header: SaoRequestHeader, request: bytes) -> bytes: + #have_object/get_support_log_user_data_list + supportIdsData = self.game_data.static.get_support_log_ids(0, True) + + resp = SaoGetSupportLogUserDataListResponse(header.cmd +1, supportIdsData) + return resp.make() + + def handle_c800(self, header: SaoRequestHeader, request: bytes) -> bytes: + #custom/get_title_user_data_list + titleIdsData = self.game_data.static.get_title_ids(0, True) + + resp = SaoGetTitleUserDataListResponse(header.cmd +1, titleIdsData) + return resp.make() + + def handle_c608(self, header: SaoRequestHeader, request: bytes) -> bytes: + #have_object/get_episode_append_data_list + req = SaoGetEpisodeAppendDataListRequest(header, request) + + profile_data = self.game_data.profile.get_profile(req.user_id) + + resp = SaoGetEpisodeAppendDataListResponse(header.cmd +1, profile_data) + return resp.make() + + def handle_c804(self, header: SaoRequestHeader, request: bytes) -> bytes: + #custom/get_party_data_list + req = SaoGetPartyDataListRequest(header, request) + + hero_party = self.game_data.item.get_hero_party(req.user_id, 0) + hero1_data = self.game_data.item.get_hero_log(req.user_id, hero_party[3]) + hero2_data = self.game_data.item.get_hero_log(req.user_id, hero_party[4]) + hero3_data = self.game_data.item.get_hero_log(req.user_id, hero_party[5]) + + resp = SaoGetPartyDataListResponse(header.cmd +1, hero1_data, hero2_data, hero3_data) + return resp.make() + + 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() + + def handle_c124(self, header: SaoRequestHeader, request: bytes) -> bytes: + #common/get_resource_path_info + resp = SaoGetResourcePathInfoResponse(header.cmd +1) + return resp.make() + + def handle_c900(self, header: SaoRequestHeader, request: bytes) -> bytes: + #quest/get_quest_scene_user_data_list // QuestScene.csv + req = SaoGetQuestSceneUserDataListRequest(header, request) + + quest_data = self.game_data.item.get_quest_logs(req.user_id) + + resp = SaoGetQuestSceneUserDataListResponse(header.cmd +1, quest_data) + return resp.make() + + def handle_c400(self, header: SaoRequestHeader, request: bytes) -> bytes: + #home/check_yui_medal_get_condition + resp = SaoCheckYuiMedalGetConditionResponse(header.cmd +1) + return resp.make() + + def handle_c402(self, header: SaoRequestHeader, request: bytes) -> bytes: + #home/get_yui_medal_bonus_user_data + resp = SaoGetYuiMedalBonusUserDataResponse(header.cmd +1) + return resp.make() + + def handle_c40a(self, header: SaoRequestHeader, request: bytes) -> bytes: + #home/check_profile_card_used_reward + resp = SaoCheckProfileCardUsedRewardResponse(header.cmd +1) + return resp.make() + + def handle_c814(self, header: SaoRequestHeader, request: bytes) -> bytes: + #custom/synthesize_enhancement_hero_log + req = SaoSynthesizeEnhancementHeroLogRequest(header, request) + + synthesize_hero_log_data = self.game_data.item.get_hero_log(req.user_id, req.origin_user_hero_log_id) + + for x in req.material_common_reward_user_data_list: + hero_exp = 0 + itemList = self.game_data.static.get_item_id(x.user_common_reward_id) + heroList = self.game_data.static.get_hero_id(x.user_common_reward_id) + equipmentList = self.game_data.static.get_equipment_id(x.user_common_reward_id) + + if itemList: + hero_exp = 2000 + int(synthesize_hero_log_data["log_exp"]) + self.game_data.item.remove_item(req.user_id, x.user_common_reward_id) + + if equipmentList: + equipment_data = self.game_data.item.get_user_equipment(req.user_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"]) + self.game_data.item.remove_equipment(req.user_id, x.user_common_reward_id) + + if heroList: + hero_data = self.game_data.item.get_hero_log(req.user_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 + + hero_exp = int(hero_data["log_exp"]) + int(synthesize_hero_log_data["log_exp"]) + self.game_data.item.remove_hero_log(req.user_id, 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!") + + self.game_data.item.put_hero_log( + req.user_id, + int(req.origin_user_hero_log_id), + synthesize_hero_log_data["log_level"], + hero_exp, + synthesize_hero_log_data["main_weapon"], + synthesize_hero_log_data["sub_equipment"], + synthesize_hero_log_data["skill_slot1_skill_id"], + synthesize_hero_log_data["skill_slot2_skill_id"], + synthesize_hero_log_data["skill_slot3_skill_id"], + synthesize_hero_log_data["skill_slot4_skill_id"], + synthesize_hero_log_data["skill_slot5_skill_id"] + ) + + profile = self.game_data.profile.get_profile(req.user_id) + new_col = int(profile["own_col"]) - 100 + + # Update profile + + self.game_data.profile.put_profile( + req.user_id, + profile["user_type"], + profile["nick_name"], + profile["rank_num"], + profile["rank_exp"], + new_col, + profile["own_vp"], + profile["own_yui_medal"], + profile["setting_title_id"] + ) + + # Load the item again to push to the response handler + synthesize_hero_log_data = self.game_data.item.get_hero_log(req.user_id, req.origin_user_hero_log_id) + + resp = SaoSynthesizeEnhancementHeroLogResponse(header.cmd +1, synthesize_hero_log_data) + return resp.make() + + def handle_c816(self, header: SaoRequestHeader, request: bytes) -> bytes: + #custom/synthesize_enhancement_equipment + req_data = SaoSynthesizeEnhancementEquipmentRequest(header, request) + synthesize_equipment_data = self.game_data.item.get_user_equipment(req_data.user_id, req_data.origin_user_equipment_id) + + for x in req_data.material_common_reward_user_data_list: + equipment_exp = 0 + itemList = self.game_data.static.get_item_id(x.user_common_reward_id) + heroList = self.game_data.static.get_hero_id(x.user_common_reward_id) + equipmentList = self.game_data.static.get_equipment_id(x.user_common_reward_id) + + if itemList: + equipment_exp = 2000 + int(synthesize_equipment_data["enhancement_exp"]) + self.game_data.item.remove_item(req_data.user_id, x.user_common_reward_id) + + if equipmentList: + equipment_data = self.game_data.item.get_user_equipment(req_data.user_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 + + equipment_exp = int(equipment_data["enhancement_exp"]) + int(synthesize_equipment_data["enhancement_exp"]) + self.game_data.item.remove_equipment(req_data.user_id, x.user_common_reward_id) + + if heroList: + hero_data = self.game_data.item.get_hero_log(req_data.user_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"]) + self.game_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 + + self.game_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 = self.game_data.profile.get_profile(req_data.user_id) + new_col = int(profile["own_col"]) - 100 + + # Update profile + + self.game_data.profile.put_profile( + req_data.user_id, + profile["user_type"], + profile["nick_name"], + profile["rank_num"], + profile["rank_exp"], + new_col, + profile["own_vp"], + profile["own_yui_medal"], + profile["setting_title_id"] + ) + + # Load the item again to push to the response handler + synthesize_equipment_data = self.game_data.item.get_user_equipment(req_data.user_id, req_data.origin_user_equipment_id) + + resp = SaoSynthesizeEnhancementEquipmentResponse(header.cmd +1, synthesize_equipment_data) + return resp.make() + + def handle_c806(self, header: SaoRequestHeader, request: bytes) -> 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 = self.game_data.item.get_hero_log(req_data.user_id, party_team.user_hero_log_id) + hero_level = 1 + hero_exp = 0 + + if hero_data: + hero_level = hero_data["log_level"] + hero_exp = hero_data["log_exp"] + + self.game_data.item.put_hero_log( + 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_hero_list.append(party_team.user_hero_log_id) + + self.game_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) + return resp.make() + + def handle_c904(self, header: SaoRequestHeader, request: bytes) -> bytes: + #quest/episode_play_start + req_data = SaoEpisodePlayStartRequest(header, request) + + user_id = req_data.user_id + profile_data = self.game_data.profile.get_profile(user_id) + + self.game_data.item.create_session( + user_id, + int(req_data.play_start_request_data[0].user_party_id), + req_data.episode_id, + req_data.play_mode, + req_data.play_start_request_data[0].quest_drop_boost_apply_flag + ) + + resp = SaoEpisodePlayStartResponse(header.cmd +1, profile_data) + return resp.make() + + def handle_c908(self, header: SaoRequestHeader, request: bytes) -> bytes: # Level calculation missing for the profile and heroes + #quest/episode_play_end + + req_data = SaoEpisodePlayEndRequest(header, request) + + # 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 + + profile = self.game_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) + + 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 = self.game_data.static.get_quests_id(episode_id) + episode_id = questId[2] + + self.game_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 + + + # Calculate level based off experience and the CSV list + with open(r'titles/sao/data/PlayerRank.csv') as csv_file: + csv_reader = csv.reader(csv_file, delimiter=',') + line_count = 0 + data = [] + rowf = False + for row in csv_reader: + if rowf==False: + rowf=True + else: + data.append(row) + + for i in range(0,len(data)): + if exp>=int(data[i][1]) and exp=int(data[e][1]) and log_exp 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 = self.game_data.profile.get_profile(user_id) + + self.game_data.item.create_session( + user_id, + int(req_data.play_start_request_data[0].user_party_id), + req_data.trial_tower_id, + req_data.play_mode, + req_data.play_start_request_data[0].quest_drop_boost_apply_flag + ) + + resp = SaoEpisodePlayStartResponse(header.cmd +1, profile_data) + return resp.make() + + def handle_c918(self, header: SaoRequestHeader, request: bytes) -> bytes: + #quest/trial_tower_play_end + req_data = SaoTrialTowerPlayEndRequest(header, request) + + # 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 + + self.game_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 = self.game_data.item.get_quest_log(user_id, next_tower_id) + if not checkQuest: + if next_tower_id != 3101: + self.game_data.item.put_player_quest(user_id, next_tower_id, 0, 0, 0, 0, 0) + + # Update the profile + profile = self.game_data.profile.get_profile(user_id) + + 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) + + # Calculate level based off experience and the CSV list + with open(r'titles/sao/data/PlayerRank.csv') as csv_file: + csv_reader = csv.reader(csv_file, delimiter=',') + line_count = 0 + data = [] + rowf = False + for row in csv_reader: + if rowf==False: + rowf=True + else: + data.append(row) + + for i in range(0,len(data)): + if exp>=int(data[i][1]) and exp=int(data[e][1]) and log_exp bytes: + #quest/episode_play_end_unanalyzed_log_fixed + + req = SaoEpisodePlayEndUnanalyzedLogFixedRequest(header, request) + + end_session_data = self.game_data.item.get_end_session(req.user_id) + + resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(header.cmd +1, end_session_data[4]) + return resp.make() + + def handle_c91a(self, header: SaoRequestHeader, request: bytes) -> bytes: # handler is identical to the episode + #quest/trial_tower_play_end_unanalyzed_log_fixed + req = TrialTowerPlayEndUnanalyzedLogFixed(header, request) + + end_session_data = self.game_data.item.get_end_session(req.user_id) + + resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(header.cmd +1, end_session_data[4]) + return resp.make() + + def handle_cd00(self, header: SaoRequestHeader, request: bytes) -> bytes: + #defrag_match/get_defrag_match_basic_data + resp = SaoGetDefragMatchBasicDataResponse(header.cmd +1) + return resp.make() + + def handle_cd02(self, header: SaoRequestHeader, request: bytes) -> bytes: + #defrag_match/get_defrag_match_ranking_user_data + resp = SaoGetDefragMatchRankingUserDataResponse(header.cmd +1) + return resp.make() + + def handle_cd04(self, header: SaoRequestHeader, request: bytes) -> bytes: + #defrag_match/get_defrag_match_league_point_ranking_list + resp = SaoGetDefragMatchLeaguePointRankingListResponse(header.cmd +1) + return resp.make() + + def handle_cd06(self, header: SaoRequestHeader, request: bytes) -> bytes: + #defrag_match/get_defrag_match_league_score_ranking_list + resp = SaoGetDefragMatchLeagueScoreRankingListResponse(header.cmd +1) + return resp.make() + + def handle_d404(self, header: SaoRequestHeader, request: bytes) -> bytes: + #other/bnid_serial_code_check + resp = SaoBnidSerialCodeCheckResponse(header.cmd +1) + return resp.make() + + def handle_c306(self, header: SaoRequestHeader, request: bytes) -> bytes: + #card/scan_qr_quest_profile_card + resp = SaoScanQrQuestProfileCardResponse(header.cmd +1) + return resp.make() + + 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() + + 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() + + def handle_cf0e(self, header: SaoRequestHeader, request: bytes) -> bytes: + # gasha/get_gasha_medal_shop_user_data_list + # TODO: Get user shop data + req = GetGashaMedalShopUserDataListRequest(header, request) + resp = GetGashaMedalShopUserDataListResponse(header.cmd + 1) + return resp.make() + + def handle_d5da(self, header: SaoRequestHeader, request: bytes) -> 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") + return resp.make() + + def handle_d5dc(self, header: SaoRequestHeader, request: bytes) -> 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") + return resp.make() + + def handle_d5fc(self, header: SaoRequestHeader, request: bytes) -> 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") + return resp.make() + + def handle_d5fe(self, header: SaoRequestHeader, request: bytes) -> bytes: + # master_data/get_m_gasha_medal_shop_items + return SaoNoopResponse(header.cmd + 1).make() + + 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() + + 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() \ No newline at end of file diff --git a/titles/sao/config.py b/titles/sao/config.py new file mode 100644 index 0000000..dfae3c0 --- /dev/null +++ b/titles/sao/config.py @@ -0,0 +1,81 @@ +from core.config import CoreConfig + + +class SaoServerConfig: + def __init__(self, parent_config: "SaoConfig"): + self.__config = parent_config + + @property + def enable(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "sao", "server", "enable", default=True + ) + + @property + def loglevel(self) -> int: + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "sao", "server", "loglevel", default="info" + ) + ) + + @property + def auto_register(self) -> bool: + """ + Automatically register users in `aime_user` on first carding in with sao + if they don't exist already. Set to false to display an error instead. + """ + return CoreConfig.get_config_field( + self.__config, "sao", "server", "auto_register", default=True + ) + + @property + def use_https(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "sao", "server", "use_https", default=False + ) + +class SaoCryptConfig: + def __init__(self, parent_config: "SaoConfig"): + self.__config = parent_config + + @property + def enable(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "sao", "crypt", "enable", default=False + ) + + @property + def key(self) -> str: + 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"): + self.__config = parent_config + + @property + def verify_hash(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "sao", "hash", "verify_hash", default=False + ) + + @property + def hash_base(self) -> str: + return CoreConfig.get_config_field( + self.__config, "sao", "hash", "hash_base", default="" + ) + + +class SaoConfig(dict): + def __init__(self) -> None: + self.server = SaoServerConfig(self) + self.crypt = SaoCryptConfig(self) + self.hash = SaoHashConfig(self) diff --git a/titles/sao/const.py b/titles/sao/const.py new file mode 100644 index 0000000..8bdea0f --- /dev/null +++ b/titles/sao/const.py @@ -0,0 +1,15 @@ +from enum import Enum + + +class SaoConstants: + GAME_CODE = "SDEW" + + CONFIG_NAME = "sao.yaml" + + VER_SAO = 0 + + VERSION_NAMES = ("Sword Art Online Arcade") + + @classmethod + def game_ver_to_string(cls, ver: int): + return cls.VERSION_NAMES[ver] diff --git a/titles/sao/data/EquipmentLevel.csv b/titles/sao/data/EquipmentLevel.csv new file mode 100644 index 0000000..803bf7e --- /dev/null +++ b/titles/sao/data/EquipmentLevel.csv @@ -0,0 +1,121 @@ +EquipmentLevelId,RequireExp +1,200, +2,400, +3,600, +4,800, +5,1000, +6,1200, +7,1400, +8,1600, +9,1800, +10,2000, +11,2200, +12,2400, +13,2600, +14,2800, +15,3000, +16,3200, +17,3400, +18,3600, +19,3800, +20,4000, +21,4200, +22,4400, +23,4600, +24,4800, +25,5000, +26,5200, +27,5400, +28,5600, +29,5800, +30,6000, +31,6200, +32,6400, +33,6600, +34,6800, +35,7000, +36,7200, +37,7400, +38,7600, +39,7800, +40,8000, +41,8200, +42,8400, +43,8600, +44,8800, +45,9000, +46,9200, +47,9400, +48,9600, +49,9800, +50,10000, +51,10200, +52,10400, +53,10600, +54,10800, +55,11000, +56,11200, +57,11400, +58,11600, +59,11800, +60,12000, +61,12200, +62,12400, +63,12600, +64,12800, +65,13000, +66,13200, +67,13400, +68,13600, +69,13800, +70,14000, +71,14200, +72,14400, +73,14600, +74,14800, +75,15000, +76,15200, +77,15400, +78,15600, +79,15800, +80,16000, +81,16200, +82,16400, +83,16600, +84,16800, +85,17000, +86,17200, +87,17400, +88,17600, +89,17800, +90,18000, +91,18200, +92,18400, +93,18600, +94,18800, +95,19000, +96,19200, +97,19400, +98,19600, +99,19800, +100,100000, +101,150000, +102,200000, +103,250000, +104,300000, +105,350000, +106,400000, +107,450000, +108,500000, +109,550000, +110,600000, +111,650000, +112,700000, +113,750000, +114,800000, +115,850000, +116,900000, +117,950000, +118,1000000, +119,1000000, +120,1000000, diff --git a/titles/sao/data/GashaMedalShopItems.csv b/titles/sao/data/GashaMedalShopItems.csv new file mode 100644 index 0000000..c5a46ae --- /dev/null +++ b/titles/sao/data/GashaMedalShopItems.csv @@ -0,0 +1,882 @@ +GashaMedalShopItemId,GashaMedalShopId,CommonRewardType,CommonRewardId,CommonRewardNum,Strength,Property1PropertyId,Property1Value1,Property1Value2,Property2PropertyId,Property2Value1,Property2Value2,Property3PropertyId,Property3Value1,Property3Value2,Property4PropertyId,Property4Value1,Property4Value2 +15701,15701,1,101000220,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +15702,15702,1,102000230,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +15703,15703,2,101000009,1,0,204000,5,0,1,0,0,2,0,0,2,0,0 +15801,15801,8,212000030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +15802,15802,8,201000060,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +15803,15803,8,297000090,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +15901,15901,1,118000070,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +15902,15902,1,118000050,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +15903,15903,1,118000060,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +15904,15904,2,101000016,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +16301,16301,8,209000090,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +16302,16302,8,202000110,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +16303,16303,8,205000070,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +16304,16304,8,206000070,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +16401,16401,8,212000030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +16402,16402,8,201000060,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +16403,16403,8,297000090,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +16501,16501,1,101000220,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +16502,16502,1,102000230,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +16503,16503,2,101000009,1,0,204000,5,0,1,0,0,2,0,0,2,0,0 +16601,16601,1,105000190,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +16602,16602,1,106000190,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +16603,16603,2,103000007,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +16604,16604,2,109000006,1,0,204600,25,0,1,0,0,2,0,0,2,0,0 +16801,16801,1,102000300,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +16802,16802,1,103000260,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +16803,16803,1,104000320,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +16804,16804,2,101000018,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +16805,16805,2,102000009,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +16806,16806,2,111000008,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +17201,17201,1,109000250,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +17202,17202,1,112000120,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +17203,17203,2,101000011,1,0,209900,30,0,1,0,0,2,0,0,2,0,0 +17204,17204,2,101000014,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +17301,17301,8,204000090,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +17302,17302,8,209000100,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +17303,17303,8,201000080,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +17304,17304,8,202000140,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +17305,17305,8,203000130,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +17306,17306,8,205000090,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +17307,17307,8,206000100,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +17308,17308,8,211000050,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +17601,17601,1,111000200,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +17602,17602,1,106000250,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +17603,17603,1,105000230,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +17604,17604,2,101000012,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +17605,17605,2,103000004,1,0,204100,0,0,204800,5,0,2,0,0,2,0,0 +17606,17606,2,109000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +17607,17607,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +17608,17608,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +17609,17609,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +17610,17610,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +17611,17611,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +18001,18001,1,101000280,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +18002,18002,1,102000320,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +18003,18003,1,103000280,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +18004,18004,2,101000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +18005,18005,2,102000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +18006,18006,2,105000004,1,0,202700,50,0,204700,1,0,2,0,0,2,0,0 +18007,18007,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +18008,18008,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +18009,18009,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +18010,18010,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +18011,18011,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +18401,18401,1,102000170,1,0,101000,50,0,1,0,0,2,0,0,2,0,0 +18402,18402,1,103000150,1,0,101000,50,0,1,0,0,2,0,0,2,0,0 +18403,18403,1,104000160,1,0,101000,50,0,1,0,0,2,0,0,2,0,0 +18404,18404,2,101000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +18405,18405,2,102000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +18406,18406,2,115000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +18407,18407,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +18408,18408,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +18409,18409,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +18410,18410,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +18411,18411,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +18501,18501,1,103000250,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +18502,18502,1,104000290,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +18503,18503,2,101000017,1,0,202500,50,0,1,0,0,2,0,0,2,0,0 +18504,18504,2,111000007,1,0,210100,30,0,1,0,0,2,0,0,2,0,0 +18505,18505,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +18506,18506,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +18507,18507,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +18508,18508,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +18509,18509,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +18601,18601,8,202000170,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +18602,18602,8,204000120,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +18603,18603,8,203000160,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +18604,18604,8,208000030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +18605,18605,8,209000120,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +18606,18606,8,210000020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +18607,18607,8,216000070,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +18608,18608,8,297000120,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +18901,18901,8,217000030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +18902,18902,8,205000120,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +18903,18903,8,206000130,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +18904,18904,8,218000040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19001,19001,8,211000020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19002,19002,8,202000080,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19003,19003,8,203000060,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19004,19004,8,290000070,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19101,19101,8,216000050,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19102,19102,8,202000120,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19103,19103,8,203000120,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19104,19104,8,206000090,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19201,19201,8,209000010,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19202,19202,8,205000010,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19203,19203,8,215000020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19204,19204,8,220000020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19205,19205,8,204000020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19206,19206,8,206000010,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19207,19207,8,201000020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19208,19208,8,216000020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19209,19209,8,290000030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19210,19210,8,202000030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19211,19211,8,203000020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19212,19212,8,209000020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19213,19213,8,297000010,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19214,19214,8,297000020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19301,19301,1,104000340,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +19302,19302,1,107000160,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +19303,19303,1,109000270,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +19304,19304,2,115000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +19305,19305,2,105000004,1,0,202700,50,0,204700,1,0,2,0,0,2,0,0 +19306,19306,2,101000014,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +19307,19307,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +19308,19308,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19309,19309,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19310,19310,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19311,19311,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19701,19701,1,111000180,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +19702,19702,1,106000230,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +19703,19703,1,109000260,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +19704,19704,1,110000180,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +19705,19705,2,101000012,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +19706,19706,2,103000004,1,0,204100,0,0,204800,5,0,2,0,0,2,0,0 +19707,19707,2,101000014,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +19708,19708,2,107000004,1,0,205600,0,0,1,0,0,2,0,0,2,0,0 +19709,19709,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +19710,19710,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19711,19711,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19712,19712,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19713,19713,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19801,19801,1,103000290,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +19802,19802,1,104000350,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +19803,19803,2,101000018,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +19804,19804,2,115000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +19805,19805,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +19806,19806,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19807,19807,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19808,19808,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19809,19809,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19901,19901,1,102000330,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +19902,19902,1,109000280,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +19903,19903,2,102000009,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +19904,19904,2,101000014,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +19905,19905,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +19906,19906,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19907,19907,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19908,19908,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +19909,19909,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20001,20001,8,201000070,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20002,20002,8,202000130,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20003,20003,8,211000040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20004,20004,8,212000040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20005,20005,8,290000100,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20101,20101,8,209000130,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20102,20102,8,201000100,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20103,20103,8,202000180,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20104,20104,8,203000170,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20201,20201,8,202000190,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20202,20202,8,211000070,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20203,20203,8,203000180,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20204,20204,8,204000130,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20205,20205,8,209000140,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20301,20301,8,218000050,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20302,20302,8,204000140,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20303,20303,8,206000140,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20304,20304,8,215000060,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20401,20401,8,290000040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20402,20402,8,202000040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20403,20403,8,204000030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20404,20404,8,205000020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20405,20405,8,206000020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20406,20406,8,203000030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20407,20407,8,209000030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20408,20408,8,215000030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20501,20501,8,297000030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20502,20502,8,211000010,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20503,20503,8,297000050,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20504,20504,8,202000060,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20505,20505,8,290000050,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20506,20506,8,297000040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20507,20507,8,212000010,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20508,20508,8,297000060,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20601,20601,8,205000030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20602,20602,8,290000060,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20603,20603,8,206000050,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20604,20604,8,220000030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20605,20605,8,203000040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20606,20606,8,209000050,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20607,20607,8,203000070,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20608,20608,8,204000050,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20609,20609,8,208000020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20610,20610,8,216000030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20801,20801,1,103000320,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +20802,20802,1,104000380,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +20803,20803,1,111000210,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +20804,20804,2,101000015,1,0,207100,0,0,1,0,0,2,0,0,2,0,0 +20805,20805,2,102000008,1,0,204300,50,0,1,0,0,2,0,0,2,0,0 +20806,20806,2,109000008,1,0,204100,0,0,1,0,0,2,0,0,2,0,0 +20807,20807,2,112000008,1,0,305000,100,0,305400,0,0,2,0,0,2,0,0 +20808,20808,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20809,20809,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20810,20810,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +20811,20811,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +21301,21301,1,102000240,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +21302,21302,1,105000170,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +21303,21303,1,109000190,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +21304,21304,2,102000008,1,0,204300,50,0,1,0,0,2,0,0,2,0,0 +21305,21305,2,109000008,1,0,204100,0,0,1,0,0,2,0,0,2,0,0 +21306,21306,2,101000015,1,0,207100,0,0,1,0,0,2,0,0,2,0,0 +21307,21307,2,112000008,1,0,305000,100,0,305400,0,0,2,0,0,2,0,0 +21308,21308,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +21309,21309,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +21310,21310,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +21311,21311,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +21401,21401,1,102000380,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +21402,21402,1,104000390,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +21403,21403,2,115000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +21404,21404,2,102000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +21405,21405,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +21406,21406,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +21407,21407,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +21408,21408,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +21409,21409,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +21501,21501,1,104000200,1,0,100900,50,0,101000,50,0,2,0,0,2,0,0 +21502,21502,1,106000170,1,0,100900,50,0,101000,50,0,2,0,0,2,0,0 +21503,21503,1,111000100,1,0,100900,50,0,101000,50,0,2,0,0,2,0,0 +21504,21504,2,111000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +21505,21505,2,103000004,1,0,204100,0,0,204800,5,0,2,0,0,2,0,0 +21506,21506,2,101000012,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +21507,21507,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +21508,21508,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +21509,21509,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +21510,21510,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +21511,21511,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +21601,21601,1,102000190,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +21602,21602,1,109000150,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +21603,21603,1,103000200,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +21604,21604,1,111000110,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +21605,21605,1,105000190,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +21606,21606,1,106000190,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +21607,21607,1,109000250,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +21608,21608,1,112000120,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +21609,21609,1,101000170,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +21610,21610,1,101000220,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +21611,21611,1,102000230,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +21612,21612,1,105000230,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +21613,21613,1,106000250,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +21614,21614,1,111000200,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +21615,21615,1,109000160,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +21616,21616,1,102000200,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +21617,21617,1,105000150,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +21618,21618,1,106000160,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +21801,21801,8,202000200,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +21802,21802,8,203000190,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +21803,21803,8,204000150,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +21804,21804,8,209000150,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +21901,21901,8,215000070,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +21902,21902,8,202000210,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +21903,21903,8,205000130,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +21904,21904,8,220000050,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +22001,22001,8,204000160,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +22002,22002,8,201000110,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +22003,22003,8,202000220,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +22004,22004,8,206000160,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +22101,22101,1,101000320,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +22102,22102,1,111000230,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +22103,22103,1,112000150,1,0,111500,0,0,1,0,0,2,0,0,2,0,0 +22104,22104,2,101000013,1,0,204200,5,0,1,0,0,2,0,0,2,0,0 +22105,22105,2,101000012,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +22106,22106,2,101000011,1,0,209900,30,0,1,0,0,2,0,0,2,0,0 +22107,22107,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +22108,22108,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +22109,22109,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +22110,22110,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +22111,22111,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +22201,22201,1,102000440,1,0,100900,100,0,107900,0,0,2,0,0,2,0,0 +22202,22202,1,103000340,1,0,100900,100,0,107900,0,0,2,0,0,2,0,0 +22203,22203,1,109000340,1,0,100900,100,0,107900,0,0,2,0,0,2,0,0 +22204,22204,1,110000200,1,0,100900,100,0,107900,0,0,2,0,0,2,0,0 +22205,22205,1,118000120,1,0,100900,100,0,107900,0,0,2,0,0,2,0,0 +22206,22206,2,102000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +22207,22207,2,101000017,1,0,202500,50,0,1,0,0,2,0,0,2,0,0 +22208,22208,2,101000014,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +22209,22209,2,103000004,1,0,204100,0,0,204800,5,0,2,0,0,2,0,0 +22210,22210,2,101000016,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +22211,22211,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +22212,22212,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +22213,22213,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +22214,22214,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +22215,22215,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +22801,22801,1,101000240,1,0,107800,2019,0,1,0,0,2,0,0,2,0,0 +22802,22802,1,102000250,1,0,107800,2019,0,1,0,0,2,0,0,2,0,0 +22803,22803,1,103000210,1,0,107800,2019,0,1,0,0,2,0,0,2,0,0 +22804,22804,1,109000210,1,0,107800,2019,0,1,0,0,2,0,0,2,0,0 +22805,22805,1,104000240,1,0,107800,2019,0,1,0,0,2,0,0,2,0,0 +22806,22806,1,105000200,1,0,107800,2019,0,1,0,0,2,0,0,2,0,0 +22807,22807,1,106000200,1,0,107800,2019,0,1,0,0,2,0,0,2,0,0 +22808,22808,1,108000140,1,0,107800,2019,0,1,0,0,2,0,0,2,0,0 +22809,22809,2,107000007,1,0,207700,0,0,1,0,0,2,0,0,2,0,0 +22810,22810,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +22811,22811,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +22812,22812,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +22813,22813,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +22814,22814,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23201,23201,1,101000350,1,0,102200,10,0,1,0,0,2,0,0,2,0,0 +23202,23202,1,106000300,1,0,102200,10,0,1,0,0,2,0,0,2,0,0 +23203,23203,2,101000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +23204,23204,2,103000007,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23205,23205,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +23206,23206,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23207,23207,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23208,23208,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23209,23209,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23301,23301,1,101000330,1,0,111500,0,0,1,0,0,2,0,0,2,0,0 +23302,23302,1,102000420,1,0,111500,0,0,1,0,0,2,0,0,2,0,0 +23303,23303,2,101000009,1,0,204000,5,0,1,0,0,2,0,0,2,0,0 +23304,23304,2,102000009,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +23305,23305,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +23306,23306,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23307,23307,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23308,23308,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23309,23309,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23401,23401,8,205000140,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23402,23402,8,204000180,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23403,23403,8,206000180,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23404,23404,8,209000170,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23501,23501,8,203000210,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23502,23502,8,204000190,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23503,23503,8,215000080,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23504,23504,8,217000040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23601,23601,8,209000180,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23602,23602,8,202000260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23603,23603,8,203000220,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23604,23604,8,218000060,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23701,23701,8,203000090,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23702,23702,8,212000030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23703,23703,8,202000100,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23704,23704,8,204000070,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23705,23705,8,206000060,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23706,23706,8,201000060,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23707,23707,8,297000090,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23801,23801,8,299000780,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23802,23802,8,299000790,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23803,23803,3,170005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23804,23804,3,180005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23901,23901,1,102000450,1,0,111500,0,0,1,0,0,2,0,0,2,0,0 +23902,23902,1,109000350,1,0,111500,0,0,1,0,0,2,0,0,2,0,0 +23903,23903,1,111000250,1,0,111500,0,0,1,0,0,2,0,0,2,0,0 +23904,23904,2,102000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +23905,23905,2,101000014,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +23906,23906,2,101000012,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +23907,23907,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +23908,23908,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23909,23909,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23910,23910,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +23911,23911,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +24301,24301,1,111000260,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +24302,24302,1,112000170,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +24303,24303,1,118000150,1,0,105100,10,0,1,0,0,2,0,0,2,0,0 +24304,24304,2,101000012,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +24305,24305,2,101000011,1,0,209900,30,0,1,0,0,2,0,0,2,0,0 +24306,24306,2,101000016,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +24307,24307,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +24308,24308,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +24309,24309,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +24310,24310,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +24311,24311,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +24701,24701,1,102000470,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +24702,24702,1,103000380,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +24703,24703,2,102000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +24704,24704,2,105000004,1,0,202700,50,0,204700,1,0,2,0,0,2,0,0 +24705,24705,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +24706,24706,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +24707,24707,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +24708,24708,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +24709,24709,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +24801,24801,1,105000290,1,0,102200,10,0,1,0,0,2,0,0,2,0,0 +24802,24802,1,106000310,1,0,102200,10,0,1,0,0,2,0,0,2,0,0 +24803,24803,2,109000006,1,0,204600,25,0,1,0,0,2,0,0,2,0,0 +24804,24804,2,103000007,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +24805,24805,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +24806,24806,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +24807,24807,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +24808,24808,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +24809,24809,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +24901,24901,1,101000370,1,0,105600,0,0,1,0,0,2,0,0,2,0,0 +24902,24902,1,105000300,1,0,105600,0,0,1,0,0,2,0,0,2,0,0 +24903,24903,2,101000009,1,0,204000,5,0,1,0,0,2,0,0,2,0,0 +24904,24904,2,109000006,1,0,204600,25,0,1,0,0,2,0,0,2,0,0 +24905,24905,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +24906,24906,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +24907,24907,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +24908,24908,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +24909,24909,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +25001,25001,1,103000290,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +25002,25002,1,104000350,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +25003,25003,1,102000330,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +25004,25004,1,109000280,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +25005,25005,2,101000018,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +25006,25006,2,115000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +25007,25007,2,102000009,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +25008,25008,2,101000014,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +25009,25009,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +25010,25010,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +25011,25011,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +25012,25012,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +25013,25013,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +25101,25101,8,204000240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +25102,25102,8,203000250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +25103,25103,8,206000210,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +25104,25104,8,299000910,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +25201,25201,8,206000150,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +25202,25202,8,202000250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +25203,25203,8,204000170,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +25204,25204,8,216000080,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +25301,25301,8,202000300,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +25302,25302,8,201000160,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +25303,25303,8,209000210,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +25304,25304,8,217000050,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +25501,25501,1,104000440,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +25502,25502,1,105000320,1,0,105100,10,0,1,0,0,2,0,0,2,0,0 +25503,25503,1,106000320,1,0,105100,10,0,1,0,0,2,0,0,2,0,0 +25504,25504,1,108000180,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +25505,25505,1,109000360,1,0,102200,10,0,1,0,0,2,0,0,2,0,0 +25506,25506,2,107000007,1,0,207700,0,0,1,0,0,2,0,0,2,0,0 +25507,25507,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +25508,25508,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +25509,25509,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +25510,25510,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +25511,25511,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +26101,26101,1,102000480,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +26102,26102,1,111000280,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +26103,26103,2,101000012,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +26104,26104,2,102000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +26105,26105,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +26106,26106,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +26107,26107,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +26108,26108,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +26109,26109,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +26401,26401,1,101000320,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +26402,26402,1,111000230,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +26403,26403,1,112000150,1,0,111500,0,0,1,0,0,2,0,0,2,0,0 +26404,26404,2,101000013,1,0,204200,5,0,1,0,0,2,0,0,2,0,0 +26405,26405,2,101000012,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +26406,26406,2,101000011,1,0,209900,30,0,1,0,0,2,0,0,2,0,0 +26407,26407,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +26408,26408,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +26409,26409,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +26410,26410,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +26411,26411,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +26601,26601,8,202000310,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +26602,26602,8,204000250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +26603,26603,8,209000220,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +26604,26604,8,215000090,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +26701,26701,8,205000170,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +26702,26702,8,202000320,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +26703,26703,8,206000220,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +26704,26704,8,209000230,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +26901,26901,1,101000330,1,0,111500,0,0,1,0,0,2,0,0,2,0,0 +26902,26902,1,102000420,1,0,111500,0,0,1,0,0,2,0,0,2,0,0 +26903,26903,2,101000009,1,0,204000,5,0,1,0,0,2,0,0,2,0,0 +26904,26904,2,102000009,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +26905,26905,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +26906,26906,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +26907,26907,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +26908,26908,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +26909,26909,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +27001,27001,8,206000150,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +27002,27002,8,202000250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +27003,27003,8,204000170,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +27004,27004,8,216000080,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +27101,27101,1,111000260,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +27102,27102,1,112000170,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +27103,27103,1,118000150,1,0,105100,10,0,1,0,0,2,0,0,2,0,0 +27104,27104,2,101000012,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +27105,27105,2,101000011,1,0,209900,30,0,1,0,0,2,0,0,2,0,0 +27106,27106,2,101000016,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +27107,27107,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +27108,27108,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +27109,27109,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +27110,27110,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +27111,27111,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +27501,27501,1,103000290,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +27502,27502,1,104000350,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +27503,27503,1,102000330,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +27504,27504,1,109000280,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +27505,27505,2,101000018,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +27506,27506,2,115000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +27507,27507,2,102000009,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +27508,27508,2,101000014,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +27509,27509,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +27510,27510,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +27511,27511,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +27512,27512,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +27513,27513,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +27601,27601,8,202000300,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +27602,27602,8,201000160,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +27603,27603,8,209000210,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +27604,27604,8,217000050,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +27701,27701,1,103000400,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +27702,27702,1,105000330,1,0,105100,10,0,1,0,0,2,0,0,2,0,0 +27703,27703,1,109000390,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +27704,27704,1,110000250,1,0,105100,10,0,1,0,0,2,0,0,2,0,0 +27705,27705,1,118000180,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +27706,27706,2,101000017,1,0,202500,50,0,1,0,0,2,0,0,2,0,0 +27707,27707,2,109000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +27708,27708,2,101000014,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +27709,27709,2,103000004,1,0,204100,0,0,204800,5,0,2,0,0,2,0,0 +27710,27710,2,101000016,1,0,207300,50,0,1,0,0,2,0,0,2,0,0 +27711,27711,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +27712,27712,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +27713,27713,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +27714,27714,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +27715,27715,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +28401,28401,1,102000480,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +28402,28402,1,111000280,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +28403,28403,2,101000012,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +28404,28404,2,102000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +28405,28405,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +28406,28406,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +28407,28407,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +28408,28408,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +28409,28409,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +28501,28501,1,104000460,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +28502,28502,1,111000290,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +28503,28503,2,101000012,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +28504,28504,2,115000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +28505,28505,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +28506,28506,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +28507,28507,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +28508,28508,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +28509,28509,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +28601,28601,1,102000170,1,0,101000,50,0,1,0,0,2,0,0,2,0,0 +28602,28602,1,103000150,1,0,101000,50,0,1,0,0,2,0,0,2,0,0 +28603,28603,1,104000160,1,0,101000,50,0,1,0,0,2,0,0,2,0,0 +28604,28604,1,111000200,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +28605,28605,1,106000250,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +28606,28606,1,105000230,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +28607,28607,2,102000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +28608,28608,2,101000017,1,0,202500,50,0,1,0,0,2,0,0,2,0,0 +28609,28609,2,115000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +28610,28610,2,101000012,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +28611,28611,2,103000004,1,0,204100,0,0,204800,5,0,2,0,0,2,0,0 +28612,28612,2,109000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +28613,28613,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +28614,28614,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +28615,28615,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +28616,28616,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +28617,28617,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29001,29001,8,206000230,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29002,29002,8,204000280,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29003,29003,8,211000100,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29004,29004,8,290000140,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29101,29101,8,216000100,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29102,29102,8,202000330,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29103,29103,8,204000290,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29104,29104,8,209000250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29201,29201,8,217000060,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29202,29202,8,204000260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29203,29203,8,205000180,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29204,29204,8,218000070,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29301,29301,8,209000010,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29302,29302,8,205000010,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29303,29303,8,215000020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29304,29304,8,220000020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29305,29305,8,204000020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29306,29306,8,206000010,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29307,29307,8,201000020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29308,29308,8,216000020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29309,29309,8,290000030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29310,29310,8,202000030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29311,29311,8,203000020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29312,29312,8,209000020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29313,29313,8,297000010,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29314,29314,8,297000020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29401,29401,1,103000450,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +29402,29402,1,104000490,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +29403,29403,1,111000310,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +29404,29404,1,106000370,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +29405,29405,2,101000017,1,0,202500,50,0,1,0,0,2,0,0,2,0,0 +29406,29406,2,115000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +29407,29407,2,101000012,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +29408,29408,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +29409,29409,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29410,29410,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29411,29411,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29412,29412,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29901,29901,1,105000350,1,0,105100,10,0,1,0,0,2,0,0,2,0,0 +29902,29902,1,106000380,1,0,105100,10,0,1,0,0,2,0,0,2,0,0 +29903,29903,1,110000260,1,0,105100,10,0,1,0,0,2,0,0,2,0,0 +29904,29904,2,101000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +29905,29905,2,101000009,1,0,204000,5,0,1,0,0,2,0,0,2,0,0 +29906,29906,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +29907,29907,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29908,29908,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29909,29909,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +29910,29910,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +30401,30401,1,101000400,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +30402,30402,1,102000540,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +30403,30403,2,101000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +30404,30404,2,102000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +30405,30405,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +30406,30406,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +30407,30407,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +30408,30408,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +30409,30409,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +30501,30501,1,101000410,1,0,102200,10,0,1,0,0,2,0,0,2,0,0 +30502,30502,1,102000560,1,0,102200,10,0,1,0,0,2,0,0,2,0,0 +30503,30503,2,101000009,1,0,204000,5,0,1,0,0,2,0,0,2,0,0 +30504,30504,2,102000006,1,0,202400,25,0,1,0,0,2,0,0,2,0,0 +30505,30505,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +30506,30506,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +30507,30507,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +30508,30508,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +30509,30509,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +30601,30601,8,290000040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +30602,30602,8,204000030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +30603,30603,8,205000020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +30604,30604,8,206000020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +30605,30605,8,202000040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +30606,30606,8,203000030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +30607,30607,8,209000030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +30608,30608,8,215000030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +30701,30701,8,201000180,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +30702,30702,8,204000300,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +30703,30703,8,209000260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +30704,30704,8,216000110,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +30801,30801,8,203000290,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +30802,30802,8,205000210,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +30803,30803,8,215000110,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +30804,30804,8,290000150,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +30901,30901,8,202000360,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +30902,30902,8,204000310,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +30903,30903,8,209000270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +30904,30904,8,216000120,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +31001,31001,1,102000190,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +31002,31002,1,109000150,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +31003,31003,1,103000200,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +31004,31004,1,111000110,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +31005,31005,1,105000190,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +31006,31006,1,106000190,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +31007,31007,1,109000250,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +31008,31008,1,112000120,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +31009,31009,1,102000380,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +31010,31010,1,104000390,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +31011,31011,1,101000350,1,0,102200,10,0,1,0,0,2,0,0,2,0,0 +31012,31012,1,106000300,1,0,102200,10,0,1,0,0,2,0,0,2,0,0 +31013,31013,8,297000030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +31014,31014,8,202000060,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +31015,31015,8,290000050,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +31016,31016,8,297000040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +31017,31017,8,299000780,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +31018,31018,8,299000790,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +31201,31201,1,106000390,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +31202,31202,1,110000290,1,0,105100,10,0,1,0,0,2,0,0,2,0,0 +31203,31203,1,118000200,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +31204,31204,2,102000008,1,0,204300,50,0,1,0,0,2,0,0,2,0,0 +31205,31205,2,109000008,1,0,204100,0,0,1,0,0,2,0,0,2,0,0 +31206,31206,2,101000015,1,0,207100,0,0,1,0,0,2,0,0,2,0,0 +31207,31207,2,112000008,1,0,305000,100,0,305400,0,0,2,0,0,2,0,0 +31208,31208,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +31209,31209,8,206000050,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +31210,31210,8,220000030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +31211,31211,8,203000070,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +31212,31212,8,204000050,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +31213,31213,8,216000030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +31214,31214,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +31215,31215,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +31216,31216,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +31217,31217,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +31701,31701,1,102000240,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +31702,31702,1,105000170,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +31703,31703,1,109000190,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +31704,31704,1,103000320,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +31705,31705,1,104000380,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +31706,31706,1,111000210,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +31707,31707,2,102000008,1,0,204300,50,0,1,0,0,2,0,0,2,0,0 +31708,31708,2,109000008,1,0,204100,0,0,1,0,0,2,0,0,2,0,0 +31709,31709,2,101000015,1,0,207100,0,0,1,0,0,2,0,0,2,0,0 +31710,31710,2,112000008,1,0,305000,100,0,305400,0,0,2,0,0,2,0,0 +31711,31711,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +31712,31712,8,206000050,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +31713,31713,8,220000030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +31714,31714,8,203000070,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +31715,31715,8,204000050,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +31716,31716,8,216000030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +31717,31717,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +31718,31718,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +31719,31719,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +31720,31720,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +31901,31901,8,209000300,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +31902,31902,8,201000190,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +31903,31903,8,203000320,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +31904,31904,8,204000340,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +32001,32001,1,101000390,1,0,105600,0,0,1,0,0,2,0,0,2,0,0 +32002,32002,1,103000390,1,0,105600,0,0,1,0,0,2,0,0,2,0,0 +32003,32003,2,105000007,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32004,32004,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32005,32005,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +32006,32006,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +32007,32007,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +32008,32008,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +32101,32101,1,102000170,1,0,101000,50,0,1,0,0,2,0,0,2,0,0 +32102,32102,1,103000150,1,0,101000,50,0,1,0,0,2,0,0,2,0,0 +32103,32103,1,104000160,1,0,101000,50,0,1,0,0,2,0,0,2,0,0 +32104,32104,1,105000230,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32105,32105,1,106000250,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32106,32106,1,111000200,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32107,32107,1,103000400,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +32108,32108,1,105000330,1,0,105100,10,0,1,0,0,2,0,0,2,0,0 +32109,32109,1,109000390,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +32110,32110,1,110000250,1,0,105100,10,0,1,0,0,2,0,0,2,0,0 +32111,32111,1,118000180,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +32112,32112,1,102000480,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +32113,32113,1,111000280,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +32114,32114,1,101000170,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32115,32115,1,101000220,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32116,32116,1,102000230,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32117,32117,1,103000250,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32118,32118,1,104000290,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32119,32119,1,101000370,1,0,105600,0,0,1,0,0,2,0,0,2,0,0 +32120,32120,1,105000300,1,0,105600,0,0,1,0,0,2,0,0,2,0,0 +32121,32121,1,102000470,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +32122,32122,1,103000380,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +32123,32123,1,109000160,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32127,32127,1,110000160,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32128,32128,1,112000100,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32129,32129,1,101000290,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32130,32130,1,104000360,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32131,32131,1,107000170,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32132,32132,1,103000330,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32133,32133,1,110000210,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32134,32134,1,109000330,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32135,32135,1,104000410,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32136,32136,1,102000370,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32137,32137,1,102000310,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32138,32138,1,106000240,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32139,32139,1,103000300,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32140,32140,1,102000340,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32141,32141,1,109000310,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32142,32142,1,105000250,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32143,32143,1,118000130,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32144,32144,1,101000340,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32145,32145,1,104000430,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32146,32146,1,110000220,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32147,32147,1,106000330,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32148,32148,1,109000170,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32149,32149,1,111000120,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32150,32150,1,118000090,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32151,32151,1,104000310,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32152,32152,1,106000290,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32153,32153,1,102000400,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32154,32154,1,102000200,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32155,32155,1,105000150,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32156,32156,1,106000160,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32301,32301,8,206000280,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +32302,32302,8,205000250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +32303,32303,8,209000310,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +32304,32304,8,217000100,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +32401,32401,1,102000570,1,0,105100,10,0,1,0,0,2,0,0,2,0,0 +32402,32402,1,104000510,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +32403,32403,1,111000330,1,0,102200,10,0,1,0,0,2,0,0,2,0,0 +32404,32404,2,102000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32405,32405,2,101000012,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32406,32406,2,115000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32407,32407,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32408,32408,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +32409,32409,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +32410,32410,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +32411,32411,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +32901,32901,1,101000240,1,0,107800,2019,0,1,0,0,2,0,0,2,0,0 +32902,32902,1,102000250,1,0,107800,2019,0,1,0,0,2,0,0,2,0,0 +32903,32903,1,103000210,1,0,107800,2019,0,1,0,0,2,0,0,2,0,0 +32904,32904,1,109000210,1,0,107800,2019,0,1,0,0,2,0,0,2,0,0 +32905,32905,1,104000240,1,0,107800,2019,0,1,0,0,2,0,0,2,0,0 +32906,32906,1,105000200,1,0,107800,2019,0,1,0,0,2,0,0,2,0,0 +32907,32907,1,106000200,1,0,107800,2019,0,1,0,0,2,0,0,2,0,0 +32908,32908,1,108000140,1,0,107800,2019,0,1,0,0,2,0,0,2,0,0 +32909,32909,2,107000007,1,0,207700,0,0,1,0,0,2,0,0,2,0,0 +32910,32910,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32911,32911,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +32912,32912,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +32913,32913,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +32914,32914,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +33101,33101,1,109000410,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +33102,33102,1,112000200,1,0,104000,10,0,1,0,0,2,0,0,2,0,0 +33103,33103,2,115000005,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +33104,33104,2,101000019,1,0,204400,0,0,204900,0,0,2,0,0,2,0,0 +33105,33105,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +33106,33106,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +33107,33107,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +33108,33108,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +33109,33109,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +33201,33201,8,203000330,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +33202,33202,8,202000410,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +33203,33203,8,204000350,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +33204,33204,8,216000150,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +33301,33301,8,203000090,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +33302,33302,8,202000100,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +33303,33303,8,204000070,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +33304,33304,8,206000060,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +33305,33305,8,212000030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +33306,33306,8,201000060,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +33307,33307,8,297000090,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +33401,33401,1,104000540,1,1,111500,0,0,1,0,0,2,0,0,2,0,0 +33402,33402,1,105000380,1,1,111500,0,0,1,0,0,2,0,0,2,0,0 +33403,33403,1,106000400,1,1,111500,0,0,1,0,0,2,0,0,2,0,0 +33404,33404,2,111000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +33405,33405,2,109000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +33406,33406,2,103000004,1,0,204100,0,0,204800,5,0,2,0,0,2,0,0 +33407,33407,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +33408,33408,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +33409,33409,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +33410,33410,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +33411,33411,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +33901,33901,1,102000590,1,1,105100,10,0,1,0,0,2,0,0,2,0,0 +33902,33902,1,111000360,1,1,104000,10,0,1,0,0,2,0,0,2,0,0 +33903,33903,2,107000007,1,0,207700,0,0,1,0,0,2,0,0,2,0,0 +33904,33904,2,120000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +33905,33905,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +33906,33906,3,110250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +33907,33907,3,110260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +33908,33908,3,110270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +34101,34101,1,102000450,1,1,111500,0,0,1,0,0,2,0,0,2,0,0 +34102,34102,1,109000350,1,1,111500,0,0,1,0,0,2,0,0,2,0,0 +34103,34103,1,111000250,1,1,111500,0,0,1,0,0,2,0,0,2,0,0 +34104,34104,1,101000330,1,1,111500,0,0,1,0,0,2,0,0,2,0,0 +34105,34105,1,102000420,1,1,111500,0,0,1,0,0,2,0,0,2,0,0 +34106,34106,1,104000440,1,1,104000,10,0,1,0,0,2,0,0,2,0,0 +34107,34107,1,105000320,1,1,105100,10,0,1,0,0,2,0,0,2,0,0 +34108,34108,1,106000320,1,1,105100,10,0,1,0,0,2,0,0,2,0,0 +34109,34109,1,108000180,1,1,104000,10,0,1,0,0,2,0,0,2,0,0 +34110,34110,1,109000360,1,1,102200,10,0,1,0,0,2,0,0,2,0,0 +34111,34111,1,104000200,1,1,100900,50,0,101000,50,0,2,0,0,2,0,0 +34112,34112,1,106000170,1,1,100900,50,0,101000,50,0,2,0,0,2,0,0 +34113,34113,1,111000100,1,1,100900,50,0,101000,50,0,2,0,0,2,0,0 +34114,34114,1,102000440,1,1,100900,100,0,107900,0,0,2,0,0,2,0,0 +34115,34115,1,103000340,1,1,100900,100,0,107900,0,0,2,0,0,2,0,0 +34116,34116,1,109000340,1,1,100900,100,0,107900,0,0,2,0,0,2,0,0 +34117,34117,1,110000200,1,1,100900,100,0,107900,0,0,2,0,0,2,0,0 +34118,34118,1,118000120,1,1,100900,100,0,107900,0,0,2,0,0,2,0,0 +34119,34119,1,103000290,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +34120,34120,1,104000350,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +34121,34121,1,102000330,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +34122,34122,1,109000280,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +34123,34123,1,101000320,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +34124,34124,1,111000230,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +34125,34125,1,112000150,1,1,111500,0,0,1,0,0,2,0,0,2,0,0 +34126,34126,2,101000013,1,0,204200,5,0,1,0,0,2,0,0,2,0,0 +34127,34127,2,101000012,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +34128,34128,2,101000011,1,0,209900,30,0,1,0,0,2,0,0,2,0,0 +34129,34129,2,101000018,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +34130,34130,2,101000014,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +34131,34131,2,101000016,1,0,207300,50,0,1,0,0,2,0,0,2,0,0 +34132,34132,2,102000009,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +34133,34133,2,103000007,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +34134,34134,2,107000007,1,0,207700,0,0,1,0,0,2,0,0,2,0,0 +34135,34135,2,111000008,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +34136,34136,2,115000005,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +34201,34201,1,103000490,1,1,104000,10,0,1,0,0,2,0,0,2,0,0 +34202,34202,1,109000440,1,1,102200,10,0,1,0,0,2,0,0,2,0,0 +34203,34203,1,118000220,1,1,104000,10,0,1,0,0,2,0,0,2,0,0 +34204,34204,1,105000400,1,1,105100,10,0,1,0,0,2,0,0,2,0,0 +34205,34205,1,106000420,1,1,105100,10,0,1,0,0,2,0,0,2,0,0 +34206,34206,1,103000500,1,1,111500,0,0,1,0,0,2,0,0,2,0,0 +34207,34207,1,110000310,1,1,111500,0,0,1,0,0,2,0,0,2,0,0 +34208,34208,1,118000230,1,1,111500,0,0,1,0,0,2,0,0,2,0,0 +34209,34209,1,101000440,1,1,104000,10,0,1,0,0,2,0,0,2,0,0 +34210,34210,1,102000600,1,1,104000,10,0,1,0,0,2,0,0,2,0,0 diff --git a/titles/sao/data/GashaMedalShops.csv b/titles/sao/data/GashaMedalShops.csv new file mode 100644 index 0000000..a4a78dc --- /dev/null +++ b/titles/sao/data/GashaMedalShops.csv @@ -0,0 +1,882 @@ +GashaMedalShopId,Name,GashaMedalId,UseGashaMedalNum,PurchaseLimit,StartDate,EndDate +15701,"≪不屈の誓い≫キリト",10002,300,-1,"2020/03/10 7:00:00","2020/03/31 2:00:00" +15702,"≪明日への誓い≫アスナ",10002,300,-1,"2020/03/10 7:00:00","2020/03/31 2:00:00" +15703,"ダークリパルサー",10002,60,-1,"2020/03/10 7:00:00","2020/03/31 2:00:00" +15801,"《恒久の愛情》ユージオ",10001,180,-1,"2020/03/03 7:00:00","2020/03/24 2:00:00" +15802,"《赤誠の求愛》キリト",10001,30,-1,"2020/03/03 7:00:00","2020/03/24 2:00:00" +15803,"《狂愛の誓い》クラディール",10001,30,-1,"2020/03/03 7:00:00","2020/03/24 2:00:00" +15901,"《空島繋ぐ架け橋》レイン",10159,150,-1,"2020/06/09 7:00:00","2020/07/21 2:00:00" +15902,"《一騎多閃》レイン",10159,30,-1,"2020/06/09 7:00:00","2020/07/21 2:00:00" +15903,"《温かな眼差し》レイン",10159,30,-1,"2020/06/09 7:00:00","2020/07/21 2:00:00" +15904,"ソード・オブ・ホグニ",10159,30,-1,"2020/06/09 7:00:00","2020/07/21 2:00:00" +16301,"《愛おしい日常》ユウキ",10163,90,-1,"2020/07/07 7:00:00","2020/07/28 2:00:00" +16302,"《早朝の誘い》アスナ",10163,15,-1,"2020/07/07 7:00:00","2020/07/28 2:00:00" +16303,"《花の女子高生》リズベット",10163,15,-1,"2020/07/07 7:00:00","2020/07/28 2:00:00" +16304,"《通学路の乙女》シリカ",10163,15,-1,"2020/07/07 7:00:00","2020/07/28 2:00:00" +16401,"《恒久の愛情》ユージオ",10001,180,-1,"2020/06/23 7:00:00","2020/07/14 2:00:00" +16402,"《赤誠の求愛》キリト",10001,30,-1,"2020/06/23 7:00:00","2020/07/14 2:00:00" +16403,"《狂愛の誓い》クラディール",10001,30,-1,"2020/06/23 7:00:00","2020/07/14 2:00:00" +16501,"≪不屈の誓い≫キリト",10002,300,-1,"2020/07/07 7:00:00","2020/07/28 2:00:00" +16502,"≪明日への誓い≫アスナ",10002,300,-1,"2020/07/07 7:00:00","2020/07/28 2:00:00" +16503,"ダークリパルサー",10002,60,-1,"2020/07/07 7:00:00","2020/07/28 2:00:00" +16601,"≪ハツコイ同盟≫リズベット",10166,150,-1,"2020/06/09 7:00:00","2020/07/14 2:00:00" +16602,"≪トキメキ同盟≫シリカ",10166,150,-1,"2020/06/09 7:00:00","2020/07/14 2:00:00" +16603,"ジャルディーノ",10166,30,-1,"2020/06/09 7:00:00","2020/07/14 2:00:00" +16604,"グリダヴォル",10166,30,-1,"2020/06/09 7:00:00","2020/07/14 2:00:00" +16801,"《創世神ステイシア》アスナ",10168,150,-1,"2020/07/14 7:00:00","2020/08/18 2:00:00" +16802,"《地神テラリア》リーファ",10168,150,-1,"2020/07/14 7:00:00","2020/08/18 2:00:00" +16803,"《太陽神ソルス》シノン",10168,150,-1,"2020/07/14 7:00:00","2020/08/18 2:00:00" +16804,"ヴァーデュラス・アニマ",10168,30,-1,"2020/07/14 7:00:00","2020/08/18 2:00:00" +16805,"ラディアント・ライト",10168,30,-1,"2020/07/14 7:00:00","2020/08/18 2:00:00" +16806,"アニヒレート・レイ",10168,30,-1,"2020/07/14 7:00:00","2020/08/18 2:00:00" +17201,"《英雄纏う剣才》ユウキ",10172,150,-1,"2020/07/21 7:00:00","2020/08/11 2:00:00" +17202,"《忘却の憧憬》ユージオ",10172,150,-1,"2020/07/21 7:00:00","2020/08/11 2:00:00" +17203,"青薔薇の剣",10172,30,-1,"2020/07/21 7:00:00","2020/08/11 2:00:00" +17204,"オブシディアナ",10172,30,-1,"2020/07/21 7:00:00","2020/08/11 2:00:00" +17301,"《揺るぎない信愛》シノン",10173,90,-1,"2020/07/21 7:00:00","2020/08/18 2:00:00" +17302,"《夢へ至る純愛》ユウキ",10173,90,-1,"2020/07/21 7:00:00","2020/08/18 2:00:00" +17303,"《運命の寵愛》キリト",10173,15,-1,"2020/07/21 7:00:00","2020/08/18 2:00:00" +17304,"《美貌の愛妻》アスナ",10173,15,-1,"2020/07/21 7:00:00","2020/08/18 2:00:00" +17305,"《豊かなる恵愛》直葉",10173,15,-1,"2020/07/21 7:00:00","2020/08/18 2:00:00" +17306,"《端麗な深愛》リズベット",10173,15,-1,"2020/07/21 7:00:00","2020/08/18 2:00:00" +17307,"《純真な情愛》シリカ",10173,15,-1,"2020/07/21 7:00:00","2020/08/18 2:00:00" +17308,"《高潔なる忠愛》アリス",10173,15,-1,"2020/07/21 7:00:00","2020/08/18 2:00:00" +17601,"≪透き通る青≫アリス",10176,150,-1,"2020/08/11 7:00:00","2020/09/15 2:00:00" +17602,"≪白砂の少女≫シリカ",10176,150,-1,"2020/08/11 7:00:00","2020/09/15 2:00:00" +17603,"≪目映い太陽≫リズベット",10176,150,-1,"2020/08/11 7:00:00","2020/09/15 2:00:00" +17604,"金木犀の剣",10176,30,-1,"2020/08/11 7:00:00","2020/09/15 2:00:00" +17605,"スクレープ",10176,30,-1,"2020/08/11 7:00:00","2020/09/15 2:00:00" +17606,"ジャイアント・タスク",10176,30,-1,"2020/08/11 7:00:00","2020/09/15 2:00:00" +17607,"ゴールド・ネックレス",10176,30,-1,"2020/08/11 7:00:00","2020/09/15 2:00:00" +17608,"ヒーローコード:ボスキラー",10176,100,-1,"2020/08/11 7:00:00","2020/09/15 2:00:00" +17609,"ヒーローコード:Mobキラー",10176,100,-1,"2020/08/11 7:00:00","2020/09/15 2:00:00" +17610,"ヒーローコード:HP最大時強化",10176,100,-1,"2020/08/11 7:00:00","2020/09/15 2:00:00" +17611,"ヒーローコード:HP低下時強化",10176,100,-1,"2020/08/11 7:00:00","2020/09/15 2:00:00" +18001,"≪現実を超えて≫キリト",10180,150,-1,"2020/09/08 7:00:00","2020/10/13 2:00:00" +18002,"≪鮮やかな閃光≫アスナ",10180,150,-1,"2020/09/08 7:00:00","2020/10/13 2:00:00" +18003,"≪剣に乗せた想い≫リーファ",10180,150,-1,"2020/09/08 7:00:00","2020/10/13 2:00:00" +18004,"エリュシデータ",10180,30,-1,"2020/09/08 7:00:00","2020/10/13 2:00:00" +18005,"アロンダイト",10180,30,-1,"2020/09/08 7:00:00","2020/10/13 2:00:00" +18006,"ムラマサ",10180,30,-1,"2020/09/08 7:00:00","2020/10/13 2:00:00" +18007,"ゴールド・ネックレス",10180,30,-1,"2020/09/08 7:00:00","2020/10/13 2:00:00" +18008,"ヒーローコード:ボスキラー",10180,100,-1,"2020/09/08 7:00:00","2020/10/13 2:00:00" +18009,"ヒーローコード:Mobキラー",10180,100,-1,"2020/09/08 7:00:00","2020/10/13 2:00:00" +18010,"ヒーローコード:HP最大時強化",10180,100,-1,"2020/09/08 7:00:00","2020/10/13 2:00:00" +18011,"ヒーローコード:HP低下時強化",10180,100,-1,"2020/09/08 7:00:00","2020/10/13 2:00:00" +18401,"《真夏の眩耀》アスナ",10184,150,-1,"2020/08/11 7:00:00","2020/09/01 2:00:00" +18402,"《魅惑の夏日》リーファ",10184,150,-1,"2020/08/11 7:00:00","2020/09/01 2:00:00" +18403,"《砂浜の女神》シノン",10184,150,-1,"2020/08/11 7:00:00","2020/09/01 2:00:00" +18404,"エリュシデータ",10184,30,-1,"2020/08/11 7:00:00","2020/09/01 2:00:00" +18405,"アロンダイト",10184,30,-1,"2020/08/11 7:00:00","2020/09/01 2:00:00" +18406,"MEBRマニアック",10184,30,-1,"2020/08/11 7:00:00","2020/09/01 2:00:00" +18407,"ゴールド・ネックレス",10184,30,-1,"2020/08/11 7:00:00","2020/09/01 2:00:00" +18408,"ヒーローコード:ボスキラー",10184,100,-1,"2020/08/11 7:00:00","2020/09/01 2:00:00" +18409,"ヒーローコード:Mobキラー",10184,100,-1,"2020/08/11 7:00:00","2020/09/01 2:00:00" +18410,"ヒーローコード:HP最大時強化",10184,100,-1,"2020/08/11 7:00:00","2020/09/01 2:00:00" +18411,"ヒーローコード:HP低下時強化",10184,100,-1,"2020/08/11 7:00:00","2020/09/01 2:00:00" +18501,"《兄想う妖精》リーファ",10185,150,-1,"2020/09/22 7:00:00","2020/10/13 2:00:00" +18502,"《向き合う勇気》シノン",10185,150,-1,"2020/09/22 7:00:00","2020/10/13 2:00:00" +18503,"ヴァルトレーニス",10185,30,-1,"2020/09/22 7:00:00","2020/10/13 2:00:00" +18504,"アネモイ",10185,30,-1,"2020/09/22 7:00:00","2020/10/13 2:00:00" +18505,"ゴールド・ネックレス",10185,30,-1,"2020/09/22 7:00:00","2020/10/13 2:00:00" +18506,"ヒーローコード:ボスキラー",10185,100,-1,"2020/09/22 7:00:00","2020/10/13 2:00:00" +18507,"ヒーローコード:Mobキラー",10185,100,-1,"2020/09/22 7:00:00","2020/10/13 2:00:00" +18508,"ヒーローコード:HP最大時強化",10185,100,-1,"2020/09/22 7:00:00","2020/10/13 2:00:00" +18509,"ヒーローコード:HP低下時強化",10185,100,-1,"2020/09/22 7:00:00","2020/10/13 2:00:00" +18601,"《納涼の令嬢》アスナ",10186,90,-1,"2020/08/18 7:00:00","2020/09/15 2:00:00" +18602,"《真夏の誘惑》シノン",10186,90,-1,"2020/08/18 7:00:00","2020/09/15 2:00:00" +18603,"《優美な夏姫》直葉",10186,15,-1,"2020/08/18 7:00:00","2020/09/15 2:00:00" +18604,"《海の漢》エギル",10186,15,-1,"2020/08/18 7:00:00","2020/09/15 2:00:00" +18605,"《南風の爛漫娘》ユウキ",10186,15,-1,"2020/08/18 7:00:00","2020/09/15 2:00:00" +18606,"《炎天の疾影》アルゴ",10186,15,-1,"2020/08/18 7:00:00","2020/09/15 2:00:00" +18607,"《清夏の羽休め》フィリア",10186,15,-1,"2020/08/18 7:00:00","2020/09/15 2:00:00" +18608,"《夏空の遊翼》ユイ&ピナ",10186,15,-1,"2020/08/18 7:00:00","2020/09/15 2:00:00" +18901,"《盛夏の花形》セブン",10189,90,-1,"2020/08/25 7:00:00","2020/09/15 2:00:00" +18902,"《艶やか彩花》リズベット",10189,15,-1,"2020/08/25 7:00:00","2020/09/15 2:00:00" +18903,"《祭礼の彩り》シリカ",10189,15,-1,"2020/08/25 7:00:00","2020/09/15 2:00:00" +18904,"《賑わいの明光》レイン",10189,15,-1,"2020/08/25 7:00:00","2020/09/15 2:00:00" +19001,"《お姫様メイド》アリス",10190,90,-1,"2020/09/08 7:00:00","2020/09/29 2:00:00" +19002,"《お嬢様メイド》アスナ",10190,15,-1,"2020/09/08 7:00:00","2020/09/29 2:00:00" +19003,"《お世話好きメイド》直葉",10190,15,-1,"2020/09/08 7:00:00","2020/09/29 2:00:00" +19004,"《純真メイド》ユイ",10190,15,-1,"2020/09/08 7:00:00","2020/09/29 2:00:00" +19101,"《チアー・アップ!》フィリア",10191,90,-1,"2020/09/22 7:00:00","2020/10/13 2:00:00" +19102,"《チアフル・リーダー》アスナ",10191,15,-1,"2020/09/22 7:00:00","2020/10/13 2:00:00" +19103,"《チアフル・ジャンプ》リーファ",10191,15,-1,"2020/09/22 7:00:00","2020/10/13 2:00:00" +19104,"《チアフル・エール》シリカ",10191,15,-1,"2020/09/22 7:00:00","2020/10/13 2:00:00" +19201,"《晴天の高揚》ユウキ",10192,90,-1,"2020/08/04 7:00:00","2020/08/25 2:00:00" +19202,"《熱砂の輝き》リズベット",10192,90,-1,"2020/08/04 7:00:00","2020/08/25 2:00:00" +19203,"《炎夏のひと時》ストレア",10192,90,-1,"2020/08/04 7:00:00","2020/08/25 2:00:00" +19204,"《盛夏の思い出》サチ",10192,90,-1,"2020/08/04 7:00:00","2020/08/25 2:00:00" +19205,"《華夜の巧者》シノン",10192,90,-1,"2020/08/04 7:00:00","2020/08/25 2:00:00" +19206,"《はじける笑顔》シリカ",10192,15,-1,"2020/08/04 7:00:00","2020/08/25 2:00:00" +19207,"《真夏の遊戯》キリト",10192,15,-1,"2020/08/04 7:00:00","2020/08/25 2:00:00" +19208,"《納涼の誘い》フィリア",10192,15,-1,"2020/08/04 7:00:00","2020/08/25 2:00:00" +19209,"《暑熱の高鳴り》ユイ",10192,15,-1,"2020/08/04 7:00:00","2020/08/25 2:00:00" +19210,"《一夏の華》アスナ",10192,15,-1,"2020/08/04 7:00:00","2020/08/25 2:00:00" +19211,"《華輝の妖精》リーファ",10192,15,-1,"2020/08/04 7:00:00","2020/08/25 2:00:00" +19212,"《真夏の憧憬》ユウキ",10192,15,-1,"2020/08/04 7:00:00","2020/08/25 2:00:00" +19213,"《熱暑の漢達》エギル&クライン",10192,10,-1,"2020/08/04 7:00:00","2020/08/25 2:00:00" +19214,"《水辺の戯れ》セブン&レイン",10192,10,-1,"2020/08/04 7:00:00","2020/08/25 2:00:00" +19301,"《現を射抜く眼》シノン",10193,150,-1,"2020/10/20 7:00:00","2020/12/01 2:00:00" +19302,"《同志と共に》クライン",10193,150,-1,"2020/10/20 7:00:00","2020/12/01 2:00:00" +19303,"《仮想を駆けて》ユウキ",10193,150,-1,"2020/10/20 7:00:00","2020/12/01 2:00:00" +19304,"MEBRマニアック",10193,30,-1,"2020/10/20 7:00:00","2020/12/01 2:00:00" +19305,"ムラマサ",10193,30,-1,"2020/10/20 7:00:00","2020/12/01 2:00:00" +19306,"オブシディアナ",10193,30,-1,"2020/10/20 7:00:00","2020/12/01 2:00:00" +19307,"ゴールド・ネックレス",10193,30,-1,"2020/10/20 7:00:00","2020/12/01 2:00:00" +19308,"ヒーローコード:ボスキラー",10193,100,-1,"2020/10/20 7:00:00","2020/12/01 2:00:00" +19309,"ヒーローコード:Mobキラー",10193,100,-1,"2020/10/20 7:00:00","2020/12/01 2:00:00" +19310,"ヒーローコード:HP最大時強化",10193,100,-1,"2020/10/20 7:00:00","2020/12/01 2:00:00" +19311,"ヒーローコード:HP低下時強化",10193,100,-1,"2020/10/20 7:00:00","2020/12/01 2:00:00" +19701,"《劇甚なる金木犀》アリス",10197,150,-1,"2020/11/10 7:00:00","2020/12/01 2:00:00" +19702,"《蒼撃の竜姫》シリカ",10197,30,-1,"2020/11/10 7:00:00","2020/12/01 2:00:00" +19703,"《光速の絶技》ユウキ",10197,30,-1,"2020/11/10 7:00:00","2020/12/01 2:00:00" +19704,"《疾鼠の刻爪》アルゴ",10197,30,-1,"2020/11/10 7:00:00","2020/12/01 2:00:00" +19705,"金木犀の剣",10197,30,-1,"2020/11/10 7:00:00","2020/12/01 2:00:00" +19706,"スクレープ",10197,30,-1,"2020/11/10 7:00:00","2020/12/01 2:00:00" +19707,"オブシディアナ",10197,30,-1,"2020/11/10 7:00:00","2020/12/01 2:00:00" +19708,"ウィズィー・ガジェット",10197,30,-1,"2020/11/10 7:00:00","2020/12/01 2:00:00" +19709,"ゴールド・ネックレス",10197,30,-1,"2020/11/10 7:00:00","2020/12/01 2:00:00" +19710,"ヒーローコード:ボスキラー",10197,100,-1,"2020/11/10 7:00:00","2020/12/01 2:00:00" +19711,"ヒーローコード:Mobキラー",10197,100,-1,"2020/11/10 7:00:00","2020/12/01 2:00:00" +19712,"ヒーローコード:HP最大時強化",10197,100,-1,"2020/11/10 7:00:00","2020/12/01 2:00:00" +19713,"ヒーローコード:HP低下時強化",10197,100,-1,"2020/11/10 7:00:00","2020/12/01 2:00:00" +19801,"《倒れぬ意志》リーファ",10198,150,-1,"2020/10/06 7:00:00","2020/10/27 2:00:00" +19802,"《見据える勝利》シノン",10198,150,-1,"2020/10/06 7:00:00","2020/10/27 2:00:00" +19803,"ヴァーデュラス・アニマ",10198,30,-1,"2020/10/06 7:00:00","2020/10/27 2:00:00" +19804,"MEBRマニアック",10198,30,-1,"2020/10/06 7:00:00","2020/10/27 2:00:00" +19805,"ゴールド・ネックレス",10198,30,-1,"2020/10/06 7:00:00","2020/10/27 2:00:00" +19806,"ヒーローコード:ボスキラー",10198,100,-1,"2020/10/06 7:00:00","2020/10/27 2:00:00" +19807,"ヒーローコード:Mobキラー",10198,100,-1,"2020/10/06 7:00:00","2020/10/27 2:00:00" +19808,"ヒーローコード:HP最大時強化",10198,100,-1,"2020/10/06 7:00:00","2020/10/27 2:00:00" +19809,"ヒーローコード:HP低下時強化",10198,100,-1,"2020/10/06 7:00:00","2020/10/27 2:00:00" +19901,"《羽ばたく希望》アスナ",10199,150,-1,"2020/11/24 7:00:00","2020/12/15 2:00:00" +19902,"《輝く勇気》ユウキ",10199,150,-1,"2020/11/24 7:00:00","2020/12/15 2:00:00" +19903,"ラディアント・ライト",10199,30,-1,"2020/11/24 7:00:00","2020/12/15 2:00:00" +19904,"オブシディアナ",10199,30,-1,"2020/11/24 7:00:00","2020/12/15 2:00:00" +19905,"ゴールド・ネックレス",10199,30,-1,"2020/11/24 7:00:00","2020/12/15 2:00:00" +19906,"ヒーローコード:ボスキラー",10199,100,-1,"2020/11/24 7:00:00","2020/12/15 2:00:00" +19907,"ヒーローコード:Mobキラー",10199,100,-1,"2020/11/24 7:00:00","2020/12/15 2:00:00" +19908,"ヒーローコード:HP最大時強化",10199,100,-1,"2020/11/24 7:00:00","2020/12/15 2:00:00" +19909,"ヒーローコード:HP低下時強化",10199,100,-1,"2020/11/24 7:00:00","2020/12/15 2:00:00" +20001,"《宵闇の戦士》キリト",10200,90,-1,"2020/10/13 7:00:00","2020/11/03 2:00:00" +20002,"《愛を紡ぐ天使》アスナ",10200,90,-1,"2020/10/13 7:00:00","2020/11/03 2:00:00" +20003,"《光輝の天使》アリス",10200,15,-1,"2020/10/13 7:00:00","2020/11/03 2:00:00" +20004,"《闇の貴公子》ユージオ",10200,15,-1,"2020/10/13 7:00:00","2020/11/03 2:00:00" +20005,"《愛しき闇天使》ユイ",10200,10,-1,"2020/10/13 7:00:00","2020/11/03 2:00:00" +20101,"《キューティ・ウルフ》ユウキ",10201,90,-1,"2020/10/27 7:00:00","2020/11/17 2:00:00" +20102,"《ブラッキー・ヴァンプ》キリト",10201,15,-1,"2020/10/27 7:00:00","2020/11/17 2:00:00" +20103,"《ラグー・ウィッチ》アスナ",10201,15,-1,"2020/10/27 7:00:00","2020/11/17 2:00:00" +20104,"《ムーンナイト・ウルフ》リーファ",10201,15,-1,"2020/10/27 7:00:00","2020/11/17 2:00:00" +20201,"《輝夜の月うさぎ》アスナ",10202,90,-1,"2020/11/03 7:00:00","2020/11/24 2:00:00" +20202,"《佳宵の月うさぎ》アリス",10202,90,-1,"2020/11/03 7:00:00","2020/11/24 2:00:00" +20203,"《暁の月うさぎ》リーファ",10202,15,-1,"2020/11/03 7:00:00","2020/11/24 2:00:00" +20204,"《宵闇の月うさぎ》シノン",10202,15,-1,"2020/11/03 7:00:00","2020/11/24 2:00:00" +20205,"《黄昏の月うさぎ》ユウキ",10202,10,-1,"2020/11/03 7:00:00","2020/11/24 2:00:00" +20301,"《笑顔の奉迎》レイン",10203,90,-1,"2020/11/17 7:00:00","2020/12/08 2:00:00" +20302,"《氷静の差添》シノン",10203,15,-1,"2020/11/17 7:00:00","2020/12/08 2:00:00" +20303,"《桜袖の応援》シリカ",10203,15,-1,"2020/11/17 7:00:00","2020/12/08 2:00:00" +20304,"《奉仕の戦華》ストレア",10203,15,-1,"2020/11/17 7:00:00","2020/12/08 2:00:00" +20401,"《スウィートウィッチ》ユイ",10204,90,-1,"2020/10/06 7:00:00","2020/10/27 2:00:00" +20402,"《ドリーミィ・バニー》アスナ",10204,90,-1,"2020/10/06 7:00:00","2020/10/27 2:00:00" +20403,"《クレバーソーサレス》シノン",10204,15,-1,"2020/10/06 7:00:00","2020/10/27 2:00:00" +20404,"《マジカルスミス》リズベット",10204,15,-1,"2020/10/06 7:00:00","2020/10/27 2:00:00" +20405,"《キャットモンスター》シリカ",10204,15,-1,"2020/10/06 7:00:00","2020/10/27 2:00:00" +20406,"《ファイン・サービング》直葉",10204,15,-1,"2020/10/06 7:00:00","2020/10/27 2:00:00" +20407,"《ベット・タイム!》ユウキ",10204,15,-1,"2020/10/06 7:00:00","2020/10/27 2:00:00" +20408,"《ベスト・サービス》ストレア",10204,15,-1,"2020/10/06 7:00:00","2020/10/27 2:00:00" +20501,"《永久の約束》キリト&アスナ&ユイ",10205,90,-1,"2020/10/20 7:00:00","2020/11/10 2:00:00" +20502,"《在りし日の宝物》アリス",10205,90,-1,"2020/10/20 7:00:00","2020/11/10 2:00:00" +20503,"《竹馬の友》キリト&ユージオ",10205,90,-1,"2020/10/20 7:00:00","2020/11/10 2:00:00" +20504,"《優艶な幼な妻》アスナ",10205,15,-1,"2020/10/20 7:00:00","2020/11/10 2:00:00" +20505,"《光彩の加護》ユイ",10205,15,-1,"2020/10/20 7:00:00","2020/11/10 2:00:00" +20506,"《絆の双刃》キリト&アスナ",10205,15,-1,"2020/10/20 7:00:00","2020/11/10 2:00:00" +20507,"《在りし日の親友》ユージオ",10205,15,-1,"2020/10/20 7:00:00","2020/11/10 2:00:00" +20508,"《協同調和》アリス&アスナ",10205,15,-1,"2020/10/20 7:00:00","2020/11/10 2:00:00" +20601,"《はにかみ露天》リズベット",10206,90,-1,"2020/11/24 7:00:00","2020/12/15 2:00:00" +20602,"《湯煙のひと時》ユイ",10206,90,-1,"2020/11/24 7:00:00","2020/12/15 2:00:00" +20603,"《食いしん坊トナカイ》シリカ",10206,90,-1,"2020/11/24 7:00:00","2020/12/15 2:00:00" +20604,"《聖夜の歌声》サチ",10206,90,-1,"2020/11/24 7:00:00","2020/12/15 2:00:00" +20605,"《温熱の誘惑》リーファ",10206,15,-1,"2020/11/24 7:00:00","2020/12/15 2:00:00" +20606,"《浴場の洗戯》ユウキ",10206,15,-1,"2020/11/24 7:00:00","2020/12/15 2:00:00" +20607,"《魅惑の綺羅星》リーファ",10206,15,-1,"2020/11/24 7:00:00","2020/12/15 2:00:00" +20608,"《雪明りの贈り物》シノン",10206,15,-1,"2020/11/24 7:00:00","2020/12/15 2:00:00" +20609,"《湯上りの一本》エギル",10206,10,-1,"2020/11/24 7:00:00","2020/12/15 2:00:00" +20610,"《雪旅の宝物》フィリア",10206,10,-1,"2020/11/24 7:00:00","2020/12/15 2:00:00" +20801,"《ジョイフルキャロル》リーファ",10208,150,-1,"2020/12/08 7:00:00","2021/01/19 2:00:00" +20802,"《ホワイトジングル》シノン",10208,150,-1,"2020/12/08 7:00:00","2021/01/19 2:00:00" +20803,"《スノープレゼント》アリス",10208,150,-1,"2020/12/08 7:00:00","2021/01/19 2:00:00" +20804,"エターナル・ツリー",10208,30,-1,"2020/12/08 7:00:00","2021/01/19 2:00:00" +20805,"スノウマン・ステッキ",10208,30,-1,"2020/12/08 7:00:00","2021/01/19 2:00:00" +20806,"ジングル・ヘッド",10208,30,-1,"2020/12/08 7:00:00","2021/01/19 2:00:00" +20807,"マジカル・リース",10208,30,-1,"2020/12/08 7:00:00","2021/01/19 2:00:00" +20808,"ヒーローコード:ボスキラー",10208,100,-1,"2020/12/08 7:00:00","2021/01/19 2:00:00" +20809,"ヒーローコード:Mobキラー",10208,100,-1,"2020/12/08 7:00:00","2021/01/19 2:00:00" +20810,"ヒーローコード:HP最大時強化",10208,100,-1,"2020/12/08 7:00:00","2021/01/19 2:00:00" +20811,"ヒーローコード:HP低下時強化",10208,100,-1,"2020/12/08 7:00:00","2021/01/19 2:00:00" +21301,"《ハッピークリスマス》アスナ",10213,150,-1,"2020/12/08 7:00:00","2021/01/08 2:00:00" +21302,"《ホーリーデコレイト》リズベット",10213,150,-1,"2020/12/08 7:00:00","2021/01/08 2:00:00" +21303,"《ギフト・トゥギャザー! 》ユウキ",10213,150,-1,"2020/12/08 7:00:00","2021/01/08 2:00:00" +21304,"スノウマン・ステッキ",10213,30,-1,"2020/12/08 7:00:00","2021/01/08 2:00:00" +21305,"ジングル・ヘッド",10213,30,-1,"2020/12/08 7:00:00","2021/01/08 2:00:00" +21306,"エターナル・ツリー",10213,30,-1,"2020/12/08 7:00:00","2021/01/08 2:00:00" +21307,"マジカル・リース",10213,30,-1,"2020/12/08 7:00:00","2021/01/08 2:00:00" +21308,"ヒーローコード:ボスキラー",10213,100,-1,"2020/12/08 7:00:00","2021/01/08 2:00:00" +21309,"ヒーローコード:Mobキラー",10213,100,-1,"2020/12/08 7:00:00","2021/01/08 2:00:00" +21310,"ヒーローコード:HP最大時強化",10213,100,-1,"2020/12/08 7:00:00","2021/01/08 2:00:00" +21311,"ヒーローコード:HP低下時強化",10213,100,-1,"2020/12/08 7:00:00","2021/01/08 2:00:00" +21401,"《荒野の閃光》アスナ",10214,150,-1,"2021/01/12 7:00:00","2021/02/02 2:00:00" +21402,"《血盟の狙撃手》シノン",10214,150,-1,"2021/01/12 7:00:00","2021/02/02 2:00:00" +21403,"MEBRマニアック",10214,30,-1,"2021/01/12 7:00:00","2021/02/02 2:00:00" +21404,"アロンダイト",10214,30,-1,"2021/01/12 7:00:00","2021/02/02 2:00:00" +21405,"ゴールド・ネックレス",10214,30,-1,"2021/01/12 7:00:00","2021/02/02 2:00:00" +21406,"ヒーローコード:ボスキラー",10214,100,-1,"2021/01/12 7:00:00","2021/02/02 2:00:00" +21407,"ヒーローコード:Mobキラー",10214,100,-1,"2021/01/12 7:00:00","2021/02/02 2:00:00" +21408,"ヒーローコード:HP最大時強化",10214,100,-1,"2021/01/12 7:00:00","2021/02/02 2:00:00" +21409,"ヒーローコード:HP低下時強化",10214,100,-1,"2021/01/12 7:00:00","2021/02/02 2:00:00" +21501,"《湯浴みの紅潮》シノン",10215,150,-1,"2021/01/19 7:00:00","2021/02/09 2:00:00" +21502,"《秘湯の恥じらい》シリカ",10215,150,-1,"2021/01/19 7:00:00","2021/02/09 2:00:00" +21503,"《火照る温浴》アリス",10215,150,-1,"2021/01/19 7:00:00","2021/02/09 2:00:00" +21504,"フェイルノート",10215,30,-1,"2021/01/19 7:00:00","2021/02/09 2:00:00" +21505,"スクレープ",10215,30,-1,"2021/01/19 7:00:00","2021/02/09 2:00:00" +21506,"金木犀の剣",10215,30,-1,"2021/01/19 7:00:00","2021/02/09 2:00:00" +21507,"ゴールド・ネックレス",10215,30,-1,"2021/01/19 7:00:00","2021/02/09 2:00:00" +21508,"ヒーローコード:ボスキラー",10215,100,-1,"2021/01/19 7:00:00","2021/02/09 2:00:00" +21509,"ヒーローコード:Mobキラー",10215,100,-1,"2021/01/19 7:00:00","2021/02/09 2:00:00" +21510,"ヒーローコード:HP最大時強化",10215,100,-1,"2021/01/19 7:00:00","2021/02/09 2:00:00" +21511,"ヒーローコード:HP低下時強化",10215,100,-1,"2021/01/19 7:00:00","2021/02/09 2:00:00" +21601,"《生彩放つ飛躍》アスナ",10216,150,-1,"2021/01/01 7:00:00","2021/01/26 2:00:00" +21602,"《純真な想望》ユウキ",10216,150,-1,"2021/01/01 7:00:00","2021/01/26 2:00:00" +21603,"《親愛なる漆黒》リーファ",10216,150,-1,"2021/01/01 7:00:00","2021/01/26 2:00:00" +21604,"《黒衣の聖騎士》アリス",10216,150,-1,"2021/01/01 7:00:00","2021/01/26 2:00:00" +21605,"《ハツコイ同盟》リズベット",10216,150,-1,"2021/01/01 7:00:00","2021/01/26 2:00:00" +21606,"《トキメキ同盟》シリカ",10216,150,-1,"2021/01/01 7:00:00","2021/01/26 2:00:00" +21607,"《英雄纏う剣才》ユウキ",10216,150,-1,"2021/01/01 7:00:00","2021/01/26 2:00:00" +21608,"《忘却の憧憬》ユージオ",10216,150,-1,"2021/01/01 7:00:00","2021/01/26 2:00:00" +21609,"《SAOゲーム攻略会議2019》キリト",10216,150,-1,"2021/01/01 7:00:00","2021/01/26 2:00:00" +21610,"《不屈の誓い》キリト",10216,150,-1,"2021/01/01 7:00:00","2021/01/26 2:00:00" +21611,"《明日への誓い》アスナ",10216,150,-1,"2021/01/01 7:00:00","2021/01/26 2:00:00" +21612,"《目映い太陽》リズベット",10216,150,-1,"2021/01/01 7:00:00","2021/01/26 2:00:00" +21613,"《白砂の少女》シリカ",10216,150,-1,"2021/01/01 7:00:00","2021/01/26 2:00:00" +21614,"《透き通る青》アリス",10216,150,-1,"2021/01/01 7:00:00","2021/01/26 2:00:00" +21615,"《紅葉の祝福》ユウキ",10216,150,-1,"2021/01/01 7:00:00","2021/01/26 2:00:00" +21616,"《慈愛の光彩》アスナ",10216,30,-1,"2021/01/01 7:00:00","2021/01/26 2:00:00" +21617,"《快活なお誘い》リズベット",10216,30,-1,"2021/01/01 7:00:00","2021/01/26 2:00:00" +21618,"《至福のひと時》シリカ",10216,30,-1,"2021/01/01 7:00:00","2021/01/26 2:00:00" +21801,"《雪華の美姫》アスナ",10218,90,-1,"2020/12/01 7:00:00","2020/12/22 2:00:00" +21802,"《プリンセスオリオン》直葉",10218,15,-1,"2020/12/01 7:00:00","2020/12/22 2:00:00" +21803,"《聖夜の妖精》シノン",10218,15,-1,"2020/12/01 7:00:00","2020/12/22 2:00:00" +21804,"《ウインタークラウス》ユウキ",10218,15,-1,"2020/12/01 7:00:00","2020/12/22 2:00:00" +21901,"《冬宴の魅雪》ストレア",10219,90,-1,"2020/12/15 7:00:00","2021/01/08 2:00:00" +21902,"《妖精のクリスマス》アスナ",10219,15,-1,"2020/12/15 7:00:00","2021/01/08 2:00:00" +21903,"《秘める恋心》リズベット",10219,15,-1,"2020/12/15 7:00:00","2021/01/08 2:00:00" +21904,"《聖夜の贈り物》サチ",10219,15,-1,"2020/12/15 7:00:00","2021/01/08 2:00:00" +22001,"《温泉の弓姫》シノン",10220,90,-1,"2021/01/19 7:00:00","2021/02/09 2:00:00" +22002,"《湯上り剣士》キリト",10220,15,-1,"2021/01/19 7:00:00","2021/02/09 2:00:00" +22003,"《背流し癒湯姫》アスナ",10220,15,-1,"2021/01/19 7:00:00","2021/02/09 2:00:00" +22004,"《仲良し洗いっこ》シリカ",10220,15,-1,"2021/01/19 7:00:00","2021/02/09 2:00:00" +22101,"《世界を包む星空》キリト",10221,150,-1,"2021/01/26 7:00:00","2021/03/02 2:00:00" +22102,"《世界を導く光》アリス",10221,150,-1,"2021/01/26 7:00:00","2021/03/02 2:00:00" +22103,"《世界を見守る剣》ユージオ",10221,150,-1,"2021/01/26 7:00:00","2021/03/02 2:00:00" +22104,"夜空の剣",10221,30,-1,"2021/01/26 7:00:00","2021/03/02 2:00:00" +22105,"金木犀の剣",10221,30,-1,"2021/01/26 7:00:00","2021/03/02 2:00:00" +22106,"青薔薇の剣",10221,30,-1,"2021/01/26 7:00:00","2021/03/02 2:00:00" +22107,"ゴールド・ネックレス",10221,30,-1,"2021/01/26 7:00:00","2021/03/02 2:00:00" +22108,"ヒーローコード:ボスキラー",10221,100,-1,"2021/01/26 7:00:00","2021/03/02 2:00:00" +22109,"ヒーローコード:Mobキラー",10221,100,-1,"2021/01/26 7:00:00","2021/03/02 2:00:00" +22110,"ヒーローコード:HP最大時強化",10221,100,-1,"2021/01/26 7:00:00","2021/03/02 2:00:00" +22111,"ヒーローコード:HP低下時強化",10221,100,-1,"2021/01/26 7:00:00","2021/03/02 2:00:00" +22201,"《湯の華咲いた》アスナ",10222,150,-1,"2021/02/02 7:00:00","2021/03/23 2:00:00" +22202,"《豊潤な湯浴み》リーファ",10222,150,-1,"2021/02/02 7:00:00","2021/03/23 2:00:00" +22203,"《温浴の寛ぎ》ユウキ",10222,150,-1,"2021/02/02 7:00:00","2021/03/23 2:00:00" +22204,"《秘境の癒湯》アルゴ",10222,150,-1,"2021/02/02 7:00:00","2021/03/23 2:00:00" +22205,"《湯上り素顔》レイン",10222,150,-1,"2021/02/02 7:00:00","2021/03/23 2:00:00" +22206,"アロンダイト",10222,30,-1,"2021/02/02 7:00:00","2021/03/23 2:00:00" +22207,"ヴァルトレーニス",10222,30,-1,"2021/02/02 7:00:00","2021/03/23 2:00:00" +22208,"オブシディアナ",10222,30,-1,"2021/02/02 7:00:00","2021/03/23 2:00:00" +22209,"スクレープ",10222,30,-1,"2021/02/02 7:00:00","2021/03/23 2:00:00" +22210,"ソード・オブ・ホグニ",10222,30,-1,"2021/02/02 7:00:00","2021/03/23 2:00:00" +22211,"ゴールド・ネックレス",10222,30,-1,"2021/02/02 7:00:00","2021/03/23 2:00:00" +22212,"ヒーローコード:ボスキラー",10222,100,-1,"2021/02/02 7:00:00","2021/03/23 2:00:00" +22213,"ヒーローコード:Mobキラー",10222,100,-1,"2021/02/02 7:00:00","2021/03/23 2:00:00" +22214,"ヒーローコード:HP最大時強化",10222,100,-1,"2021/02/02 7:00:00","2021/03/23 2:00:00" +22215,"ヒーローコード:HP低下時強化",10222,100,-1,"2021/02/02 7:00:00","2021/03/23 2:00:00" +22801,"《気儘な黒猫》キリト",10228,150,-1,"2021/02/09 7:00:00","2021/03/16 2:00:00" +22802,"《ねころび愛猫》アスナ",10228,150,-1,"2021/02/09 7:00:00","2021/03/16 2:00:00" +22803,"《奔放な萌猫》リーファ",10228,150,-1,"2021/02/09 7:00:00","2021/03/16 2:00:00" +22804,"《またたび仔猫》ユウキ",10228,150,-1,"2021/02/09 7:00:00","2021/03/16 2:00:00" +22805,"《慧眼の弓猫》シノン",10228,30,-1,"2021/02/09 7:00:00","2021/03/16 2:00:00" +22806,"《友情の桃猫》リズベット",10228,30,-1,"2021/02/09 7:00:00","2021/03/16 2:00:00" +22807,"《可憐な幼猫》シリカ",10228,30,-1,"2021/02/09 7:00:00","2021/03/16 2:00:00" +22808,"《猫耳の豪商》エギル",10228,30,-1,"2021/02/09 7:00:00","2021/03/16 2:00:00" +22809,"キティ・パウ",10228,30,-1,"2021/02/09 7:00:00","2021/03/16 2:00:00" +22810,"ゴールド・ネックレス",10228,30,-1,"2021/02/09 7:00:00","2021/03/16 2:00:00" +22811,"ヒーローコード:ボスキラー",10228,100,-1,"2021/02/09 7:00:00","2021/03/16 2:00:00" +22812,"ヒーローコード:Mobキラー",10228,100,-1,"2021/02/09 7:00:00","2021/03/16 2:00:00" +22813,"ヒーローコード:HP最大時強化",10228,100,-1,"2021/02/09 7:00:00","2021/03/16 2:00:00" +22814,"ヒーローコード:HP低下時強化",10228,100,-1,"2021/02/09 7:00:00","2021/03/16 2:00:00" +23201,"《笑顔守るために》キリト",10232,150,-1,"2021/03/02 7:00:00","2021/03/23 2:00:00" +23202,"《電脳の歌姫》シリカ",10232,150,-1,"2021/03/02 7:00:00","2021/03/23 2:00:00" +23203,"エリュシデータ",10232,30,-1,"2021/03/02 7:00:00","2021/03/23 2:00:00" +23204,"ジャルディーノ",10232,30,-1,"2021/03/02 7:00:00","2021/03/23 2:00:00" +23205,"ゴールド・ネックレス",10232,30,-1,"2021/03/02 7:00:00","2021/03/23 2:00:00" +23206,"ヒーローコード:ボスキラー",10232,100,-1,"2021/03/02 7:00:00","2021/03/23 2:00:00" +23207,"ヒーローコード:Mobキラー",10232,100,-1,"2021/03/02 7:00:00","2021/03/23 2:00:00" +23208,"ヒーローコード:HP最大時強化",10232,100,-1,"2021/03/02 7:00:00","2021/03/23 2:00:00" +23209,"ヒーローコード:HP低下時強化",10232,100,-1,"2021/03/02 7:00:00","2021/03/23 2:00:00" +23301,"《ふたとせ求めて》キリト",10233,150,-1,"2021/03/16 7:00:00","2021/04/13 2:00:00" +23302,"《ふたとせ歩みて》アスナ",10233,150,-1,"2021/03/16 7:00:00","2021/04/13 2:00:00" +23303,"ダークリパルサー",10233,30,-1,"2021/03/16 7:00:00","2021/04/13 2:00:00" +23304,"ラディアント・ライト",10233,30,-1,"2021/03/16 7:00:00","2021/04/13 2:00:00" +23305,"ゴールド・ネックレス",10233,30,-1,"2021/03/16 7:00:00","2021/04/13 2:00:00" +23306,"ヒーローコード:ボスキラー",10233,100,-1,"2021/03/16 7:00:00","2021/04/13 2:00:00" +23307,"ヒーローコード:Mobキラー",10233,100,-1,"2021/03/16 7:00:00","2021/04/13 2:00:00" +23308,"ヒーローコード:HP最大時強化",10233,100,-1,"2021/03/16 7:00:00","2021/04/13 2:00:00" +23309,"ヒーローコード:HP低下時強化",10233,100,-1,"2021/03/16 7:00:00","2021/04/13 2:00:00" +23401,"《甘い真心》リズベット",10234,90,-1,"2021/02/02 7:00:00","2021/02/23 2:00:00" +23402,"《甘い恋のコクハク》シノン",10234,15,-1,"2021/02/02 7:00:00","2021/02/23 2:00:00" +23403,"《思慕の猫姫》シリカ",10234,15,-1,"2021/02/02 7:00:00","2021/02/23 2:00:00" +23404,"《恋焦がれるココロ》ユウキ",10234,15,-1,"2021/02/02 7:00:00","2021/02/23 2:00:00" +23501,"《安らかな夢見》リーファ",10235,90,-1,"2021/02/16 7:00:00","2021/03/09 2:00:00" +23502,"《早天の射手》シノン",10235,15,-1,"2021/02/16 7:00:00","2021/03/09 2:00:00" +23503,"《夢路への誘い》ストレア",10235,15,-1,"2021/02/16 7:00:00","2021/03/09 2:00:00" +23504,"《憩いの歌姫》セブン",10235,15,-1,"2021/02/16 7:00:00","2021/03/09 2:00:00" +23601,"《紫光の絶拳》ユウキ",10236,90,-1,"2021/03/02 7:00:00","2021/03/23 2:00:00" +23602,"《華麗な紅華》アスナ",10236,15,-1,"2021/03/02 7:00:00","2021/03/23 2:00:00" +23603,"《疾風の美技》リーファ",10236,15,-1,"2021/03/02 7:00:00","2021/03/23 2:00:00" +23604,"《赤影の明花》レイン",10236,15,-1,"2021/03/02 7:00:00","2021/03/23 2:00:00" +23701,"《恋情パティシエ》リーファ",10237,90,-1,"2021/03/09 7:00:00","2021/03/30 2:00:00" +23702,"《恒久の愛情》ユージオ",10237,90,-1,"2021/03/09 7:00:00","2021/03/30 2:00:00" +23703,"《最愛パティシエ》アスナ",10237,15,-1,"2021/03/09 7:00:00","2021/03/30 2:00:00" +23704,"《純情ショコラ》シノン",10237,15,-1,"2021/03/09 7:00:00","2021/03/30 2:00:00" +23705,"《恋慕ショコラ》シリカ",10237,15,-1,"2021/03/09 7:00:00","2021/03/30 2:00:00" +23706,"《赤誠の求愛》キリト",10237,15,-1,"2021/03/09 7:00:00","2021/03/30 2:00:00" +23707,"《狂愛の誓い》クラディール",10237,15,-1,"2021/03/09 7:00:00","2021/03/30 2:00:00" +23801,"《心照らす歌声》ユナ",10238,90,-1,"2021/03/16 7:00:00","2021/04/06 2:00:00" +23802,"《心奮う戦い》エイジ",10238,90,-1,"2021/03/16 7:00:00","2021/04/06 2:00:00" +23803,"EXPメモリ",10238,5,-1,"2021/03/16 7:00:00","2021/04/06 2:00:00" +23804,"プラチナ",10238,5,-1,"2021/03/16 7:00:00","2021/04/06 2:00:00" +23901,"《うららかジャンプ》アスナ",10239,150,-1,"2021/03/30 7:00:00","2021/05/04 2:00:00" +23902,"《はるかぜジャンプ》ユウキ",10239,150,-1,"2021/03/30 7:00:00","2021/05/04 2:00:00" +23903,"《ゆきどけジャンプ》アリス",10239,150,-1,"2021/03/30 7:00:00","2021/05/04 2:00:00" +23904,"アロンダイト",10239,30,-1,"2021/03/30 7:00:00","2021/05/04 2:00:00" +23905,"オブシディアナ",10239,30,-1,"2021/03/30 7:00:00","2021/05/04 2:00:00" +23906,"金木犀の剣",10239,30,-1,"2021/03/30 7:00:00","2021/05/04 2:00:00" +23907,"ゴールド・ネックレス",10239,30,-1,"2021/03/30 7:00:00","2021/05/04 2:00:00" +23908,"ヒーローコード:ボスキラー",10239,100,-1,"2021/03/30 7:00:00","2021/05/04 2:00:00" +23909,"ヒーローコード:Mobキラー",10239,100,-1,"2021/03/30 7:00:00","2021/05/04 2:00:00" +23910,"ヒーローコード:HP最大時強化",10239,100,-1,"2021/03/30 7:00:00","2021/05/04 2:00:00" +23911,"ヒーローコード:HP低下時強化",10239,100,-1,"2021/03/30 7:00:00","2021/05/04 2:00:00" +24301,"《世界を越えて》アリス",10243,150,-1,"2021/04/27 7:00:00","2021/06/01 2:00:00" +24302,"《新たな挑戦》ユージオ",10243,150,-1,"2021/04/27 7:00:00","2021/06/01 2:00:00" +24303,"《共に駆ける今》レイン",10243,150,-1,"2021/04/27 7:00:00","2021/06/01 2:00:00" +24304,"金木犀の剣",10243,30,-1,"2021/04/27 7:00:00","2021/06/01 2:00:00" +24305,"青薔薇の剣",10243,30,-1,"2021/04/27 7:00:00","2021/06/01 2:00:00" +24306,"ソード・オブ・ホグニ",10243,30,-1,"2021/04/27 7:00:00","2021/06/01 2:00:00" +24307,"ゴールド・ネックレス",10243,30,-1,"2021/04/27 7:00:00","2021/06/01 2:00:00" +24308,"ヒーローコード:ボスキラー",10243,100,-1,"2021/04/27 7:00:00","2021/06/01 2:00:00" +24309,"ヒーローコード:Mobキラー",10243,100,-1,"2021/04/27 7:00:00","2021/06/01 2:00:00" +24310,"ヒーローコード:HP最大時強化",10243,100,-1,"2021/04/27 7:00:00","2021/06/01 2:00:00" +24311,"ヒーローコード:HP低下時強化",10243,100,-1,"2021/04/27 7:00:00","2021/06/01 2:00:00" +24701,"《フェアリー・ワルツ》アスナ",10247,150,-1,"2021/05/25 7:00:00","2021/06/22 2:00:00" +24702,"《フェアリー・マージ》リーファ",10247,150,-1,"2021/05/25 7:00:00","2021/06/22 2:00:00" +24703,"アロンダイト",10247,30,-1,"2021/05/25 7:00:00","2021/06/22 2:00:00" +24704,"ムラマサ",10247,30,-1,"2021/05/25 7:00:00","2021/06/22 2:00:00" +24705,"ゴールド・ネックレス",10247,30,-1,"2021/05/25 7:00:00","2021/06/22 2:00:00" +24706,"ヒーローコード:ボスキラー",10247,100,-1,"2021/05/25 7:00:00","2021/06/22 2:00:00" +24707,"ヒーローコード:Mobキラー",10247,100,-1,"2021/05/25 7:00:00","2021/06/22 2:00:00" +24708,"ヒーローコード:HP最大時強化",10247,100,-1,"2021/05/25 7:00:00","2021/06/22 2:00:00" +24709,"ヒーローコード:HP低下時強化",10247,100,-1,"2021/05/25 7:00:00","2021/06/22 2:00:00" +24801,"《頼れる盟友》リズベット",10248,150,-1,"2021/03/23 7:00:00","2021/04/20 2:00:00" +24802,"《勇気の跳躍》シリカ",10248,150,-1,"2021/03/23 7:00:00","2021/04/20 2:00:00" +24803,"グリダヴォル",10248,30,-1,"2021/03/23 7:00:00","2021/04/20 2:00:00" +24804,"ジャルディーノ",10248,30,-1,"2021/03/23 7:00:00","2021/04/20 2:00:00" +24805,"ゴールド・ネックレス",10248,30,-1,"2021/03/23 7:00:00","2021/04/20 2:00:00" +24806,"ヒーローコード:ボスキラー",10248,100,-1,"2021/03/23 7:00:00","2021/04/20 2:00:00" +24807,"ヒーローコード:Mobキラー",10248,100,-1,"2021/03/23 7:00:00","2021/04/20 2:00:00" +24808,"ヒーローコード:HP最大時強化",10248,100,-1,"2021/03/23 7:00:00","2021/04/20 2:00:00" +24809,"ヒーローコード:HP低下時強化",10248,100,-1,"2021/03/23 7:00:00","2021/04/20 2:00:00" +24901,"《黒衣の英雄》キリト",10249,150,-1,"2021/04/13 7:00:00","2021/05/08 2:00:00" +24902,"《英雄の剣匠》リズベット",10249,150,-1,"2021/04/13 7:00:00","2021/05/08 2:00:00" +24903,"ダークリパルサー",10249,30,-1,"2021/04/13 7:00:00","2021/05/08 2:00:00" +24904,"グリダヴォル",10249,30,-1,"2021/04/13 7:00:00","2021/05/08 2:00:00" +24905,"ゴールド・ネックレス",10249,30,-1,"2021/04/13 7:00:00","2021/05/08 2:00:00" +24906,"ヒーローコード:ボスキラー",10249,100,-1,"2021/04/13 7:00:00","2021/05/08 2:00:00" +24907,"ヒーローコード:Mobキラー",10249,100,-1,"2021/04/13 7:00:00","2021/05/08 2:00:00" +24908,"ヒーローコード:HP最大時強化",10249,100,-1,"2021/04/13 7:00:00","2021/05/08 2:00:00" +24909,"ヒーローコード:HP低下時強化",10249,100,-1,"2021/04/13 7:00:00","2021/05/08 2:00:00" +25001,"《倒れぬ意志》リーファ",10250,150,-1,"2021/05/01 7:00:00","2021/06/01 2:00:00" +25002,"《見据える勝利》シノン",10250,150,-1,"2021/05/01 7:00:00","2021/06/01 2:00:00" +25003,"《羽ばたく希望》アスナ",10250,150,-1,"2021/05/01 7:00:00","2021/06/01 2:00:00" +25004,"《輝く勇気》ユウキ",10250,150,-1,"2021/05/01 7:00:00","2021/06/01 2:00:00" +25005,"ヴァーデュラス・アニマ",10250,30,-1,"2021/05/01 7:00:00","2021/06/01 2:00:00" +25006,"MEBRマニアック",10250,30,-1,"2021/05/01 7:00:00","2021/06/01 2:00:00" +25007,"ラディアント・ライト",10250,30,-1,"2021/05/01 7:00:00","2021/06/01 2:00:00" +25008,"オブシディアナ",10250,30,-1,"2021/05/01 7:00:00","2021/06/01 2:00:00" +25009,"ゴールド・ネックレス",10250,30,-1,"2021/05/01 7:00:00","2021/06/01 2:00:00" +25010,"ヒーローコード:ボスキラー",10250,100,-1,"2021/05/01 7:00:00","2021/06/01 2:00:00" +25011,"ヒーローコード:Mobキラー",10250,100,-1,"2021/05/01 7:00:00","2021/06/01 2:00:00" +25012,"ヒーローコード:HP最大時強化",10250,100,-1,"2021/05/01 7:00:00","2021/06/01 2:00:00" +25013,"ヒーローコード:HP低下時強化",10250,100,-1,"2021/05/01 7:00:00","2021/06/01 2:00:00" +25101,"《心の傷を越えて》シノン",10251,90,-1,"2021/04/06 7:00:00","2021/04/27 2:00:00" +25102,"《春満開》リーファ",10251,15,-1,"2021/04/06 7:00:00","2021/04/27 2:00:00" +25103,"《敬慕の瞳》シリカ",10251,15,-1,"2021/04/06 7:00:00","2021/04/27 2:00:00" +25104,"《番長》キバオウ",10251,15,-1,"2021/04/06 7:00:00","2021/04/27 2:00:00" +25201,"《碧海の踊り子》シリカ",10252,90,-1,"2021/04/20 7:00:00","2021/05/18 2:00:00" +25202,"《閃花の舞姫》アスナ",10252,15,-1,"2021/04/20 7:00:00","2021/05/18 2:00:00" +25203,"《氷弾の闘舞》シノン",10252,15,-1,"2021/04/20 7:00:00","2021/05/18 2:00:00" +25204,"《探求の踊り手》フィリア",10252,15,-1,"2021/04/20 7:00:00","2021/05/18 2:00:00" +25301,"《完全無欠メイド》アスナ",10253,90,-1,"2021/05/11 7:00:00","2021/06/08 2:00:00" +25302,"《有頂天外メイド》キリト",10253,15,-1,"2021/05/11 7:00:00","2021/06/08 2:00:00" +25303,"《天真爛漫メイド》ユウキ",10253,15,-1,"2021/05/11 7:00:00","2021/06/08 2:00:00" +25304,"《才色兼備メイド》セブン",10253,15,-1,"2021/05/11 7:00:00","2021/06/08 2:00:00" +25501,"《麗凛な獣猫》シノン",10255,150,-1,"2021/06/08 7:00:00","2021/07/27 2:00:00" +25502,"《毛繕い桃猫》リズベット",10255,150,-1,"2021/06/08 7:00:00","2021/07/27 2:00:00" +25503,"《まったり愛猫》シリカ",10255,150,-1,"2021/06/08 7:00:00","2021/07/27 2:00:00" +25504,"《隆隆狩猫》エギル",10255,150,-1,"2021/06/08 7:00:00","2021/07/27 2:00:00" +25505,"《とびつき活猫》ユウキ",10255,150,-1,"2021/06/08 7:00:00","2021/07/27 2:00:00" +25506,"キティ・パウ",10255,30,-1,"2021/06/08 7:00:00","2021/07/27 2:00:00" +25507,"ゴールド・ネックレス",10255,30,-1,"2021/06/08 7:00:00","2021/07/27 2:00:00" +25508,"ヒーローコード:ボスキラー",10255,100,-1,"2021/06/08 7:00:00","2021/07/27 2:00:00" +25509,"ヒーローコード:Mobキラー",10255,100,-1,"2021/06/08 7:00:00","2021/07/27 2:00:00" +25510,"ヒーローコード:HP最大時強化",10255,100,-1,"2021/06/08 7:00:00","2021/07/27 2:00:00" +25511,"ヒーローコード:HP低下時強化",10255,100,-1,"2021/06/08 7:00:00","2021/07/27 2:00:00" +26101,"《犀薫る浜辺》アスナ",10261,150,-1,"1971/07/20 7:00:00","1971/08/17 2:00:00" +26102,"《波打ち際の彩》アリス",10261,150,-1,"1971/07/20 7:00:00","1971/08/17 2:00:00" +26103,"金木犀の剣",10261,30,-1,"1971/07/20 7:00:00","1971/08/17 2:00:00" +26104,"アロンダイト",10261,30,-1,"1971/07/20 7:00:00","1971/08/17 2:00:00" +26105,"ゴールド・ネックレス",10261,30,-1,"1971/07/20 7:00:00","1971/08/17 2:00:00" +26106,"ヒーローコード:ボスキラー",10261,100,-1,"1971/07/20 7:00:00","1971/08/17 2:00:00" +26107,"ヒーローコード:Mobキラー",10261,100,-1,"1971/07/20 7:00:00","1971/08/17 2:00:00" +26108,"ヒーローコード:HP最大時強化",10261,100,-1,"1971/07/20 7:00:00","1971/08/17 2:00:00" +26109,"ヒーローコード:HP低下時強化",10261,100,-1,"1971/07/20 7:00:00","1971/08/17 2:00:00" +26401,"《世界を包む星空》キリト",10264,150,-1,"2021/07/20 7:00:00","2021/08/17 2:00:00" +26402,"《世界を導く光》アリス",10264,150,-1,"2021/07/20 7:00:00","2021/08/17 2:00:00" +26403,"《世界を見守る剣》ユージオ",10264,150,-1,"2021/07/20 7:00:00","2021/08/17 2:00:00" +26404,"夜空の剣",10264,30,-1,"2021/07/20 7:00:00","2021/08/17 2:00:00" +26405,"金木犀の剣",10264,30,-1,"2021/07/20 7:00:00","2021/08/17 2:00:00" +26406,"青薔薇の剣",10264,30,-1,"2021/07/20 7:00:00","2021/08/17 2:00:00" +26407,"ゴールド・ネックレス",10264,30,-1,"2021/07/20 7:00:00","2021/08/17 2:00:00" +26408,"ヒーローコード:ボスキラー",10264,100,-1,"2021/07/20 7:00:00","2021/08/17 2:00:00" +26409,"ヒーローコード:Mobキラー",10264,100,-1,"2021/07/20 7:00:00","2021/08/17 2:00:00" +26410,"ヒーローコード:HP最大時強化",10264,100,-1,"2021/07/20 7:00:00","2021/08/17 2:00:00" +26411,"ヒーローコード:HP低下時強化",10264,100,-1,"2021/07/20 7:00:00","2021/08/17 2:00:00" +26601,"《天幸の恩愛》アスナ",10266,90,-1,"2021/06/01 7:00:00","2021/06/29 2:00:00" +26602,"《甘やかな誓愛》シノン",10266,15,-1,"2021/06/01 7:00:00","2021/06/29 2:00:00" +26603,"《無邪気な幼愛》ユウキ",10266,15,-1,"2021/06/01 7:00:00","2021/06/29 2:00:00" +26604,"《優艶な祝愛》ストレア",10266,15,-1,"2021/06/01 7:00:00","2021/06/29 2:00:00" +26701,"《夜天の桃虹》リズベット",10267,90,-1,"2021/07/06 7:00:00","2021/08/03 2:00:00" +26702,"《光星の天女》アスナ",10267,15,-1,"2021/07/06 7:00:00","2021/08/03 2:00:00" +26703,"《星彩の願い》シリカ",10267,15,-1,"2021/07/06 7:00:00","2021/08/03 2:00:00" +26704,"《宵闇の流星》ユウキ",10267,15,-1,"2021/07/06 7:00:00","2021/08/03 2:00:00" +26901,"《ふたとせ求めて》キリト",10233,150,-1,"2021/06/01 7:00:00","2021/06/29 2:00:00" +26902,"《ふたとせ歩みて》アスナ",10233,150,-1,"2021/06/01 7:00:00","2021/06/29 2:00:00" +26903,"ダークリパルサー",10233,30,-1,"2021/06/01 7:00:00","2021/06/29 2:00:00" +26904,"ラディアント・ライト",10233,30,-1,"2021/06/01 7:00:00","2021/06/29 2:00:00" +26905,"ゴールド・ネックレス",10233,30,-1,"2021/06/01 7:00:00","2021/06/29 2:00:00" +26906,"ヒーローコード:ボスキラー",10233,100,-1,"2021/06/01 7:00:00","2021/06/29 2:00:00" +26907,"ヒーローコード:Mobキラー",10233,100,-1,"2021/06/01 7:00:00","2021/06/29 2:00:00" +26908,"ヒーローコード:HP最大時強化",10233,100,-1,"2021/06/01 7:00:00","2021/06/29 2:00:00" +26909,"ヒーローコード:HP低下時強化",10233,100,-1,"2021/06/01 7:00:00","2021/06/29 2:00:00" +27001,"《碧海の踊り子》シリカ",10252,90,-1,"2021/07/13 7:00:00","2021/08/10 2:00:00" +27002,"《閃花の舞姫》アスナ",10252,15,-1,"2021/07/13 7:00:00","2021/08/10 2:00:00" +27003,"《氷弾の闘舞》シノン",10252,15,-1,"2021/07/13 7:00:00","2021/08/10 2:00:00" +27004,"《探求の踊り手》フィリア",10252,15,-1,"2021/07/13 7:00:00","2021/08/10 2:00:00" +27101,"《世界を越えて》アリス",10243,150,-1,"2021/06/29 7:00:00","2021/08/03 2:00:00" +27102,"《新たな挑戦》ユージオ",10243,150,-1,"2021/06/29 7:00:00","2021/08/03 2:00:00" +27103,"《共に駆ける今》レイン",10243,150,-1,"2021/06/29 7:00:00","2021/08/03 2:00:00" +27104,"金木犀の剣",10243,30,-1,"2021/06/29 7:00:00","2021/08/03 2:00:00" +27105,"青薔薇の剣",10243,30,-1,"2021/06/29 7:00:00","2021/08/03 2:00:00" +27106,"ソード・オブ・ホグニ",10243,30,-1,"2021/06/29 7:00:00","2021/08/03 2:00:00" +27107,"ゴールド・ネックレス",10243,30,-1,"2021/06/29 7:00:00","2021/08/03 2:00:00" +27108,"ヒーローコード:ボスキラー",10243,100,-1,"2021/06/29 7:00:00","2021/08/03 2:00:00" +27109,"ヒーローコード:Mobキラー",10243,100,-1,"2021/06/29 7:00:00","2021/08/03 2:00:00" +27110,"ヒーローコード:HP最大時強化",10243,100,-1,"2021/06/29 7:00:00","2021/08/03 2:00:00" +27111,"ヒーローコード:HP低下時強化",10243,100,-1,"2021/06/29 7:00:00","2021/08/03 2:00:00" +27501,"《倒れぬ意志》リーファ",10250,150,-1,"2021/06/15 7:00:00","2021/07/13 2:00:00" +27502,"《見据える勝利》シノン",10250,150,-1,"2021/06/15 7:00:00","2021/07/13 2:00:00" +27503,"《羽ばたく希望》アスナ",10250,150,-1,"2021/06/15 7:00:00","2021/07/13 2:00:00" +27504,"《輝く勇気》ユウキ",10250,150,-1,"2021/06/15 7:00:00","2021/07/13 2:00:00" +27505,"ヴァーデュラス・アニマ",10250,30,-1,"2021/06/15 7:00:00","2021/07/13 2:00:00" +27506,"MEBRマニアック",10250,30,-1,"2021/06/15 7:00:00","2021/07/13 2:00:00" +27507,"ラディアント・ライト",10250,30,-1,"2021/06/15 7:00:00","2021/07/13 2:00:00" +27508,"オブシディアナ",10250,30,-1,"2021/06/15 7:00:00","2021/07/13 2:00:00" +27509,"ゴールド・ネックレス",10250,30,-1,"2021/06/15 7:00:00","2021/07/13 2:00:00" +27510,"ヒーローコード:ボスキラー",10250,100,-1,"2021/06/15 7:00:00","2021/07/13 2:00:00" +27511,"ヒーローコード:Mobキラー",10250,100,-1,"2021/06/15 7:00:00","2021/07/13 2:00:00" +27512,"ヒーローコード:HP最大時強化",10250,100,-1,"2021/06/15 7:00:00","2021/07/13 2:00:00" +27513,"ヒーローコード:HP低下時強化",10250,100,-1,"2021/06/15 7:00:00","2021/07/13 2:00:00" +27601,"《完全無欠メイド》アスナ",10253,90,-1,"2021/06/22 7:00:00","2021/07/20 2:00:00" +27602,"《有頂天外メイド》キリト",10253,15,-1,"2021/06/22 7:00:00","2021/07/20 2:00:00" +27603,"《天真爛漫メイド》ユウキ",10253,15,-1,"2021/06/22 7:00:00","2021/07/20 2:00:00" +27604,"《才色兼備メイド》セブン",10253,15,-1,"2021/06/22 7:00:00","2021/07/20 2:00:00" +27701,"《微睡の砂浜》リーファ",10277,150,-1,"2021/07/27 7:00:00","2021/09/28 2:00:00" +27702,"《海辺の休息》リズベット",10277,150,-1,"2021/07/27 7:00:00","2021/09/28 2:00:00" +27703,"《夏の陽の夢》ユウキ",10277,150,-1,"2021/07/27 7:00:00","2021/09/28 2:00:00" +27704,"《波際の司令塔》アルゴ",10277,150,-1,"2021/07/27 7:00:00","2021/09/28 2:00:00" +27705,"《心躍る岸辺》レイン",10277,150,-1,"2021/07/27 7:00:00","2021/09/28 2:00:00" +27706,"ヴァルトレーニス",10277,30,-1,"2021/07/27 7:00:00","2021/09/28 2:00:00" +27707,"ジャイアント・タスク",10277,30,-1,"2021/07/27 7:00:00","2021/09/28 2:00:00" +27708,"オブシディアナ",10277,30,-1,"2021/07/27 7:00:00","2021/09/28 2:00:00" +27709,"スクレープ",10277,30,-1,"2021/07/27 7:00:00","2021/09/28 2:00:00" +27710,"ソード・オブ・ホグニ",10277,30,-1,"2021/07/27 7:00:00","2021/09/28 2:00:00" +27711,"ゴールド・ネックレス",10277,30,-1,"2021/07/27 7:00:00","2021/09/28 2:00:00" +27712,"ヒーローコード:ボスキラー",10277,100,-1,"2021/07/27 7:00:00","2021/09/28 2:00:00" +27713,"ヒーローコード:Mobキラー",10277,100,-1,"2021/07/27 7:00:00","2021/09/28 2:00:00" +27714,"ヒーローコード:HP最大時強化",10277,100,-1,"2021/07/27 7:00:00","2021/09/28 2:00:00" +27715,"ヒーローコード:HP低下時強化",10277,100,-1,"2021/07/27 7:00:00","2021/09/28 2:00:00" +28401,"《犀薫る浜辺》アスナ",10284,150,-1,"2021/08/10 7:00:00","2021/09/07 2:00:00" +28402,"《波打ち際の彩》アリス",10284,150,-1,"2021/08/10 7:00:00","2021/09/07 2:00:00" +28403,"金木犀の剣",10284,30,-1,"2021/08/10 7:00:00","2021/09/07 2:00:00" +28404,"アロンダイト",10284,30,-1,"2021/08/10 7:00:00","2021/09/07 2:00:00" +28405,"ゴールド・ネックレス",10284,30,-1,"2021/08/10 7:00:00","2021/09/07 2:00:00" +28406,"ヒーローコード:ボスキラー",10284,100,-1,"2021/08/10 7:00:00","2021/09/07 2:00:00" +28407,"ヒーローコード:Mobキラー",10284,100,-1,"2021/08/10 7:00:00","2021/09/07 2:00:00" +28408,"ヒーローコード:HP最大時強化",10284,100,-1,"2021/08/10 7:00:00","2021/09/07 2:00:00" +28409,"ヒーローコード:HP低下時強化",10284,100,-1,"2021/08/10 7:00:00","2021/09/07 2:00:00" +28501,"《騎士の心得》シノン",10285,150,-1,"2021/09/07 7:00:00","2021/10/05 2:00:00" +28502,"《射撃手の心得》アリス",10285,150,-1,"2021/09/07 7:00:00","2021/10/05 2:00:00" +28503,"金木犀の剣",10285,30,-1,"2021/09/07 7:00:00","2021/10/05 2:00:00" +28504,"MEBRマニアック",10285,30,-1,"2021/09/07 7:00:00","2021/10/05 2:00:00" +28505,"ゴールド・ネックレス",10285,30,-1,"2021/09/07 7:00:00","2021/10/05 2:00:00" +28506,"ヒーローコード:ボスキラー",10285,100,-1,"2021/09/07 7:00:00","2021/10/05 2:00:00" +28507,"ヒーローコード:Mobキラー",10285,100,-1,"2021/09/07 7:00:00","2021/10/05 2:00:00" +28508,"ヒーローコード:HP最大時強化",10285,100,-1,"2021/09/07 7:00:00","2021/10/05 2:00:00" +28509,"ヒーローコード:HP低下時強化",10285,100,-1,"2021/09/07 7:00:00","2021/10/05 2:00:00" +28601,"《真夏の眩耀》アスナ",10286,150,-1,"2021/08/24 7:00:00","2021/09/28 2:00:00" +28602,"《魅惑の夏日》リーファ",10286,150,-1,"2021/08/24 7:00:00","2021/09/28 2:00:00" +28603,"《砂浜の女神》シノン",10286,150,-1,"2021/08/24 7:00:00","2021/09/28 2:00:00" +28604,"《透き通る青》アリス",10286,150,-1,"2021/08/24 7:00:00","2021/09/28 2:00:00" +28605,"《白砂の少女》シリカ",10286,150,-1,"2021/08/24 7:00:00","2021/09/28 2:00:00" +28606,"《目映い太陽》リズベット",10286,150,-1,"2021/08/24 7:00:00","2021/09/28 2:00:00" +28607,"アロンダイト",10286,30,-1,"2021/08/24 7:00:00","2021/09/28 2:00:00" +28608,"ヴァルトレーニス",10286,30,-1,"2021/08/24 7:00:00","2021/09/28 2:00:00" +28609,"MEBRマニアック",10286,30,-1,"2021/08/24 7:00:00","2021/09/28 2:00:00" +28610,"金木犀の剣",10286,30,-1,"2021/08/24 7:00:00","2021/09/28 2:00:00" +28611,"スクレープ",10286,30,-1,"2021/08/24 7:00:00","2021/09/28 2:00:00" +28612,"ジャイアント・タスク",10286,30,-1,"2021/08/24 7:00:00","2021/09/28 2:00:00" +28613,"ゴールド・ネックレス",10286,30,-1,"2021/08/24 7:00:00","2021/09/28 2:00:00" +28614,"ヒーローコード:ボスキラー",10286,100,-1,"2021/08/24 7:00:00","2021/09/28 2:00:00" +28615,"ヒーローコード:Mobキラー",10286,100,-1,"2021/08/24 7:00:00","2021/09/28 2:00:00" +28616,"ヒーローコード:HP最大時強化",10286,100,-1,"2021/08/24 7:00:00","2021/09/28 2:00:00" +28617,"ヒーローコード:HP低下時強化",10286,100,-1,"2021/08/24 7:00:00","2021/09/28 2:00:00" +29001,"《キャンディポップ》シリカ",10290,90,-1,"2021/08/03 7:00:00","2021/08/31 2:00:00" +29002,"《オーシャンケットシー》シノン",10290,15,-1,"2021/08/03 7:00:00","2021/08/31 2:00:00" +29003,"《サマーサンシャイン》アリス",10290,15,-1,"2021/08/03 7:00:00","2021/08/31 2:00:00" +29004,"《サマープリティ》ユイ",10290,15,-1,"2021/08/03 7:00:00","2021/08/31 2:00:00" +29101,"《夏夜の彩り》フィリア",10291,90,-1,"2021/08/24 7:00:00","2021/09/21 2:00:00" +29102,"《月下の憩い》アスナ",10291,15,-1,"2021/08/24 7:00:00","2021/09/21 2:00:00" +29103,"《納涼の弓姫》シノン",10291,15,-1,"2021/08/24 7:00:00","2021/09/21 2:00:00" +29104,"《ひと夏の思い出》ユウキ",10291,15,-1,"2021/08/24 7:00:00","2021/09/21 2:00:00" +29201,"《心舞の熱唱》セブン",10292,90,-1,"2021/09/14 7:00:00","2021/10/12 2:00:00" +29202,"《快勝の導き》シノン",10292,15,-1,"2021/09/14 7:00:00","2021/10/12 2:00:00" +29203,"《熱烈な鼓舞》リズベット",10292,15,-1,"2021/09/14 7:00:00","2021/10/12 2:00:00" +29204,"《飛躍の舞踊》レイン",10292,15,-1,"2021/09/14 7:00:00","2021/10/12 2:00:00" +29301,"《晴天の高揚》ユウキ",10293,90,-1,"2021/07/27 7:00:00","2021/08/24 2:00:00" +29302,"《熱砂の輝き》リズベット",10293,90,-1,"2021/07/27 7:00:00","2021/08/24 2:00:00" +29303,"《炎夏のひと時》ストレア",10293,90,-1,"2021/07/27 7:00:00","2021/08/24 2:00:00" +29304,"《盛夏の思い出》サチ",10293,90,-1,"2021/07/27 7:00:00","2021/08/24 2:00:00" +29305,"《華夜の巧者》シノン",10293,90,-1,"2021/07/27 7:00:00","2021/08/24 2:00:00" +29306,"《はじける笑顔》シリカ",10293,15,-1,"2021/07/27 7:00:00","2021/08/24 2:00:00" +29307,"《真夏の遊戯》キリト",10293,15,-1,"2021/07/27 7:00:00","2021/08/24 2:00:00" +29308,"《納涼の誘い》フィリア",10293,15,-1,"2021/07/27 7:00:00","2021/08/24 2:00:00" +29309,"《暑熱の高鳴り》ユイ",10293,15,-1,"2021/07/27 7:00:00","2021/08/24 2:00:00" +29310,"《一夏の華》アスナ",10293,15,-1,"2021/07/27 7:00:00","2021/08/24 2:00:00" +29311,"《華輝の妖精》リーファ",10293,15,-1,"2021/07/27 7:00:00","2021/08/24 2:00:00" +29312,"《真夏の憧憬》ユウキ",10293,15,-1,"2021/07/27 7:00:00","2021/08/24 2:00:00" +29313,"《熱暑の漢達》エギル&クライン",10293,10,-1,"2021/07/27 7:00:00","2021/08/24 2:00:00" +29314,"《水辺の戯れ》セブン&レイン",10293,10,-1,"2021/07/27 7:00:00","2021/08/24 2:00:00" +29401,"《賽の悦楽》リーファ",10294,150,-1,"2021/09/28 7:00:00","2021/11/09 2:00:00" +29402,"《廻る享楽》シノン",10294,150,-1,"2021/09/28 7:00:00","2021/11/09 2:00:00" +29403,"《兎の務め》アリス",10294,150,-1,"2021/09/28 7:00:00","2021/11/09 2:00:00" +29404,"《二色の歓楽》シリカ",10294,30,-1,"2021/09/28 7:00:00","2021/11/09 2:00:00" +29405,"ヴァルトレーニス",10294,30,-1,"2021/09/28 7:00:00","2021/11/09 2:00:00" +29406,"MEBRマニアック",10294,30,-1,"2021/09/28 7:00:00","2021/11/09 2:00:00" +29407,"金木犀の剣",10294,30,-1,"2021/09/28 7:00:00","2021/11/09 2:00:00" +29408,"ゴールド・ネックレス",10294,30,-1,"2021/09/28 7:00:00","2021/11/09 2:00:00" +29409,"ヒーローコード:ボスキラー",10294,100,-1,"2021/09/28 7:00:00","2021/11/09 2:00:00" +29410,"ヒーローコード:Mobキラー",10294,100,-1,"2021/09/28 7:00:00","2021/11/09 2:00:00" +29411,"ヒーローコード:HP最大時強化",10294,100,-1,"2021/09/28 7:00:00","2021/11/09 2:00:00" +29412,"ヒーローコード:HP低下時強化",10294,100,-1,"2021/09/28 7:00:00","2021/11/09 2:00:00" +29901,"《ぬくもりの黒制服》リズベット",10299,150,-1,"2021/11/02 7:00:00","2021/12/14 2:00:00" +29902,"《闇色の心頼》シリカ",10299,150,-1,"2021/11/02 7:00:00","2021/12/14 2:00:00" +29903,"《黒尽くめの密か》アルゴ",10299,150,-1,"2021/11/02 7:00:00","2021/12/14 2:00:00" +29904,"エリュシデータ",10299,30,-1,"2021/11/02 7:00:00","2021/12/14 2:00:00" +29905,"ダークリパルサー",10299,30,-1,"2021/11/02 7:00:00","2021/12/14 2:00:00" +29906,"ゴールド・ネックレス",10299,30,-1,"2021/11/02 7:00:00","2021/12/14 2:00:00" +29907,"ヒーローコード:ボスキラー",10299,100,-1,"2021/11/02 7:00:00","2021/12/14 2:00:00" +29908,"ヒーローコード:Mobキラー",10299,100,-1,"2021/11/02 7:00:00","2021/12/14 2:00:00" +29909,"ヒーローコード:HP最大時強化",10299,100,-1,"2021/11/02 7:00:00","2021/12/14 2:00:00" +29910,"ヒーローコード:HP低下時強化",10299,100,-1,"2021/11/02 7:00:00","2021/12/14 2:00:00" +30401,"《純白の誓い》キリト",10304,150,-1,"2021/11/16 7:00:00","2021/12/14 2:00:00" +30402,"《純黒の誓い》アスナ",10304,150,-1,"2021/11/16 7:00:00","2021/12/14 2:00:00" +30403,"エリュシデータ",10304,30,-1,"2021/11/16 7:00:00","2021/12/14 2:00:00" +30404,"アロンダイト",10304,30,-1,"2021/11/16 7:00:00","2021/12/14 2:00:00" +30405,"ゴールド・ネックレス",10304,30,-1,"2021/11/16 7:00:00","2021/12/14 2:00:00" +30406,"ヒーローコード:ボスキラー",10304,100,-1,"2021/11/16 7:00:00","2021/12/14 2:00:00" +30407,"ヒーローコード:Mobキラー",10304,100,-1,"2021/11/16 7:00:00","2021/12/14 2:00:00" +30408,"ヒーローコード:HP最大時強化",10304,100,-1,"2021/11/16 7:00:00","2021/12/14 2:00:00" +30409,"ヒーローコード:HP低下時強化",10304,100,-1,"2021/11/16 7:00:00","2021/12/14 2:00:00" +30501,"《赦しの一振り》キリト",10305,150,-1,"2021/10/05 7:00:00","2021/11/02 2:00:00" +30502,"《正しき一突き》アスナ",10305,150,-1,"2021/10/05 7:00:00","2021/11/02 2:00:00" +30503,"ダークリパルサー",10305,30,-1,"2021/10/05 7:00:00","2021/11/02 2:00:00" +30504,"ランベントライト",10305,30,-1,"2021/10/05 7:00:00","2021/11/02 2:00:00" +30505,"ゴールド・ネックレス",10305,30,-1,"2021/10/05 7:00:00","2021/11/02 2:00:00" +30506,"ヒーローコード:ボスキラー",10305,100,-1,"2021/10/05 7:00:00","2021/11/02 2:00:00" +30507,"ヒーローコード:Mobキラー",10305,100,-1,"2021/10/05 7:00:00","2021/11/02 2:00:00" +30508,"ヒーローコード:HP最大時強化",10305,100,-1,"2021/10/05 7:00:00","2021/11/02 2:00:00" +30509,"ヒーローコード:HP低下時強化",10305,100,-1,"2021/10/05 7:00:00","2021/11/02 2:00:00" +30601,"《スウィートウィッチ》ユイ",10306,90,-1,"2021/09/28 7:00:00","2021/10/26 2:00:00" +30602,"《クレバーソーサレス》シノン",10306,15,-1,"2021/09/28 7:00:00","2021/10/26 2:00:00" +30603,"《マジカルスミス》リズベット",10306,15,-1,"2021/09/28 7:00:00","2021/10/26 2:00:00" +30604,"《キャットモンスター》シリカ",10306,15,-1,"2021/09/28 7:00:00","2021/10/26 2:00:00" +30605,"《ドリーミィ・バニー》アスナ",10306,90,-1,"2021/09/28 7:00:00","2021/10/26 2:00:00" +30606,"《ファイン・サービング》直葉",10306,15,-1,"2021/09/28 7:00:00","2021/10/26 2:00:00" +30607,"《ベット・タイム!》ユウキ",10306,15,-1,"2021/09/28 7:00:00","2021/10/26 2:00:00" +30608,"《ベスト・サービス》ストレア",10306,15,-1,"2021/09/28 7:00:00","2021/10/26 2:00:00" +30701,"《ジェット・ヴォルフ》キリト",10307,90,-1,"2021/10/05 7:00:00","2021/11/02 2:00:00" +30702,"《弓猫の魔法》シノン",10307,15,-1,"2021/10/05 7:00:00","2021/11/02 2:00:00" +30703,"《トリックキャット》ユウキ",10307,15,-1,"2021/10/05 7:00:00","2021/11/02 2:00:00" +30704,"《キャットマジック》フィリア",10307,15,-1,"2021/10/05 7:00:00","2021/11/02 2:00:00" +30801,"《黒煌の剣影》リーファ",10308,90,-1,"2021/10/26 7:00:00","2021/11/23 2:00:00" +30802,"《漆黒の桃華》リズベット",10308,15,-1,"2021/10/26 7:00:00","2021/11/23 2:00:00" +30803,"《ゴシックビウィッチ》ストレア",10308,15,-1,"2021/10/26 7:00:00","2021/11/23 2:00:00" +30804,"《可憐な小華》ユイ",10308,15,-1,"2021/10/26 7:00:00","2021/11/23 2:00:00" +30901,"《聖夜の純愛》アスナ",10309,90,-1,"2021/12/07 7:00:00","2022/01/04 2:00:00" +30902,"《彩氷の福音》シノン",10309,15,-1,"2021/12/07 7:00:00","2022/01/04 2:00:00" +30903,"《冬宴の楽しみ》ユウキ",10309,15,-1,"2021/12/07 7:00:00","2022/01/04 2:00:00" +30904,"《魅惑の甘味》フィリア",10309,15,-1,"2021/12/07 7:00:00","2022/01/04 2:00:00" +31001,"《生彩放つ飛躍》アスナ",10310,150,-1,"2021/10/30 7:00:00","2021/12/21 2:00:00" +31002,"《純真な想望》ユウキ",10310,150,-1,"2021/10/30 7:00:00","2021/12/21 2:00:00" +31003,"《親愛なる漆黒》リーファ",10310,150,-1,"2021/10/30 7:00:00","2021/12/21 2:00:00" +31004,"《黒衣の聖騎士》アリス",10310,150,-1,"2021/10/30 7:00:00","2021/12/21 2:00:00" +31005,"《ハツコイ同盟》リズベット",10310,150,-1,"2021/10/30 7:00:00","2021/12/21 2:00:00" +31006,"《トキメキ同盟》シリカ",10310,150,-1,"2021/10/30 7:00:00","2021/12/21 2:00:00" +31007,"《英雄纏う剣才》ユウキ",10310,150,-1,"2021/10/30 7:00:00","2021/12/21 2:00:00" +31008,"《忘却の憧憬》ユージオ",10310,150,-1,"2021/10/30 7:00:00","2021/12/21 2:00:00" +31009,"《荒野の閃光》アスナ",10310,150,-1,"2021/10/30 7:00:00","2021/12/21 2:00:00" +31010,"《血盟の狙撃手》シノン",10310,150,-1,"2021/10/30 7:00:00","2021/12/21 2:00:00" +31011,"《笑顔守るために》キリト",10310,150,-1,"2021/10/30 7:00:00","2021/12/21 2:00:00" +31012,"《電脳の歌姫》シリカ",10310,150,-1,"2021/10/30 7:00:00","2021/12/21 2:00:00" +31013,"《永久の約束》キリト&アスナ&ユイ",10310,90,-1,"2021/10/30 7:00:00","2021/12/21 2:00:00" +31014,"《優艶な幼な妻》アスナ",10310,15,-1,"2021/10/30 7:00:00","2021/12/21 2:00:00" +31015,"《光彩の加護》ユイ",10310,15,-1,"2021/10/30 7:00:00","2021/12/21 2:00:00" +31016,"《絆の双刃》キリト&アスナ",10310,15,-1,"2021/10/30 7:00:00","2021/12/21 2:00:00" +31017,"《心照らす歌声》ユナ",10310,90,-1,"2021/10/30 7:00:00","2021/12/21 2:00:00" +31018,"《心奮う戦い》エイジ",10310,90,-1,"2021/10/30 7:00:00","2021/12/21 2:00:00" +31201,"《聖しあの夜》シリカ",10312,150,-1,"2021/12/14 7:00:00","2022/01/25 2:00:00" +31202,"《慶びの枕元》アルゴ",10312,150,-1,"2021/12/14 7:00:00","2022/01/25 2:00:00" +31203,"《届ける聖歌》レイン",10312,150,-1,"2021/12/14 7:00:00","2022/01/25 2:00:00" +31204,"スノウマン・ステッキ",10312,30,-1,"2021/12/14 7:00:00","2022/01/25 2:00:00" +31205,"ジングル・ヘッド",10312,30,-1,"2021/12/14 7:00:00","2022/01/25 2:00:00" +31206,"エターナル・ツリー",10312,30,-1,"2021/12/14 7:00:00","2022/01/25 2:00:00" +31207,"マジカル・リース",10312,30,-1,"2021/12/14 7:00:00","2022/01/25 2:00:00" +31208,"ゴールド・ネックレス",10312,30,-1,"2021/12/14 7:00:00","2022/01/25 2:00:00" +31209,"《食いしん坊トナカイ》シリカ",10312,90,-1,"2021/12/14 7:00:00","2022/01/25 2:00:00" +31210,"《聖夜の歌声》サチ",10312,90,-1,"2021/12/14 7:00:00","2022/01/25 2:00:00" +31211,"《魅惑の綺羅星》リーファ",10312,15,-1,"2021/12/14 7:00:00","2022/01/25 2:00:00" +31212,"《雪明りの贈り物》シノン",10312,15,-1,"2021/12/14 7:00:00","2022/01/25 2:00:00" +31213,"《雪旅の宝物》フィリア",10312,10,-1,"2021/12/14 7:00:00","2022/01/25 2:00:00" +31214,"ヒーローコード:ボスキラー",10312,100,-1,"2021/12/14 7:00:00","2022/01/25 2:00:00" +31215,"ヒーローコード:Mobキラー",10312,100,-1,"2021/12/14 7:00:00","2022/01/25 2:00:00" +31216,"ヒーローコード:HP最大時強化",10312,100,-1,"2021/12/14 7:00:00","2022/01/25 2:00:00" +31217,"ヒーローコード:HP低下時強化",10312,100,-1,"2021/12/14 7:00:00","2022/01/25 2:00:00" +31701,"《ハッピークリスマス》アスナ",10317,150,-1,"2021/12/14 7:00:00","2022/01/08 2:00:00" +31702,"《ホーリーデコレイト》リズベット",10317,150,-1,"2021/12/14 7:00:00","2022/01/08 2:00:00" +31703,"《ギフト・トゥギャザー!》ユウキ",10317,150,-1,"2021/12/14 7:00:00","2022/01/08 2:00:00" +31704,"《ジョイフルキャロル》リーファ",10317,150,-1,"2021/12/14 7:00:00","2022/01/08 2:00:00" +31705,"《ホワイトジングル》シノン",10317,150,-1,"2021/12/14 7:00:00","2022/01/08 2:00:00" +31706,"《スノープレゼント》アリス",10317,150,-1,"2021/12/14 7:00:00","2022/01/08 2:00:00" +31707,"スノウマン・ステッキ",10317,30,-1,"2021/12/14 7:00:00","2022/01/08 2:00:00" +31708,"ジングル・ヘッド",10317,30,-1,"2021/12/14 7:00:00","2022/01/08 2:00:00" +31709,"エターナル・ツリー",10317,30,-1,"2021/12/14 7:00:00","2022/01/08 2:00:00" +31710,"マジカル・リース",10317,30,-1,"2021/12/14 7:00:00","2022/01/08 2:00:00" +31711,"ゴールド・ネックレス",10317,30,-1,"2021/12/14 7:00:00","2022/01/08 2:00:00" +31712,"《食いしん坊トナカイ》シリカ",10317,90,-1,"2021/12/14 7:00:00","2022/01/08 2:00:00" +31713,"《聖夜の歌声》サチ",10317,90,-1,"2021/12/14 7:00:00","2022/01/08 2:00:00" +31714,"《魅惑の綺羅星》リーファ",10317,15,-1,"2021/12/14 7:00:00","2022/01/08 2:00:00" +31715,"《雪明りの贈り物》シノン",10317,15,-1,"2021/12/14 7:00:00","2022/01/08 2:00:00" +31716,"《雪旅の宝物》フィリア",10317,10,-1,"2021/12/14 7:00:00","2022/01/08 2:00:00" +31717,"ヒーローコード:ボスキラー",10317,100,-1,"2021/12/14 7:00:00","2022/01/08 2:00:00" +31718,"ヒーローコード:Mobキラー",10317,100,-1,"2021/12/14 7:00:00","2022/01/08 2:00:00" +31719,"ヒーローコード:HP最大時強化",10317,100,-1,"2021/12/14 7:00:00","2022/01/08 2:00:00" +31720,"ヒーローコード:HP低下時強化",10317,100,-1,"2021/12/14 7:00:00","2022/01/08 2:00:00" +31901,"《鏡花水月》ユウキ",10319,90,-1,"2021/12/28 7:00:00","2022/01/25 2:00:00" +31902,"《萬福衝打》キリト",10319,15,-1,"2021/12/28 7:00:00","2022/01/25 2:00:00" +31903,"《錦上添花》直葉",10319,15,-1,"2021/12/28 7:00:00","2022/01/25 2:00:00" +31904,"《雲中白鶴》シノン",10319,15,-1,"2021/12/28 7:00:00","2022/01/25 2:00:00" +32001,"《飛翔の新天地》キリト",10320,150,-1,"2022/01/01 7:00:00","2022/02/01 2:00:00" +32002,"《風舞う妖精》リーファ",10320,150,-1,"2022/01/01 7:00:00","2022/02/01 2:00:00" +32003,"アメノハバキリ",10320,30,-1,"2022/01/01 7:00:00","2022/02/01 2:00:00" +32004,"ゴールド・ネックレス",10320,30,-1,"2022/01/01 7:00:00","2022/02/01 2:00:00" +32005,"ヒーローコード:ボスキラー",10320,100,-1,"2022/01/01 7:00:00","2022/02/01 2:00:00" +32006,"ヒーローコード:Mobキラー",10320,100,-1,"2022/01/01 7:00:00","2022/02/01 2:00:00" +32007,"ヒーローコード:HP最大時強化",10320,100,-1,"2022/01/01 7:00:00","2022/02/01 2:00:00" +32008,"ヒーローコード:HP低下時強化",10320,100,-1,"2022/01/01 7:00:00","2022/02/01 2:00:00" +32101,"《真夏の眩耀》アスナ",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32102,"《魅惑の夏日》リーファ",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32103,"《砂浜の女神》シノン",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32104,"《目映い太陽》リズベット",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32105,"《白砂の少女》シリカ",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32106,"《透き通る青》アリス",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32107,"《微睡の砂浜》リーファ",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32108,"《海辺の休息》リズベット",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32109,"《夏の陽の夢》ユウキ",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32110,"《波際の司令塔》アルゴ",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32111,"《心躍る岸辺》レイン",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32112,"《犀薫る浜辺》アスナ",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32113,"《波打ち際の彩》アリス",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32114,"《SAOゲーム攻略会議2019》キリト",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32115,"《不屈の誓い》キリト",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32116,"《明日への誓い》アスナ",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32117,"《兄想う妖精》リーファ",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32118,"《向き合う勇気》シノン",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32119,"《黒衣の英雄》キリト",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32120,"《英雄の剣匠》リズベット",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32121,"《フェアリー・ワルツ》アスナ",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32122,"《フェアリー・マージ》リーファ",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32123,"《紅葉の祝福》ユウキ",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32127,"《休息の爪先》アルゴ",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32128,"《背氷の気迫》ユージオ",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32129,"《可憐な銃剣士》キリト",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32130,"《美麗な狙撃手》シノン",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32131,"《頭領の音頭》クライン",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32132,"《乙女の髪留め》リーファ",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32133,"《月宙の飛躍》アルゴ",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32134,"《遠望の彼方》ユウキ",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32135,"《女神の羽休め》シノン",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32136,"《プレゼント大作戦!》アスナ",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32137,"《潮風の悪戯》アスナ",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32138,"《おやつ・タイム!》シリカ",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32139,"《妖精の戯れ》リーファ",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32140,"《穏やかな一時》アスナ",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32141,"《さかさま花世界》ユウキ",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32142,"《休日の談笑》リズベット",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32143,"《晴天の君》レイン",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32144,"《安らぎの我が家》キリト",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32145,"《幸せの青い鳥》シノン",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32146,"《秘密の甘邸》アルゴ",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32147,"《一輪の幼花》シリカ",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32148,"《揺蕩う夢幻》ユウキ",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32149,"《熟達の志士》アリス",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32150,"《かろやかスキップ》レイン",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32151,"《予期せぬ接触》シノン",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32152,"《ちゃーみー・けっとしー》シリカ",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32153,"《麗姿の眼差し》アスナ",10321,150,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32154,"《慈愛の光彩》アスナ",10321,30,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32155,"《快活なお誘い》リズベット",10321,30,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32156,"《至福のひと時》シリカ",10321,30,-1,"2022/01/01 7:00:00","2022/02/22 2:00:00" +32301,"《火照り身少女》シリカ",10323,90,-1,"2022/01/18 7:00:00","2022/02/15 2:00:00" +32302,"《ほっと一息》リズベット",10323,15,-1,"2022/01/18 7:00:00","2022/02/15 2:00:00" +32303,"《湯船の遊影》ユウキ",10323,15,-1,"2022/01/18 7:00:00","2022/02/15 2:00:00" +32304,"《保養の湯掛》セブン",10323,15,-1,"2022/01/18 7:00:00","2022/02/15 2:00:00" +32401,"《相愛は甘やかに》アスナ",10324,150,-1,"2022/01/25 7:00:00","2022/03/08 2:00:00" +32402,"《親愛はとろけて》シノン",10324,150,-1,"2022/01/25 7:00:00","2022/03/08 2:00:00" +32403,"《愛慕は未だ青く》アリス",10324,150,-1,"2022/01/25 7:00:00","2022/03/08 2:00:00" +32404,"アロンダイト",10324,30,-1,"2022/01/25 7:00:00","2022/03/08 2:00:00" +32405,"金木犀の剣",10324,30,-1,"2022/01/25 7:00:00","2022/03/08 2:00:00" +32406,"MEBRマニアック",10324,30,-1,"2022/01/25 7:00:00","2022/03/08 2:00:00" +32407,"ゴールド・ネックレス",10324,30,-1,"2022/01/25 7:00:00","2022/03/08 2:00:00" +32408,"ヒーローコード:ボスキラー",10324,100,-1,"2022/01/25 7:00:00","2022/03/08 2:00:00" +32409,"ヒーローコード:Mobキラー",10324,100,-1,"2022/01/25 7:00:00","2022/03/08 2:00:00" +32410,"ヒーローコード:HP最大時強化",10324,100,-1,"2022/01/25 7:00:00","2022/03/08 2:00:00" +32411,"ヒーローコード:HP低下時強化",10324,100,-1,"2022/01/25 7:00:00","2022/03/08 2:00:00" +32901,"《気儘な黒猫》キリト",10329,150,-1,"2022/02/01 7:00:00","2022/03/08 2:00:00" +32902,"《ねころび愛猫》アスナ",10329,150,-1,"2022/02/01 7:00:00","2022/03/08 2:00:00" +32903,"《奔放な萌猫》リーファ",10329,150,-1,"2022/02/01 7:00:00","2022/03/08 2:00:00" +32904,"《またたび仔猫》ユウキ",10329,150,-1,"2022/02/01 7:00:00","2022/03/08 2:00:00" +32905,"《慧眼の弓猫》シノン",10329,30,-1,"2022/02/01 7:00:00","2022/03/08 2:00:00" +32906,"《友情の桃猫》リズベット",10329,30,-1,"2022/02/01 7:00:00","2022/03/08 2:00:00" +32907,"《可憐な幼猫》シリカ",10329,30,-1,"2022/02/01 7:00:00","2022/03/08 2:00:00" +32908,"《猫耳の豪商》エギル",10329,30,-1,"2022/02/01 7:00:00","2022/03/08 2:00:00" +32909,"キティ・パウ",10329,30,-1,"2022/02/01 7:00:00","2022/03/08 2:00:00" +32910,"ゴールド・ネックレス",10329,30,-1,"2022/02/01 7:00:00","2022/03/08 2:00:00" +32911,"ヒーローコード:ボスキラー",10329,100,-1,"2022/02/01 7:00:00","2022/03/08 2:00:00" +32912,"ヒーローコード:Mobキラー",10329,100,-1,"2022/02/01 7:00:00","2022/03/08 2:00:00" +32913,"ヒーローコード:HP最大時強化",10329,100,-1,"2022/02/01 7:00:00","2022/03/08 2:00:00" +32914,"ヒーローコード:HP低下時強化",10329,100,-1,"2022/02/01 7:00:00","2022/03/08 2:00:00" +33101,"《狙い撃つ軌道》ユウキ",10331,150,-1,"2022/02/15 7:00:00","2022/03/15 2:00:00" +33102,"《魔剣稽古》ユージオ",10331,150,-1,"2022/02/15 7:00:00","2022/03/15 2:00:00" +33103,"AMRティアマト",10331,30,-1,"2022/02/15 7:00:00","2022/03/15 2:00:00" +33104,"マサムネG4",10331,30,-1,"2022/02/15 7:00:00","2022/03/15 2:00:00" +33105,"ゴールド・ネックレス",10331,30,-1,"2022/02/15 7:00:00","2022/03/15 2:00:00" +33106,"ヒーローコード:ボスキラー",10331,100,-1,"2022/02/15 7:00:00","2022/03/15 2:00:00" +33107,"ヒーローコード:Mobキラー",10331,100,-1,"2022/02/15 7:00:00","2022/03/15 2:00:00" +33108,"ヒーローコード:HP最大時強化",10331,100,-1,"2022/02/15 7:00:00","2022/03/15 2:00:00" +33109,"ヒーローコード:HP低下時強化",10331,100,-1,"2022/02/15 7:00:00","2022/03/15 2:00:00" +33201,"《一途なラブハート》リーファ",10332,90,-1,"2022/02/08 7:00:00","2022/03/08 2:00:00" +33202,"《愛のメッセージ》アスナ",10332,15,-1,"2022/02/08 7:00:00","2022/03/08 2:00:00" +33203,"《愛情の弾丸》シノン",10332,15,-1,"2022/02/08 7:00:00","2022/03/08 2:00:00" +33204,"《甘い恋心》フィリア",10332,15,-1,"2022/02/08 7:00:00","2022/03/08 2:00:00" +33301,"《恋情パティシエ》リーファ",10333,90,-1,"2022/03/01 7:00:00","2022/03/29 2:00:00" +33302,"《最愛パティシエ》アスナ",10333,15,-1,"2022/03/01 7:00:00","2022/03/29 2:00:00" +33303,"《純情ショコラ》シノン",10333,15,-1,"2022/03/01 7:00:00","2022/03/29 2:00:00" +33304,"《恋慕ショコラ》シリカ",10333,15,-1,"2022/03/01 7:00:00","2022/03/29 2:00:00" +33305,"《恒久の愛情》ユージオ",10333,90,-1,"2022/03/01 7:00:00","2022/03/29 2:00:00" +33306,"《赤誠の求愛》キリト",10333,15,-1,"2022/03/01 7:00:00","2022/03/29 2:00:00" +33307,"《狂愛の誓い》クラディール",10333,15,-1,"2022/03/01 7:00:00","2022/03/29 2:00:00" +33401,"《厳格手ほどき》シノン",10334,150,-1,"2022/03/08 7:00:00","2022/04/26 2:00:00" +33402,"《ほどけぬ胸飾り》リズベット",10334,150,-1,"2022/03/08 7:00:00","2022/04/26 2:00:00" +33403,"《夢想の放課後》シリカ",10334,150,-1,"2022/03/08 7:00:00","2022/04/26 2:00:00" +33404,"フェイルノート",10334,30,-1,"2022/03/08 7:00:00","2022/04/26 2:00:00" +33405,"ジャイアント・タスク",10334,30,-1,"2022/03/08 7:00:00","2022/04/26 2:00:00" +33406,"スクレープ",10334,30,-1,"2022/03/08 7:00:00","2022/04/26 2:00:00" +33407,"ゴールド・ネックレス",10334,30,-1,"2022/03/08 7:00:00","2022/04/26 2:00:00" +33408,"ヒーローコード:ボスキラー",10334,100,-1,"2022/03/08 7:00:00","2022/04/26 2:00:00" +33409,"ヒーローコード:Mobキラー",10334,100,-1,"2022/03/08 7:00:00","2022/04/26 2:00:00" +33410,"ヒーローコード:HP最大時強化",10334,100,-1,"2022/03/08 7:00:00","2022/04/26 2:00:00" +33411,"ヒーローコード:HP低下時強化",10334,100,-1,"2022/03/08 7:00:00","2022/04/26 2:00:00" +33901,"《熊の招宴》アスナ",10339,150,-1,"2022/03/29 7:00:00","2022/04/26 2:00:00" +33902,"《兎の密会》アリス",10339,150,-1,"2022/03/29 7:00:00","2022/04/26 2:00:00" +33903,"キティ・パウ",10339,30,-1,"2022/03/29 7:00:00","2022/04/26 2:00:00" +33904,"ゴールド・ネックレス",10339,30,-1,"2022/03/29 7:00:00","2022/04/26 2:00:00" +33905,"ヒーローコード:ボスキラー",10339,100,-1,"2022/03/29 7:00:00","2022/04/26 2:00:00" +33906,"ヒーローコード:Mobキラー",10339,100,-1,"2022/03/29 7:00:00","2022/04/26 2:00:00" +33907,"ヒーローコード:HP最大時強化",10339,100,-1,"2022/03/29 7:00:00","2022/04/26 2:00:00" +33908,"ヒーローコード:HP低下時強化",10339,100,-1,"2022/03/29 7:00:00","2022/04/26 2:00:00" +34101,"《うららかジャンプ》アスナ",10341,150,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34102,"《はるかぜジャンプ》ユウキ",10341,150,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34103,"《ゆきどけジャンプ》アリス",10341,150,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34104,"《ふたとせ求めて》キリト",10341,150,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34105,"《ふたとせ歩みて》アスナ",10341,150,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34106,"《麗凛な獣猫》シノン",10341,150,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34107,"《毛繕い桃猫》リズベット",10341,150,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34108,"《まったり愛猫》シリカ",10341,150,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34109,"《隆隆狩猫》エギル",10341,150,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34110,"《とびつき活猫》ユウキ",10341,150,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34111,"《湯浴みの紅潮》シノン",10341,150,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34112,"《秘湯の恥じらい》シリカ",10341,150,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34113,"《火照る温浴》アリス",10341,150,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34114,"《湯の華咲いた》アスナ",10341,150,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34115,"《豊潤な湯浴み》リーファ",10341,150,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34116,"《温浴の寛ぎ》ユウキ",10341,150,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34117,"《秘境の癒湯》アルゴ",10341,150,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34118,"《湯上り素顔》レイン",10341,150,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34119,"《倒れぬ意志》リーファ",10341,150,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34120,"《見据える勝利》シノン",10341,150,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34121,"《羽ばたく希望》アスナ",10341,150,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34122,"《輝く勇気》ユウキ",10341,150,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34123,"《世界を包む星空》キリト",10341,150,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34124,"《世界を導く光》アリス",10341,150,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34125,"《世界を見守る剣》ユージオ",10341,150,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34126,"夜空の剣",10341,30,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34127,"金木犀の剣",10341,30,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34128,"青薔薇の剣",10341,30,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34129,"ヴァーデュラス・アニマ",10341,30,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34130,"オブシディアナ",10341,30,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34131,"ソード・オブ・ホグニ",10341,30,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34132,"ラディアント・ライト",10341,30,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34133,"ジャルディーノ",10341,30,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34134,"キティ・パウ",10341,30,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34135,"アニヒレート・レイ",10341,30,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34136,"AMRティアマト",10341,30,-1,"2022/03/08 7:00:00","2022/05/03 2:00:00" +34201,"《零れる活力》リーファ",10342,50,-1,"2022/04/26 7:00:00","2022/06/07 2:00:00" +34202,"《瞬きの宙》ユウキ",10342,50,-1,"2022/04/26 7:00:00","2022/06/07 2:00:00" +34203,"《就床の一揖》レイン",10342,50,-1,"2022/04/26 7:00:00","2022/06/07 2:00:00" +34204,"《不屈の鍛冶屋魂》リズベット",10342,50,-1,"2022/04/26 7:00:00","2022/06/07 2:00:00" +34205,"《絆が生む力》シリカ",10342,50,-1,"2022/04/26 7:00:00","2022/06/07 2:00:00" +34206,"《養う英気》リーファ",10342,50,-1,"2022/04/26 7:00:00","2022/06/07 2:00:00" +34207,"《秘めたる日誌》アルゴ",10342,50,-1,"2022/04/26 7:00:00","2022/06/07 2:00:00" +34208,"《即席軽音楽団》レイン",10342,50,-1,"2022/04/26 7:00:00","2022/06/07 2:00:00" +34209,"《頬張り黒猫》キリト",10342,50,-1,"2022/04/26 7:00:00","2022/06/07 2:00:00" +34210,"《身支度愛猫》アスナ",10342,50,-1,"2022/04/26 7:00:00","2022/06/07 2:00:00" diff --git a/titles/sao/data/HeroLogLevel.csv b/titles/sao/data/HeroLogLevel.csv new file mode 100644 index 0000000..36a53b9 --- /dev/null +++ b/titles/sao/data/HeroLogLevel.csv @@ -0,0 +1,121 @@ +HeroLogLevelId,RequireExp +1,0, +2,1000, +3,1200, +4,1400, +5,1600, +6,1900, +7,2200, +8,2500, +9,2800, +10,3100, +11,3500, +12,3900, +13,4300, +14,4700, +15,5100, +16,5550, +17,6000, +18,6450, +19,6900, +20,7350, +21,7850, +22,8350, +23,8850, +24,9350, +25,9850, +26,10450, +27,11050, +28,11650, +29,12250, +30,12850, +31,13550, +32,14250, +33,14950, +34,15650, +35,16350, +36,17150, +37,17950, +38,18750, +39,19550, +40,20350, +41,21250, +42,22150, +43,23050, +44,23950, +45,24850, +46,25850, +47,26850, +48,27850, +49,28850, +50,29850, +51,30950, +52,32050, +53,33150, +54,34250, +55,35350, +56,36550, +57,37750, +58,38950, +59,40150, +60,41350, +61,42650, +62,43950, +63,45250, +64,46550, +65,47850, +66,49250, +67,50650, +68,52050, +69,53450, +70,54850, +71,56350, +72,57850, +73,59350, +74,60850, +75,62350, +76,63950, +77,65550, +78,67150, +79,68750, +80,70350, +81,72050, +82,73750, +83,75450, +84,77150, +85,78850, +86,80650, +87,82450, +88,84250, +89,86050, +90,87850, +91,89750, +92,91650, +93,93550, +94,95450, +95,97350, +96,99350, +97,101350, +98,103350, +99,105350, +100,107350, +101,200000, +102,300000, +103,450000, +104,600000, +105,800000, +106,1000000, +107,1250000, +108,1500000, +109,1800000, +110,2100000, +111,2100000, +112,2100000, +113,2100000, +114,2100000, +115,2100000, +116,2100000, +117,2100000, +118,2100000, +119,2100000, +120,2100000, diff --git a/titles/sao/data/PlayerRank.csv b/titles/sao/data/PlayerRank.csv new file mode 100644 index 0000000..9031661 --- /dev/null +++ b/titles/sao/data/PlayerRank.csv @@ -0,0 +1,301 @@ +PlayerRankId,TotalExp, +1,0, +2,100, +3,300, +4,500, +5,800, +6,1100, +7,1400, +8,1800, +9,2200, +10,2600,, +11,3000,, +12,3500,, +13,4000,, +14,4500,, +15,5000,, +16,5500,, +17,6100,, +18,6700,, +19,7300,, +20,7900,, +21,8500,, +22,9100,, +23,9800,, +24,10500, +25,11200, +26,11900, +27,12600, +28,13300, +29,14000, +30,14800, +31,15600, +32,16400, +33,17200, +34,18000, +35,18800, +36,19600, +37,20400, +38,21300, +39,22200, +40,23100, +41,24000, +42,24900, +43,25800, +44,26700, +45,27600, +46,28500, +47,29500, +48,30600, +49,31800, +50,33100, +51,34500, +52,36000, +53,37600, +54,39300, +55,41100, +56,43000, +57,45000, +58,47100, +59,49300, +60,51600, +61,54000, +62,56500, +63,59100, +64,61800, +65,64600, +66,67500, +67,70500, +68,73600, +69,76800, +70,80100, +71,83500, +72,87000, +73,90600, +74,94300, +75,98100, +76,102000, +77,106000, +78,110100, +79,114300, +80,118600, +81,123000, +82,127500, +83,132100, +84,136800, +85,141600, +86,146500, +87,151500, +88,156600, +89,161800, +90,167100, +91,172500, +92,178000, +93,183600, +94,189300, +95,195100, +96,201000, +97,207000, +98,213100, +99,219300, +100,225600, +101,232000, +102,238500, +103,245100, +104,251800, +105,258600, +106,265500, +107,272500, +108,279600, +109,286800, +110,294100, +111,301500, +112,309000, +113,316600, +114,324300, +115,332100, +116,340000, +117,348000, +118,356100, +119,364300, +120,372600, +121,381000, +122,389500, +123,398100, +124,406800, +125,415600, +126,424500, +127,433500, +128,442600, +129,451800, +130,461100, +131,470500, +132,480000, +133,489600, +134,499300, +135,509100, +136,519000, +137,529000, +138,539100, +139,549300, +140,559600, +141,570000, +142,580500, +143,591100, +144,601800, +145,612600, +146,623500, +147,634500, +148,645600, +149,656800, +150,668100, +151,679500, +152,691000, +153,702600, +154,714300, +155,726100, +156,738000, +157,750000, +158,762100, +159,774300, +160,786600, +161,799000, +162,811500, +163,824100, +164,836800, +165,849600, +166,862500, +167,875500, +168,888600, +169,901800, +170,915100, +171,928500, +172,942000, +173,955600, +174,969300, +175,983100, +176,997000, +177,1011000, +178,1025100, +179,1039300, +180,1053600, +181,1068000, +182,1082500, +183,1097100, +184,1111800, +185,1126600, +186,1141500, +187,1156500, +188,1171600, +189,1186800, +190,1202100, +191,1217500, +192,1233000, +193,1248600, +194,1264300, +195,1280100, +196,1296000, +197,1312000, +198,1328100, +199,1344300, +200,1360600, +201,1377000, +202,1393500, +203,1410100, +204,1426800, +205,1443600, +206,1460500, +207,1477500, +208,1494600, +209,1511800, +210,1529100, +211,1546500, +212,1564000, +213,1581600, +214,1599300, +215,1617100, +216,1635000, +217,1653000, +218,1671100, +219,1689300, +220,1707600, +221,1726000, +222,1744500, +223,1763100, +224,1781800, +225,1800600, +226,1819500, +227,1838500, +228,1857600, +229,1876800, +230,1896100, +231,1915500, +232,1935000, +233,1954600, +234,1974300, +235,1994100, +236,2014000, +237,2034000, +238,2054100, +239,2074300, +240,2094600, +241,2115000, +242,2135500, +243,2156100, +244,2176800, +245,2197600, +246,2218500, +247,2239500, +248,2260600, +249,2281800, +250,2303100, +251,2324500, +252,2346000, +253,2367600, +254,2389300, +255,2411100, +256,2433000, +257,2455000, +258,2477100, +259,2499300, +260,2521600, +261,2544000, +262,2566500, +263,2589100, +264,2611800, +265,2634600, +266,2657500, +267,2680500, +268,2703600, +269,2726800, +270,2750100, +271,2773500, +272,2797000, +273,2820600, +274,2844300, +275,2868100, +276,2892000, +277,2916000, +278,2940100, +279,2964300, +280,2988600, +281,3013000, +282,3037500, +283,3062100, +284,3086800, +285,3111600, +286,3136500, +287,3161500, +288,3186600, +289,3211800, +290,3237100, +291,3262500, +292,3288000, +293,3313600, +294,3339300, +295,3365100, +296,3391000, +297,3417000, +298,3443100, +299,3469300, +300,3495600, diff --git a/titles/sao/data/ResEarnCampaignShopItems.csv b/titles/sao/data/ResEarnCampaignShopItems.csv new file mode 100644 index 0000000..6d5dd17 --- /dev/null +++ b/titles/sao/data/ResEarnCampaignShopItems.csv @@ -0,0 +1,7 @@ +ResEarnCampaignShopItemId,ResEarnCampaignShopId,CommonRewardType,CommonRewardId,CommonRewardNum,Strength,Property1PropertyId,Property1Value1,Property1Value2,Property2PropertyId,Property2Value1,Property2Value2,Property3PropertyId,Property3Value1,Property3Value2,Property4PropertyId,Property4Value1,Property4Value2 +15701,15701,1,101000220,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +15702,15702,1,102000230,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +15703,15703,2,101000009,1,0,204000,5,0,1,0,0,2,0,0,2,0,0 +15801,15801,8,212000030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +15802,15802,8,201000060,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +15803,15803,8,297000090,1,0,1,0,0,1,0,0,1,0,0,1,0,0 diff --git a/titles/sao/data/ResEarnCampaignShops.csv b/titles/sao/data/ResEarnCampaignShops.csv new file mode 100644 index 0000000..2c201c9 --- /dev/null +++ b/titles/sao/data/ResEarnCampaignShops.csv @@ -0,0 +1,7 @@ +ResEarnCampaignShopId,ResEarnCampaignApplicationId,Name,SellingYuiMedal,SellingCol,SellingEventItemId,SellingEventItemNum,PurchaseLimit,GetApplicationPoint,StartDate,EndDate +15701,10001,"≪不屈の誓い≫キリト",-1,1,-1,-1,-1,1,"2020/01/01 7:00:00","2020/03/31 2:00:00" +15702,10001,"≪明日への誓い≫アスナ",-1,12,-1,-1,-1,1,"2020/01/01 7:00:00","2020/03/31 2:00:00" +15703,10001,"ダークリパルサー",-1,123,-1,-1,-1,1,"2020/01/01 7:00:00","2020/03/31 2:00:00" +15801,10001,"《恒久の愛情》ユージオ",-1,1234,-1,-1,-1,1,"2020/01/01 7:00:00","2020/03/24 2:00:00" +15802,10001,"《赤誠の求愛》キリト",-1,12345,-1,-1,-1,1,"2020/01/01 7:00:00","2020/03/24 2:00:00" +15803,10001,"《狂愛の誓い》クラディール",-1,123456,-1,-1,-1,1,"2020/01/01 7:00:00","2020/03/24 2:00:00" diff --git a/titles/sao/data/RewardTable.csv b/titles/sao/data/RewardTable.csv new file mode 100644 index 0000000..929ff82 --- /dev/null +++ b/titles/sao/data/RewardTable.csv @@ -0,0 +1,17274 @@ +RewardTableId,UnanalyzedLogGradeId,CommonRewardId,CommonRewardType +142,1,101000000,2 +143,2,101000001,2 +144,3,101000002,2 +145,4,101000008,2 +146,5,101000004,2 +147,1,102000000,2 +148,2,102000001,2 +149,3,102000002,2 +150,4,102000003,2 +151,5,102000004,2 +152,1,103000000,2 +153,2,103000001,2 +154,3,103000002,2 +155,4,103000003,2 +156,5,103000004,2 +157,1,105000000,2 +158,2,105000001,2 +159,3,105000002,2 +160,4,105000003,2 +161,5,105000004,2 +162,1,108000000,2 +163,2,108000001,2 +164,3,108000002,2 +165,4,108000003,2 +166,5,108000004,2 +167,1,109000000,2 +168,2,109000001,2 +169,3,109000002,2 +170,4,109000003,2 +171,5,109000004,2 +172,1,111000000,2 +173,2,111000001,2 +174,3,111000002,2 +175,4,111000003,2 +176,5,111000004,2 +177,1,112000000,2 +178,2,112000001,2 +179,3,112000002,2 +180,4,112000003,2 +181,5,112000004,2 +182,1,120000000,2 +183,2,120000001,2 +184,3,120000002,2 +185,4,120000003,2 +186,5,120000004,2 +187,1,101000010,1 +188,2,101000030,1 +189,3,101000040,1 +190,1,102000010,1 +191,2,102000030,1 +192,3,102000070,1 +193,1,103000010,1 +194,2,103000040,1 +195,3,103000070,1 +196,1,104000010,1 +197,2,104000030,1 +198,3,104000070,1 +199,1,105000010,1 +200,2,105000040,1 +201,3,105000070,1 +202,1,106000010,1 +203,2,106000030,1 +204,3,106000070,1 +205,1,107000010,1 +206,2,107000040,1 +207,3,107000070,1 +208,1,108000010,1 +209,2,108000030,1 +210,3,108000050,1 +212,1,101000000,2 +213,2,101000001,2 +214,3,101000002,2 +215,4,101000008,2 +216,5,101000004,2 +218,1,101000000,2 +219,2,101000001,2 +220,3,101000002,2 +221,4,101000008,2 +222,5,101000004,2 +223,1,102000000,2 +224,2,102000001,2 +225,3,102000002,2 +226,4,102000003,2 +227,5,102000004,2 +228,1,103000000,2 +229,2,103000001,2 +230,3,103000002,2 +231,4,103000003,2 +232,5,103000004,2 +233,1,105000000,2 +234,2,105000001,2 +235,3,105000002,2 +236,4,105000003,2 +237,5,105000004,2 +238,1,108000000,2 +239,2,108000001,2 +240,3,108000002,2 +241,4,108000003,2 +242,5,108000004,2 +243,1,109000000,2 +244,2,109000001,2 +245,3,109000002,2 +246,4,109000003,2 +247,5,109000004,2 +248,1,111000000,2 +249,2,111000001,2 +250,3,111000002,2 +251,4,111000003,2 +252,5,111000004,2 +253,1,112000000,2 +254,2,112000001,2 +255,3,112000002,2 +256,4,112000003,2 +257,5,112000004,2 +258,1,120000000,2 +259,2,120000001,2 +260,3,120000002,2 +261,4,120000003,2 +262,5,120000004,2 +263,1,101000010,1 +264,2,101000020,1 +265,2,101000030,1 +266,3,101000040,1 +267,3,101000050,1 +268,3,101000060,1 +269,3,101000070,1 +270,3,101000080,1 +271,1,102000010,1 +272,2,102000020,1 +273,2,102000030,1 +274,2,102000040,1 +275,3,102000050,1 +276,3,102000060,1 +277,3,102000070,1 +278,3,102000080,1 +279,1,103000010,1 +280,2,103000020,1 +281,2,103000030,1 +282,2,103000040,1 +283,3,103000050,1 +284,3,103000060,1 +285,3,103000070,1 +286,3,103000080,1 +287,1,104000010,1 +288,2,104000020,1 +289,2,104000030,1 +290,2,104000040,1 +291,3,104000050,1 +292,3,104000060,1 +293,3,104000070,1 +294,3,104000080,1 +295,1,105000010,1 +296,2,105000020,1 +297,2,105000030,1 +298,2,105000040,1 +299,3,105000050,1 +300,3,105000060,1 +301,3,105000070,1 +302,3,105000080,1 +303,1,106000010,1 +304,2,106000020,1 +305,2,106000030,1 +306,2,106000040,1 +307,3,106000050,1 +308,3,106000060,1 +309,3,106000070,1 +310,3,106000080,1 +311,1,107000010,1 +312,2,107000020,1 +313,2,107000030,1 +314,2,107000040,1 +315,3,107000050,1 +316,3,107000060,1 +317,3,107000070,1 +318,3,107000080,1 +319,1,108000010,1 +320,2,108000020,1 +321,2,108000030,1 +322,2,108000040,1 +323,3,108000050,1 +324,3,108000060,1 +325,3,108000070,1 +326,3,108000080,1 +327,2,180000,3 +329,1,101000000,2 +330,2,101000001,2 +331,3,101000002,2 +332,4,101000008,2 +333,5,101000004,2 +334,1,102000000,2 +335,2,102000001,2 +336,3,102000002,2 +337,4,102000003,2 +338,5,102000004,2 +339,1,103000000,2 +340,2,103000001,2 +341,3,103000002,2 +342,4,103000003,2 +343,5,103000004,2 +344,1,105000000,2 +345,2,105000001,2 +346,3,105000002,2 +347,4,105000003,2 +348,5,105000004,2 +349,1,108000000,2 +350,2,108000001,2 +351,3,108000002,2 +352,4,108000003,2 +353,5,108000004,2 +354,1,109000000,2 +355,2,109000001,2 +356,3,109000002,2 +357,4,109000003,2 +358,5,109000004,2 +359,1,111000000,2 +360,2,111000001,2 +361,3,111000002,2 +362,4,111000003,2 +363,5,111000004,2 +364,1,112000000,2 +365,2,112000001,2 +366,3,112000002,2 +367,4,112000003,2 +368,5,112000004,2 +369,1,120000000,2 +370,2,120000001,2 +371,3,120000002,2 +372,4,120000003,2 +373,5,120000004,2 +374,1,101000010,1 +375,2,101000020,1 +376,2,101000030,1 +377,3,101000040,1 +378,3,101000050,1 +379,3,101000060,1 +380,3,101000070,1 +381,3,101000080,1 +382,1,102000010,1 +383,2,102000020,1 +384,2,102000030,1 +385,2,102000040,1 +386,3,102000050,1 +387,3,102000060,1 +388,3,102000070,1 +389,3,102000080,1 +390,1,103000010,1 +391,2,103000020,1 +392,2,103000030,1 +393,2,103000040,1 +394,3,103000050,1 +395,3,103000060,1 +396,3,103000070,1 +397,3,103000080,1 +398,1,104000010,1 +399,2,104000020,1 +400,2,104000030,1 +401,2,104000040,1 +402,3,104000050,1 +403,3,104000060,1 +404,3,104000070,1 +405,3,104000080,1 +406,1,105000010,1 +407,2,105000020,1 +408,2,105000030,1 +409,2,105000040,1 +410,3,105000050,1 +411,3,105000060,1 +412,3,105000070,1 +413,3,105000080,1 +414,1,106000010,1 +415,2,106000020,1 +416,2,106000030,1 +417,2,106000040,1 +418,3,106000050,1 +419,3,106000060,1 +420,3,106000070,1 +421,3,106000080,1 +422,1,107000010,1 +423,2,107000020,1 +424,2,107000030,1 +425,2,107000040,1 +426,3,107000050,1 +427,3,107000060,1 +428,3,107000070,1 +429,3,107000080,1 +430,1,108000010,1 +431,2,108000020,1 +432,2,108000030,1 +433,2,108000040,1 +434,3,108000050,1 +435,3,108000060,1 +436,3,108000070,1 +437,3,108000080,1 +438,2,180000,3 +440,1,101000000,2 +441,2,101000001,2 +442,3,101000002,2 +443,4,101000008,2 +444,5,101000004,2 +446,1,102000000,2 +447,2,102000001,2 +448,3,102000002,2 +449,4,102000003,2 +450,5,102000004,2 +451,1,112000000,2 +452,2,112000001,2 +453,3,112000002,2 +454,4,112000003,2 +455,5,112000004,2 +457,1,101000000,2 +458,2,101000001,2 +459,3,101000002,2 +460,4,101000008,2 +461,5,101000004,2 +462,1,120000000,2 +463,2,120000001,2 +464,3,120000002,2 +465,4,120000003,2 +466,5,120000004,2 +468,1,111000000,2 +469,2,111000001,2 +470,3,111000002,2 +471,4,111000003,2 +472,5,111000004,2 +473,1,120000000,2 +474,2,120000001,2 +475,3,120000002,2 +476,4,120000003,2 +477,5,120000004,2 +479,1,109000000,2 +480,2,109000001,2 +481,3,109000002,2 +482,4,109000003,2 +483,5,109000004,2 +484,1,112000000,2 +485,2,112000001,2 +486,3,112000002,2 +487,4,112000003,2 +488,5,112000004,2 +490,1,103000000,2 +491,2,103000001,2 +492,3,103000002,2 +493,4,103000003,2 +494,5,103000004,2 +495,1,112000000,2 +496,2,112000001,2 +497,3,112000002,2 +498,4,112000003,2 +499,5,112000004,2 +501,1,105000000,2 +502,2,105000001,2 +503,3,105000002,2 +504,4,105000003,2 +505,5,105000004,2 +506,1,120000000,2 +507,2,120000001,2 +508,3,120000002,2 +509,4,120000003,2 +510,5,120000004,2 +512,1,108000000,2 +513,2,108000001,2 +514,3,108000002,2 +515,4,108000003,2 +516,5,108000004,2 +517,1,120000000,2 +518,2,120000001,2 +519,3,120000002,2 +520,4,120000003,2 +521,5,120000004,2 +523,1,101000010,1 +524,1,103000010,1 +525,2,103000030,1 +526,2,103000040,1 +527,2,107000020,1 +528,2,101000030,1 +529,3,101000050,1 +530,3,101000060,1 +531,3,101000080,1 +532,3,103000060,1 +533,3,103000070,1 +534,3,103000080,1 +535,3,101000040,1 +536,1,101000000,2 +537,2,101000001,2 +538,3,101000002,2 +539,4,101000008,2 +540,5,101000004,2 +541,1,112000000,2 +542,2,112000001,2 +543,3,112000002,2 +544,4,112000003,2 +545,5,112000004,2 +546,2,101000005,2 +547,2,112000005,2 +548,3,170000,3 +549,4,170001,3 +551,1,102000010,1 +552,2,102000030,1 +553,2,102000040,1 +554,2,104000020,1 +555,3,102000060,1 +556,3,102000070,1 +557,3,102000080,1 +558,3,103000050,1 +559,3,105000050,1 +560,1,102000000,2 +561,2,102000001,2 +562,3,102000002,2 +563,4,102000003,2 +564,5,102000004,2 +565,1,112000000,2 +566,2,112000001,2 +567,3,112000002,2 +568,4,112000003,2 +569,5,112000004,2 +570,3,170000,3 +571,4,170001,3 +573,1,106000010,1 +574,2,106000030,1 +575,2,106000040,1 +576,2,105000020,1 +577,3,106000060,1 +578,3,106000070,1 +579,3,106000080,1 +580,3,101000070,1 +581,1,103000000,2 +582,2,103000001,2 +583,3,103000002,2 +584,4,103000003,2 +585,5,103000004,2 +586,1,112000000,2 +587,2,112000001,2 +588,3,112000002,2 +589,4,112000003,2 +590,5,112000004,2 +591,3,170000,3 +592,4,170001,3 +594,1,104000010,1 +595,2,104000030,1 +596,2,104000040,1 +597,2,108000020,1 +598,3,104000060,1 +599,3,104000070,1 +600,3,104000080,1 +601,3,102000050,1 +602,1,111000000,2 +603,2,111000001,2 +604,3,111000002,2 +605,4,111000003,2 +606,5,111000004,2 +607,1,120000000,2 +608,2,120000001,2 +609,3,120000002,2 +610,4,120000003,2 +611,5,120000004,2 +612,1,110020,3 +613,1,110030,3 +614,1,110040,3 +615,1,110050,3 +616,3,110060,3 +617,3,110070,3 +618,3,110080,3 +619,3,110090,3 +620,3,110100,3 +621,3,110110,3 +622,3,110120,3 +623,3,110130,3 +624,3,110140,3 +625,3,110150,3 +626,3,110160,3 +627,3,110170,3 +628,3,110180,3 +629,3,110190,3 +630,3,110200,3 +631,3,110210,3 +632,3,110220,3 +633,3,110230,3 +634,3,110240,3 +635,3,110250,3 +636,3,110260,3 +637,3,110270,3 +639,1,107000010,1 +640,2,107000030,1 +641,2,107000040,1 +642,2,101000020,1 +643,3,107000060,1 +644,3,107000070,1 +645,3,107000080,1 +646,3,108000050,1 +647,1,105000000,2 +648,2,105000001,2 +649,3,105000002,2 +650,4,105000003,2 +651,5,105000004,2 +652,1,120000000,2 +653,2,120000001,2 +654,3,120000002,2 +655,4,120000003,2 +656,5,120000004,2 +657,3,180000,3 +658,4,180001,3 +660,1,105000010,1 +661,2,105000030,1 +662,2,105000040,1 +663,2,102000020,1 +664,2,103000020,1 +665,3,105000060,1 +666,3,105000070,1 +667,3,105000080,1 +668,3,104000050,1 +669,3,106000050,1 +670,1,109000000,2 +671,2,109000001,2 +672,3,109000002,2 +673,4,109000003,2 +674,5,109000004,2 +675,1,112000000,2 +676,2,112000001,2 +677,3,112000002,2 +678,4,112000003,2 +679,5,112000004,2 +680,1,120020,3 +681,1,120030,3 +682,1,120040,3 +683,1,120050,3 +684,3,120240,3 +685,3,120250,3 +686,3,120260,3 +687,3,120270,3 +688,3,120300,3 +689,3,120310,3 +690,3,120320,3 +691,3,120330,3 +692,3,120340,3 +693,3,120350,3 +694,3,120360,3 +695,3,120370,3 +696,3,120380,3 +697,3,120390,3 +698,3,120400,3 +699,3,120410,3 +700,3,120420,3 +701,3,120430,3 +702,3,120450,3 +703,3,120460,3 +705,1,108000010,1 +706,2,108000030,1 +707,2,108000040,1 +708,2,106000020,1 +709,3,108000060,1 +710,3,108000070,1 +711,3,108000080,1 +712,3,107000050,1 +713,1,108000000,2 +714,2,108000001,2 +715,3,108000002,2 +716,4,108000003,2 +717,5,108000004,2 +718,1,120000000,2 +719,2,120000001,2 +720,3,120000002,2 +721,4,120000003,2 +722,5,120000004,2 +723,1,130020,3 +724,1,130030,3 +725,1,130040,3 +726,1,130050,3 +727,3,130060,3 +728,3,130070,3 +729,3,130080,3 +730,3,130090,3 +731,3,130100,3 +732,3,130110,3 +733,3,130120,3 +734,3,130130,3 +735,3,130140,3 +736,3,130150,3 +737,3,130160,3 +738,3,130170,3 +739,3,130180,3 +740,3,130190,3 +741,3,130200,3 +742,3,130420,3 +743,3,130510,3 +744,3,130520,3 +745,3,130530,3 +746,3,130540,3 +748,1,105000010,1 +749,2,105000030,1 +750,2,105000040,1 +751,2,102000020,1 +752,2,103000020,1 +753,3,105000060,1 +754,3,105000070,1 +755,3,105000080,1 +756,3,104000050,1 +757,3,106000050,1 +758,1,109000000,2 +759,2,109000001,2 +760,3,109000002,2 +761,4,109000003,2 +762,5,109000004,2 +763,1,112000000,2 +764,2,112000001,2 +765,3,112000002,2 +766,4,112000003,2 +767,5,112000004,2 +768,3,170000,3 +769,4,170001,3 +771,1,104000010,1 +772,2,104000030,1 +773,2,104000040,1 +774,2,108000020,1 +775,3,104000060,1 +776,3,104000070,1 +777,3,104000080,1 +778,3,102000050,1 +779,1,111000000,2 +780,2,111000001,2 +781,3,111000002,2 +782,4,111000003,2 +783,5,111000004,2 +784,1,120000000,2 +785,2,120000001,2 +786,3,120000002,2 +787,4,120000003,2 +788,5,120000004,2 +789,1,110020,3 +790,1,110030,3 +791,1,110040,3 +792,1,110050,3 +793,3,110060,3 +794,3,110070,3 +795,3,110080,3 +796,3,110090,3 +797,3,110100,3 +798,3,110110,3 +799,3,110120,3 +800,3,110130,3 +801,3,110140,3 +802,3,110150,3 +803,3,110160,3 +804,3,110170,3 +805,3,110180,3 +806,3,110190,3 +807,3,110200,3 +808,3,110210,3 +809,3,110220,3 +810,3,110230,3 +811,3,110240,3 +812,3,110250,3 +813,3,110260,3 +814,3,110270,3 +816,1,102000010,1 +817,2,102000030,1 +818,2,102000040,1 +819,2,104000020,1 +820,3,102000060,1 +821,3,102000070,1 +822,3,102000080,1 +823,3,103000050,1 +824,3,105000050,1 +825,1,102000000,2 +826,2,102000001,2 +827,3,102000002,2 +828,4,102000003,2 +829,5,102000004,2 +830,1,112000000,2 +831,2,112000001,2 +832,3,112000002,2 +833,4,112000003,2 +834,5,112000004,2 +835,3,170001,3 +836,4,170002,3 +838,1,102000010,1 +839,2,102000030,1 +840,2,102000040,1 +841,2,104000020,1 +842,3,102000060,1 +843,3,102000070,1 +844,3,102000080,1 +845,3,103000050,1 +846,3,105000050,1 +847,1,102000000,2 +848,2,102000001,2 +849,3,102000002,2 +850,4,102000003,2 +851,5,102000004,2 +852,1,112000000,2 +853,2,112000001,2 +854,3,112000002,2 +855,4,112000003,2 +856,5,112000004,2 +857,1,110020,3 +858,1,110030,3 +859,1,110040,3 +860,1,110050,3 +861,2,110021,3 +862,2,110031,3 +863,2,110041,3 +864,2,110051,3 +865,3,110060,3 +866,3,110070,3 +867,3,110080,3 +868,3,110090,3 +869,3,110100,3 +870,3,110110,3 +871,3,110120,3 +872,3,110130,3 +873,3,110140,3 +874,3,110150,3 +875,3,110160,3 +876,3,110170,3 +877,3,110180,3 +878,3,110190,3 +879,3,110200,3 +880,3,110210,3 +881,3,110220,3 +882,3,110230,3 +883,3,110240,3 +884,3,110250,3 +885,3,110260,3 +886,3,110270,3 +887,4,140000,3 +889,1,101000010,1 +890,1,103000010,1 +891,2,103000030,1 +892,2,103000040,1 +893,2,107000020,1 +894,2,101000030,1 +895,3,101000050,1 +896,3,101000060,1 +897,3,101000080,1 +898,3,103000060,1 +899,3,103000070,1 +900,3,103000080,1 +901,3,101000040,1 +902,1,101000000,2 +903,2,101000001,2 +904,3,101000002,2 +905,4,101000008,2 +906,5,101000004,2 +907,1,112000000,2 +908,2,112000001,2 +909,3,112000002,2 +910,4,112000003,2 +911,5,112000004,2 +912,1,120020,3 +913,1,120030,3 +914,1,120040,3 +915,1,120050,3 +916,2,120021,3 +917,2,120031,3 +918,2,120041,3 +919,2,120051,3 +920,3,120240,3 +921,3,120250,3 +922,3,120260,3 +923,3,120270,3 +924,3,120300,3 +925,3,120310,3 +926,3,120320,3 +927,3,120330,3 +928,3,120340,3 +929,3,120350,3 +930,3,120360,3 +931,3,120370,3 +932,3,120380,3 +933,3,120390,3 +934,3,120400,3 +935,3,120410,3 +936,3,120420,3 +937,3,120430,3 +938,3,120450,3 +939,3,120460,3 +940,4,140000,3 +942,1,106000010,1 +943,2,106000030,1 +944,2,106000040,1 +945,2,105000020,1 +946,3,106000060,1 +947,3,106000070,1 +948,3,106000080,1 +949,3,101000070,1 +950,1,103000000,2 +951,2,103000001,2 +952,3,103000002,2 +953,4,103000003,2 +954,5,103000004,2 +955,1,112000000,2 +956,2,112000001,2 +957,3,112000002,2 +958,4,112000003,2 +959,5,112000004,2 +960,3,180001,3 +961,4,180002,3 +963,1,101000010,1 +964,1,103000010,1 +965,2,103000030,1 +966,2,103000040,1 +967,2,107000020,1 +968,2,101000030,1 +969,3,101000050,1 +970,3,101000060,1 +971,3,101000080,1 +972,3,103000060,1 +973,3,103000070,1 +974,3,103000080,1 +975,3,101000040,1 +976,1,101000000,2 +977,2,101000001,2 +978,3,101000002,2 +979,4,101000008,2 +980,5,101000004,2 +981,1,120000000,2 +982,2,120000001,2 +983,3,120000002,2 +984,4,120000003,2 +985,5,120000004,2 +986,3,170001,3 +987,4,170002,3 +989,1,105000010,1 +990,2,105000030,1 +991,2,105000040,1 +992,2,102000020,1 +993,2,103000020,1 +994,3,105000060,1 +995,3,105000070,1 +996,3,105000080,1 +997,3,104000050,1 +998,3,106000050,1 +999,1,109000000,2 +1000,2,109000001,2 +1001,3,109000002,2 +1002,4,109000003,2 +1003,5,109000004,2 +1004,1,112000000,2 +1005,2,112000001,2 +1006,3,112000002,2 +1007,4,112000003,2 +1008,5,112000004,2 +1009,1,130020,3 +1010,1,130030,3 +1011,1,130040,3 +1012,1,130050,3 +1013,2,130021,3 +1014,2,130031,3 +1015,2,130041,3 +1016,2,130051,3 +1017,3,130060,3 +1018,3,130070,3 +1019,3,130080,3 +1020,3,130090,3 +1021,3,130100,3 +1022,3,130110,3 +1023,3,130120,3 +1024,3,130130,3 +1025,3,130140,3 +1026,3,130150,3 +1027,3,130160,3 +1028,3,130170,3 +1029,3,130180,3 +1030,3,130190,3 +1031,3,130200,3 +1032,3,130420,3 +1033,3,130510,3 +1034,3,130520,3 +1035,3,130530,3 +1036,3,130540,3 +1037,4,140000,3 +1039,1,107000010,1 +1040,2,107000030,1 +1041,2,107000040,1 +1042,2,101000020,1 +1043,3,107000060,1 +1044,3,107000070,1 +1045,3,107000080,1 +1046,3,108000050,1 +1047,1,105000000,2 +1048,2,105000001,2 +1049,3,105000002,2 +1050,4,105000003,2 +1051,5,105000004,2 +1052,1,120000000,2 +1053,2,120000001,2 +1054,3,120000002,2 +1055,4,120000003,2 +1056,5,120000004,2 +1057,1,120020,3 +1058,1,120030,3 +1059,1,120040,3 +1060,1,120050,3 +1061,2,120021,3 +1062,2,120031,3 +1063,2,120041,3 +1064,2,120051,3 +1065,3,120240,3 +1066,3,120250,3 +1067,3,120260,3 +1068,3,120270,3 +1069,3,120300,3 +1070,3,120310,3 +1071,3,120320,3 +1072,3,120330,3 +1073,3,120340,3 +1074,3,120350,3 +1075,3,120360,3 +1076,3,120370,3 +1077,3,120380,3 +1078,3,120390,3 +1079,3,120400,3 +1080,3,120410,3 +1081,3,120420,3 +1082,3,120430,3 +1083,3,120450,3 +1084,3,120460,3 +1085,4,140000,3 +1087,1,108000010,1 +1088,2,108000030,1 +1089,2,108000040,1 +1090,2,106000020,1 +1091,3,108000060,1 +1092,3,108000070,1 +1093,3,108000080,1 +1094,3,107000050,1 +1095,1,108000000,2 +1096,2,108000001,2 +1097,3,108000002,2 +1098,4,108000003,2 +1099,5,108000004,2 +1100,1,120000000,2 +1101,2,120000001,2 +1102,3,120000002,2 +1103,4,120000003,2 +1104,5,120000004,2 +1105,3,170001,3 +1106,4,170002,3 +1108,1,101000010,1 +1109,1,103000010,1 +1110,2,103000030,1 +1111,2,103000040,1 +1112,2,107000020,1 +1113,2,101000030,1 +1114,3,101000050,1 +1115,3,101000060,1 +1116,3,101000080,1 +1117,3,103000060,1 +1118,3,103000070,1 +1119,3,103000080,1 +1120,3,101000040,1 +1121,1,101000000,2 +1122,2,101000001,2 +1123,3,101000002,2 +1124,4,101000008,2 +1125,5,101000004,2 +1126,1,112000000,2 +1127,2,112000001,2 +1128,3,112000002,2 +1129,4,112000003,2 +1130,5,112000004,2 +1131,2,101000005,2 +1132,2,112000005,2 +1133,3,170001,3 +1134,4,170002,3 +1136,1,102000010,1 +1137,2,102000030,1 +1138,2,102000040,1 +1139,2,104000020,1 +1140,3,102000060,1 +1141,3,102000070,1 +1142,3,102000080,1 +1143,3,103000050,1 +1144,3,105000050,1 +1145,1,102000000,2 +1146,2,102000001,2 +1147,3,102000002,2 +1148,4,102000003,2 +1149,5,102000004,2 +1150,1,112000000,2 +1151,2,112000001,2 +1152,3,112000002,2 +1153,4,112000003,2 +1154,5,112000004,2 +1155,1,120020,3 +1156,1,120030,3 +1157,1,120040,3 +1158,1,120050,3 +1159,2,120021,3 +1160,2,120031,3 +1161,2,120041,3 +1162,2,120051,3 +1163,3,120240,3 +1164,3,120250,3 +1165,3,120260,3 +1166,3,120270,3 +1167,3,120300,3 +1168,3,120310,3 +1169,3,120320,3 +1170,3,120330,3 +1171,3,120340,3 +1172,3,120350,3 +1173,3,120360,3 +1174,3,120370,3 +1175,3,120380,3 +1176,3,120390,3 +1177,3,120400,3 +1178,3,120410,3 +1179,3,120420,3 +1180,3,120430,3 +1181,3,120450,3 +1182,3,120460,3 +1183,4,140000,3 +1185,2,104000030,1 +1186,2,104000040,1 +1187,2,108000020,1 +1188,3,104000060,1 +1189,3,104000070,1 +1190,3,104000080,1 +1191,3,102000050,1 +1192,4,104000100,1 +1193,4,104000110,1 +1194,4,107000090,1 +1195,1,111000000,2 +1196,2,111000001,2 +1197,3,111000002,2 +1198,4,111000003,2 +1199,5,111000004,2 +1200,1,120000000,2 +1201,2,120000001,2 +1202,3,120000002,2 +1203,4,120000003,2 +1204,5,120000004,2 +1205,3,170002,3 +1206,4,170003,3 +1208,2,104000030,1 +1209,2,104000040,1 +1210,2,108000020,1 +1211,3,104000060,1 +1212,3,104000070,1 +1213,3,104000080,1 +1214,3,102000050,1 +1215,4,104000100,1 +1216,4,104000110,1 +1217,4,107000090,1 +1218,1,111000000,2 +1219,2,111000001,2 +1220,3,111000002,2 +1221,4,111000003,2 +1222,5,111000004,2 +1223,1,120000000,2 +1224,2,120000001,2 +1225,3,120000002,2 +1226,4,120000003,2 +1227,5,120000004,2 +1228,1,110020,3 +1229,1,110030,3 +1230,1,110040,3 +1231,1,110050,3 +1232,2,110021,3 +1233,2,110031,3 +1234,2,110041,3 +1235,2,110051,3 +1236,3,110022,3 +1237,3,110032,3 +1238,3,110042,3 +1239,3,110052,3 +1240,3,110060,3 +1241,3,110070,3 +1242,3,110080,3 +1243,3,110090,3 +1244,3,110100,3 +1245,3,110110,3 +1246,3,110120,3 +1247,3,110130,3 +1248,3,110140,3 +1249,3,110150,3 +1250,3,110160,3 +1251,3,110170,3 +1252,3,110180,3 +1253,3,110190,3 +1254,3,110200,3 +1255,3,110210,3 +1256,3,110220,3 +1257,3,110230,3 +1258,3,110240,3 +1259,3,110250,3 +1260,3,110260,3 +1261,3,110270,3 +1262,4,140000,3 +1264,2,105000030,1 +1265,2,105000040,1 +1266,2,102000020,1 +1267,2,103000020,1 +1268,3,105000060,1 +1269,3,105000070,1 +1270,3,105000080,1 +1271,3,104000050,1 +1272,3,106000050,1 +1273,4,105000100,1 +1274,4,105000110,1 +1275,4,108000090,1 +1276,1,109000000,2 +1277,2,109000001,2 +1278,3,109000002,2 +1279,4,109000003,2 +1280,5,109000004,2 +1281,1,112000000,2 +1282,2,112000001,2 +1283,3,112000002,2 +1284,4,112000003,2 +1285,5,112000004,2 +1286,3,170002,3 +1287,4,170003,3 +1289,2,106000030,1 +1290,2,106000040,1 +1291,2,105000020,1 +1292,3,106000060,1 +1293,3,106000070,1 +1294,3,106000080,1 +1295,3,101000070,1 +1296,4,106000100,1 +1297,4,106000110,1 +1298,4,104000090,1 +1299,1,103000000,2 +1300,2,103000001,2 +1301,3,103000002,2 +1302,4,103000003,2 +1303,5,103000004,2 +1304,1,112000000,2 +1305,2,112000001,2 +1306,3,112000002,2 +1307,4,112000003,2 +1308,5,112000004,2 +1309,1,130020,3 +1310,1,130030,3 +1311,1,130040,3 +1312,1,130050,3 +1313,2,130021,3 +1314,2,130031,3 +1315,2,130041,3 +1316,2,130051,3 +1317,3,130022,3 +1318,3,130032,3 +1319,3,130042,3 +1320,3,130052,3 +1321,3,130060,3 +1322,3,130070,3 +1323,3,130080,3 +1324,3,130090,3 +1325,3,130100,3 +1326,3,130110,3 +1327,3,130120,3 +1328,3,130130,3 +1329,3,130140,3 +1330,3,130150,3 +1331,3,130160,3 +1332,3,130170,3 +1333,3,130180,3 +1334,3,130190,3 +1335,3,130200,3 +1336,3,130420,3 +1337,3,130510,3 +1338,3,130520,3 +1339,3,130530,3 +1340,3,130540,3 +1341,4,140000,3 +1343,2,108000030,1 +1344,2,108000040,1 +1345,2,106000020,1 +1346,3,108000060,1 +1347,3,108000070,1 +1348,3,108000080,1 +1349,3,107000050,1 +1350,4,108000100,1 +1351,4,105000090,1 +1352,1,108000000,2 +1353,2,108000001,2 +1354,3,108000002,2 +1355,4,108000003,2 +1356,5,108000004,2 +1357,1,120000000,2 +1358,2,120000001,2 +1359,3,120000002,2 +1360,4,120000003,2 +1361,5,120000004,2 +1362,1,120020,3 +1363,1,120030,3 +1364,1,120040,3 +1365,1,120050,3 +1366,2,120021,3 +1367,2,120031,3 +1368,2,120041,3 +1369,2,120051,3 +1370,3,120022,3 +1371,3,120032,3 +1372,3,120042,3 +1373,3,120052,3 +1374,3,120240,3 +1375,3,120250,3 +1376,3,120260,3 +1377,3,120270,3 +1378,3,120300,3 +1379,3,120310,3 +1380,3,120320,3 +1381,3,120330,3 +1382,3,120340,3 +1383,3,120350,3 +1384,3,120360,3 +1385,3,120370,3 +1386,3,120380,3 +1387,3,120390,3 +1388,3,120400,3 +1389,3,120410,3 +1390,3,120420,3 +1391,3,120430,3 +1392,3,120450,3 +1393,3,120460,3 +1394,4,140000,3 +1396,2,103000030,1 +1397,2,103000040,1 +1398,2,107000020,1 +1399,2,101000030,1 +1400,3,101000050,1 +1401,3,101000060,1 +1402,3,101000080,1 +1403,3,103000060,1 +1404,3,103000070,1 +1405,3,103000080,1 +1406,3,101000040,1 +1407,4,101000090,1 +1408,4,102000090,1 +1409,4,103000100,1 +1410,4,101000100,1 +1411,4,101000110,1 +1412,1,101000000,2 +1413,2,101000001,2 +1414,3,101000002,2 +1415,4,101000008,2 +1416,5,101000004,2 +1417,1,120000000,2 +1418,2,120000001,2 +1419,3,120000002,2 +1420,4,120000003,2 +1421,5,120000004,2 +1422,3,170002,3 +1423,4,170003,3 +1425,2,105000030,1 +1426,2,105000040,1 +1427,2,102000020,1 +1428,2,103000020,1 +1429,3,105000060,1 +1430,3,105000070,1 +1431,3,105000080,1 +1432,3,104000050,1 +1433,3,106000050,1 +1434,4,105000100,1 +1435,4,105000110,1 +1436,4,108000090,1 +1437,1,109000000,2 +1438,2,109000001,2 +1439,3,109000002,2 +1440,4,109000003,2 +1441,5,109000004,2 +1442,1,112000000,2 +1443,2,112000001,2 +1444,3,112000002,2 +1445,4,112000003,2 +1446,5,112000004,2 +1447,3,180002,3 +1448,4,180003,3 +1450,2,107000030,1 +1451,2,107000040,1 +1452,2,101000020,1 +1453,3,107000060,1 +1454,3,107000070,1 +1455,3,107000080,1 +1456,3,108000050,1 +1457,4,107000100,1 +1458,4,103000090,1 +1459,1,105000000,2 +1460,2,105000001,2 +1461,3,105000002,2 +1462,4,105000003,2 +1463,5,105000004,2 +1464,1,120000000,2 +1465,2,120000001,2 +1466,3,120000002,2 +1467,4,120000003,2 +1468,5,120000004,2 +1469,3,170002,3 +1470,4,170003,3 +1472,2,104000030,1 +1473,2,104000040,1 +1474,2,108000020,1 +1475,3,104000060,1 +1476,3,104000070,1 +1477,3,104000080,1 +1478,3,102000050,1 +1479,4,104000100,1 +1480,4,104000110,1 +1481,4,107000090,1 +1482,1,111000000,2 +1483,2,111000001,2 +1484,3,111000002,2 +1485,4,111000003,2 +1486,5,111000004,2 +1487,1,120000000,2 +1488,2,120000001,2 +1489,3,120000002,2 +1490,4,120000003,2 +1491,5,120000004,2 +1492,1,110020,3 +1493,1,110030,3 +1494,1,110040,3 +1495,1,110050,3 +1496,2,110021,3 +1497,2,110031,3 +1498,2,110041,3 +1499,2,110051,3 +1500,3,110022,3 +1501,3,110032,3 +1502,3,110042,3 +1503,3,110052,3 +1504,3,110060,3 +1505,3,110070,3 +1506,3,110080,3 +1507,3,110090,3 +1508,3,110100,3 +1509,3,110110,3 +1510,3,110120,3 +1511,3,110130,3 +1512,3,110140,3 +1513,3,110150,3 +1514,3,110160,3 +1515,3,110170,3 +1516,3,110180,3 +1517,3,110190,3 +1518,3,110200,3 +1519,3,110210,3 +1520,3,110220,3 +1521,3,110230,3 +1522,3,110240,3 +1523,3,110250,3 +1524,3,110260,3 +1525,3,110270,3 +1526,4,140000,3 +1528,2,108000030,1 +1529,2,108000040,1 +1530,2,106000020,1 +1531,3,108000060,1 +1532,3,108000070,1 +1533,3,108000080,1 +1534,3,107000050,1 +1535,4,108000100,1 +1536,4,105000090,1 +1537,1,108000000,2 +1538,2,108000001,2 +1539,3,108000002,2 +1540,4,108000003,2 +1541,5,108000004,2 +1542,1,120000000,2 +1543,2,120000001,2 +1544,3,120000002,2 +1545,4,120000003,2 +1546,5,120000004,2 +1547,3,170002,3 +1548,4,170003,3 +1550,2,103000030,1 +1551,2,103000040,1 +1552,2,107000020,1 +1553,2,101000030,1 +1554,3,101000050,1 +1555,3,101000060,1 +1556,3,101000080,1 +1557,3,103000060,1 +1558,3,103000070,1 +1559,3,103000080,1 +1560,3,101000040,1 +1561,4,101000090,1 +1562,4,102000090,1 +1563,4,103000100,1 +1564,4,101000100,1 +1565,4,101000110,1 +1566,1,101000000,2 +1567,2,101000001,2 +1568,3,101000002,2 +1569,4,101000008,2 +1570,5,101000004,2 +1571,1,112000000,2 +1572,2,112000001,2 +1573,3,112000002,2 +1574,4,112000003,2 +1575,5,112000004,2 +1576,1,120020,3 +1577,1,120030,3 +1578,1,120040,3 +1579,1,120050,3 +1580,2,120021,3 +1581,2,120031,3 +1582,2,120041,3 +1583,2,120051,3 +1584,3,120022,3 +1585,3,120032,3 +1586,3,120042,3 +1587,3,120052,3 +1588,4,120023,3 +1589,4,120033,3 +1590,4,120043,3 +1591,4,120053,3 +1592,3,120240,3 +1593,3,120250,3 +1594,3,120260,3 +1595,3,120270,3 +1596,3,120300,3 +1597,3,120310,3 +1598,3,120320,3 +1599,3,120330,3 +1600,3,120340,3 +1601,3,120350,3 +1602,3,120360,3 +1603,3,120370,3 +1604,3,120380,3 +1605,3,120390,3 +1606,3,120400,3 +1607,3,120410,3 +1608,3,120420,3 +1609,3,120430,3 +1610,3,120450,3 +1611,3,120460,3 +1612,4,140000,3 +1613,4,150010,3 +1614,4,150020,3 +1615,4,150030,3 +1616,4,150040,3 +1618,2,102000030,1 +1619,2,102000040,1 +1620,2,104000020,1 +1621,3,102000060,1 +1622,3,102000070,1 +1623,3,102000080,1 +1624,3,103000050,1 +1625,3,105000050,1 +1626,4,102000100,1 +1627,4,102000110,1 +1628,4,106000090,1 +1629,1,102000000,2 +1630,2,102000001,2 +1631,3,102000002,2 +1632,4,102000003,2 +1633,5,102000004,2 +1634,1,112000000,2 +1635,2,112000001,2 +1636,3,112000002,2 +1637,4,112000003,2 +1638,5,112000004,2 +1639,1,110020,3 +1640,1,110030,3 +1641,1,110040,3 +1642,1,110050,3 +1643,2,110021,3 +1644,2,110031,3 +1645,2,110041,3 +1646,2,110051,3 +1647,3,110022,3 +1648,3,110032,3 +1649,3,110042,3 +1650,3,110052,3 +1651,4,110023,3 +1652,4,110033,3 +1653,4,110043,3 +1654,4,110053,3 +1655,3,110060,3 +1656,3,110070,3 +1657,3,110080,3 +1658,3,110090,3 +1659,3,110100,3 +1660,3,110110,3 +1661,3,110120,3 +1662,3,110130,3 +1663,3,110140,3 +1664,3,110150,3 +1665,3,110160,3 +1666,3,110170,3 +1667,3,110180,3 +1668,3,110190,3 +1669,3,110200,3 +1670,3,110210,3 +1671,3,110220,3 +1672,3,110230,3 +1673,3,110240,3 +1674,3,110250,3 +1675,3,110260,3 +1676,3,110270,3 +1677,4,140000,3 +1678,4,150010,3 +1679,4,150020,3 +1680,4,150030,3 +1681,4,150040,3 +1683,2,106000030,1 +1684,2,106000040,1 +1685,2,105000020,1 +1686,3,106000060,1 +1687,3,106000070,1 +1688,3,106000080,1 +1689,3,101000070,1 +1690,4,106000100,1 +1691,4,106000110,1 +1692,4,104000090,1 +1693,1,103000000,2 +1694,2,103000001,2 +1695,3,103000002,2 +1696,4,103000003,2 +1697,5,103000004,2 +1698,1,112000000,2 +1699,2,112000001,2 +1700,3,112000002,2 +1701,4,112000003,2 +1702,5,112000004,2 +1703,1,120020,3 +1704,1,120030,3 +1705,1,120040,3 +1706,1,120050,3 +1707,2,120021,3 +1708,2,120031,3 +1709,2,120041,3 +1710,2,120051,3 +1711,3,120022,3 +1712,3,120032,3 +1713,3,120042,3 +1714,3,120052,3 +1715,4,120023,3 +1716,4,120033,3 +1717,4,120043,3 +1718,4,120053,3 +1719,3,120240,3 +1720,3,120250,3 +1721,3,120260,3 +1722,3,120270,3 +1723,3,120300,3 +1724,3,120310,3 +1725,3,120320,3 +1726,3,120330,3 +1727,3,120340,3 +1728,3,120350,3 +1729,3,120360,3 +1730,3,120370,3 +1731,3,120380,3 +1732,3,120390,3 +1733,3,120400,3 +1734,3,120410,3 +1735,3,120420,3 +1736,3,120430,3 +1737,3,120450,3 +1738,3,120460,3 +1739,4,140000,3 +1740,4,150010,3 +1741,4,150020,3 +1742,4,150030,3 +1743,4,150040,3 +1745,2,103000030,1 +1746,2,103000040,1 +1747,2,107000020,1 +1748,2,101000030,1 +1749,3,101000050,1 +1750,3,101000060,1 +1751,3,101000080,1 +1752,3,103000060,1 +1753,3,103000070,1 +1754,3,103000080,1 +1755,3,101000040,1 +1756,4,101000090,1 +1757,4,102000090,1 +1758,4,103000100,1 +1759,4,101000100,1 +1760,4,101000110,1 +1761,1,101000000,2 +1762,2,101000001,2 +1763,3,101000002,2 +1764,4,101000008,2 +1765,5,101000004,2 +1766,1,120000000,2 +1767,2,120000001,2 +1768,3,120000002,2 +1769,4,120000003,2 +1770,5,120000004,2 +1771,3,170003,3 +1772,4,170004,3 +1774,2,107000030,1 +1775,2,107000040,1 +1776,2,101000020,1 +1777,3,107000060,1 +1778,3,107000070,1 +1779,3,107000080,1 +1780,3,108000050,1 +1781,4,107000100,1 +1782,4,103000090,1 +1783,1,105000000,2 +1784,2,105000001,2 +1785,3,105000002,2 +1786,4,105000003,2 +1787,5,105000004,2 +1788,1,120000000,2 +1789,2,120000001,2 +1790,3,120000002,2 +1791,4,120000003,2 +1792,5,120000004,2 +1793,1,130020,3 +1794,1,130030,3 +1795,1,130040,3 +1796,1,130050,3 +1797,2,130021,3 +1798,2,130031,3 +1799,2,130041,3 +1800,2,130051,3 +1801,3,130022,3 +1802,3,130032,3 +1803,3,130042,3 +1804,3,130052,3 +1805,4,130023,3 +1806,4,130033,3 +1807,4,130043,3 +1808,4,130053,3 +1809,3,130060,3 +1810,3,130070,3 +1811,3,130080,3 +1812,3,130090,3 +1813,3,130100,3 +1814,3,130110,3 +1815,3,130120,3 +1816,3,130130,3 +1817,3,130140,3 +1818,3,130150,3 +1819,3,130160,3 +1820,3,130170,3 +1821,3,130180,3 +1822,3,130190,3 +1823,3,130200,3 +1824,3,130420,3 +1825,3,130510,3 +1826,3,130520,3 +1827,3,130530,3 +1828,3,130540,3 +1829,4,140000,3 +1830,4,150010,3 +1831,4,150020,3 +1832,4,150030,3 +1833,4,150040,3 +1835,2,102000030,1 +1836,2,102000040,1 +1837,2,104000020,1 +1838,3,102000060,1 +1839,3,102000070,1 +1840,3,102000080,1 +1841,3,103000050,1 +1842,3,105000050,1 +1843,4,102000100,1 +1844,4,102000110,1 +1845,4,106000090,1 +1846,1,102000000,2 +1847,2,102000001,2 +1848,3,102000002,2 +1849,4,102000003,2 +1850,5,102000004,2 +1851,1,112000000,2 +1852,2,112000001,2 +1853,3,112000002,2 +1854,4,112000003,2 +1855,5,112000004,2 +1856,1,120020,3 +1857,1,120030,3 +1858,1,120040,3 +1859,1,120050,3 +1860,2,120021,3 +1861,2,120031,3 +1862,2,120041,3 +1863,2,120051,3 +1864,3,120022,3 +1865,3,120032,3 +1866,3,120042,3 +1867,3,120052,3 +1868,4,120023,3 +1869,4,120033,3 +1870,4,120043,3 +1871,4,120053,3 +1872,3,120240,3 +1873,3,120250,3 +1874,3,120260,3 +1875,3,120270,3 +1876,3,120300,3 +1877,3,120310,3 +1878,3,120320,3 +1879,3,120330,3 +1880,3,120340,3 +1881,3,120350,3 +1882,3,120360,3 +1883,3,120370,3 +1884,3,120380,3 +1885,3,120390,3 +1886,3,120400,3 +1887,3,120410,3 +1888,3,120420,3 +1889,3,120430,3 +1890,3,120450,3 +1891,3,120460,3 +1892,4,140000,3 +1893,4,150010,3 +1894,4,150020,3 +1895,4,150030,3 +1896,4,150040,3 +1898,2,108000030,1 +1899,2,108000040,1 +1900,2,106000020,1 +1901,3,108000060,1 +1902,3,108000070,1 +1903,3,108000080,1 +1904,3,107000050,1 +1905,4,108000100,1 +1906,4,105000090,1 +1907,1,108000000,2 +1908,2,108000001,2 +1909,3,108000002,2 +1910,4,108000003,2 +1911,5,108000004,2 +1912,1,120000000,2 +1913,2,120000001,2 +1914,3,120000002,2 +1915,4,120000003,2 +1916,5,120000004,2 +1917,3,170003,3 +1918,4,170004,3 +1920,2,103000030,1 +1921,2,103000040,1 +1922,2,107000020,1 +1923,2,101000030,1 +1924,3,101000050,1 +1925,3,101000060,1 +1926,3,101000080,1 +1927,3,103000060,1 +1928,3,103000070,1 +1929,3,103000080,1 +1930,3,101000040,1 +1931,4,101000090,1 +1932,4,102000090,1 +1933,4,103000100,1 +1934,4,101000100,1 +1935,4,101000110,1 +1936,1,101000000,2 +1937,2,101000001,2 +1938,3,101000002,2 +1939,4,101000008,2 +1940,5,101000004,2 +1941,1,112000000,2 +1942,2,112000001,2 +1943,3,112000002,2 +1944,4,112000003,2 +1945,5,112000004,2 +1946,3,170003,3 +1947,4,170004,3 +1949,2,105000030,1 +1950,2,105000040,1 +1951,2,102000020,1 +1952,2,103000020,1 +1953,3,105000060,1 +1954,3,105000070,1 +1955,3,105000080,1 +1956,3,104000050,1 +1957,3,106000050,1 +1958,4,105000100,1 +1959,4,105000110,1 +1960,4,108000090,1 +1961,1,109000000,2 +1962,2,109000001,2 +1963,3,109000002,2 +1964,4,109000003,2 +1965,5,109000004,2 +1966,1,112000000,2 +1967,2,112000001,2 +1968,3,112000002,2 +1969,4,112000003,2 +1970,5,112000004,2 +1971,3,180003,3 +1972,4,180004,3 +1974,2,104000030,1 +1975,2,104000040,1 +1976,2,108000020,1 +1977,3,104000060,1 +1978,3,104000070,1 +1979,3,104000080,1 +1980,3,102000050,1 +1981,4,104000100,1 +1982,4,104000110,1 +1983,4,107000090,1 +1984,1,111000000,2 +1985,2,111000001,2 +1986,3,111000002,2 +1987,4,111000003,2 +1988,5,111000004,2 +1989,1,120000000,2 +1990,2,120000001,2 +1991,3,120000002,2 +1992,4,120000003,2 +1993,5,120000004,2 +1994,1,110020,3 +1995,1,110030,3 +1996,1,110040,3 +1997,1,110050,3 +1998,2,110021,3 +1999,2,110031,3 +2000,2,110041,3 +2001,2,110051,3 +2002,3,110022,3 +2003,3,110032,3 +2004,3,110042,3 +2005,3,110052,3 +2006,4,110023,3 +2007,4,110033,3 +2008,4,110043,3 +2009,4,110053,3 +2010,3,110060,3 +2011,3,110070,3 +2012,3,110080,3 +2013,3,110090,3 +2014,3,110100,3 +2015,3,110110,3 +2016,3,110120,3 +2017,3,110130,3 +2018,3,110140,3 +2019,3,110150,3 +2020,3,110160,3 +2021,3,110170,3 +2022,3,110180,3 +2023,3,110190,3 +2024,3,110200,3 +2025,3,110210,3 +2026,3,110220,3 +2027,3,110230,3 +2028,3,110240,3 +2029,3,110250,3 +2030,3,110260,3 +2031,3,110270,3 +2032,4,140000,3 +2033,4,150010,3 +2034,4,150020,3 +2035,4,150030,3 +2036,4,150040,3 +2038,3,105000060,1 +2039,3,105000070,1 +2040,3,105000080,1 +2041,3,104000050,1 +2042,3,106000050,1 +2043,4,105000100,1 +2044,4,105000110,1 +2045,4,108000090,1 +2046,5,105000120,1 +2047,1,109000000,2 +2048,2,109000001,2 +2049,3,109000002,2 +2050,4,109000003,2 +2051,5,109000004,2 +2052,1,112000000,2 +2053,2,112000001,2 +2054,3,112000002,2 +2055,4,112000003,2 +2056,5,112000004,2 +2057,3,170004,3 +2059,3,101000050,1 +2060,3,101000060,1 +2061,3,101000080,1 +2062,3,103000060,1 +2063,3,103000070,1 +2064,3,103000080,1 +2065,3,101000040,1 +2066,4,101000090,1 +2067,4,102000090,1 +2068,4,103000100,1 +2069,4,101000100,1 +2070,4,101000110,1 +2071,5,101000120,1 +2072,5,103000120,1 +2073,1,101000000,2 +2074,2,101000001,2 +2075,3,101000002,2 +2076,4,101000008,2 +2077,5,101000004,2 +2078,1,120000000,2 +2079,2,120000001,2 +2080,3,120000002,2 +2081,4,120000003,2 +2082,5,120000004,2 +2083,3,170004,3 +2085,3,107000060,1 +2086,3,107000070,1 +2087,3,107000080,1 +2088,3,108000050,1 +2089,4,107000100,1 +2090,4,103000090,1 +2091,5,107000110,1 +2092,1,105000000,2 +2093,2,105000001,2 +2094,3,105000002,2 +2095,4,105000003,2 +2096,5,105000004,2 +2097,1,120000000,2 +2098,2,120000001,2 +2099,3,120000002,2 +2100,4,120000003,2 +2101,5,120000004,2 +2102,3,170004,3 +2104,3,101000050,1 +2105,3,101000060,1 +2106,3,101000080,1 +2107,3,103000060,1 +2108,3,103000070,1 +2109,3,103000080,1 +2110,3,101000040,1 +2111,4,101000090,1 +2112,4,102000090,1 +2113,4,103000100,1 +2114,4,101000100,1 +2115,4,101000110,1 +2116,5,101000120,1 +2117,5,103000120,1 +2118,1,101000000,2 +2119,2,101000001,2 +2120,3,101000002,2 +2121,4,101000008,2 +2122,5,101000004,2 +2123,1,112000000,2 +2124,2,112000001,2 +2125,3,112000002,2 +2126,4,112000003,2 +2127,5,112000004,2 +2128,1,130020,3 +2129,1,130030,3 +2130,1,130040,3 +2131,1,130050,3 +2132,2,130021,3 +2133,2,130031,3 +2134,2,130041,3 +2135,2,130051,3 +2136,3,130022,3 +2137,3,130032,3 +2138,3,130042,3 +2139,3,130052,3 +2140,4,130023,3 +2141,4,130033,3 +2142,4,130043,3 +2143,4,130053,3 +2144,5,130024,3 +2145,5,130034,3 +2146,5,130044,3 +2147,5,130054,3 +2148,3,130060,3 +2149,3,130070,3 +2150,3,130080,3 +2151,3,130090,3 +2152,3,130100,3 +2153,3,130110,3 +2154,3,130120,3 +2155,3,130130,3 +2156,3,130140,3 +2157,3,130150,3 +2158,3,130160,3 +2159,3,130170,3 +2160,3,130180,3 +2161,3,130190,3 +2162,3,130200,3 +2163,3,130420,3 +2164,3,130510,3 +2165,3,130520,3 +2166,3,130530,3 +2167,3,130540,3 +2168,4,140000,3 +2169,4,150010,3 +2170,4,150020,3 +2171,4,150030,3 +2172,4,150040,3 +2174,3,102000060,1 +2175,3,102000070,1 +2176,3,102000080,1 +2177,3,103000050,1 +2178,3,105000050,1 +2179,4,102000100,1 +2180,4,102000110,1 +2181,4,106000090,1 +2182,5,102000120,1 +2183,1,102000000,2 +2184,2,102000001,2 +2185,3,102000002,2 +2186,4,102000003,2 +2187,5,102000004,2 +2188,1,112000000,2 +2189,2,112000001,2 +2190,3,112000002,2 +2191,4,112000003,2 +2192,5,112000004,2 +2193,3,170004,3 +2195,3,101000050,1 +2196,3,101000060,1 +2197,3,101000080,1 +2198,3,103000060,1 +2199,3,103000070,1 +2200,3,103000080,1 +2201,3,101000040,1 +2202,4,101000090,1 +2203,4,102000090,1 +2204,4,103000100,1 +2205,4,101000100,1 +2206,4,101000110,1 +2207,5,101000120,1 +2208,5,103000120,1 +2209,1,101000000,2 +2210,2,101000001,2 +2211,3,101000002,2 +2212,4,101000008,2 +2213,5,101000004,2 +2214,1,120000000,2 +2215,2,120000001,2 +2216,3,120000002,2 +2217,4,120000003,2 +2218,5,120000004,2 +2219,3,170004,3 +2221,3,106000060,1 +2222,3,106000070,1 +2223,3,106000080,1 +2224,3,101000070,1 +2225,4,106000100,1 +2226,4,106000110,1 +2227,4,104000090,1 +2228,5,106000120,1 +2229,1,103000000,2 +2230,2,103000001,2 +2231,3,103000002,2 +2232,4,103000003,2 +2233,5,103000004,2 +2234,1,112000000,2 +2235,2,112000001,2 +2236,3,112000002,2 +2237,4,112000003,2 +2238,5,112000004,2 +2239,1,130020,3 +2240,1,130030,3 +2241,1,130040,3 +2242,1,130050,3 +2243,2,130021,3 +2244,2,130031,3 +2245,2,130041,3 +2246,2,130051,3 +2247,3,130022,3 +2248,3,130032,3 +2249,3,130042,3 +2250,3,130052,3 +2251,4,130023,3 +2252,4,130033,3 +2253,4,130043,3 +2254,4,130053,3 +2255,5,130024,3 +2256,5,130034,3 +2257,5,130044,3 +2258,5,130054,3 +2259,3,130060,3 +2260,3,130070,3 +2261,3,130080,3 +2262,3,130090,3 +2263,3,130100,3 +2264,3,130110,3 +2265,3,130120,3 +2266,3,130130,3 +2267,3,130140,3 +2268,3,130150,3 +2269,3,130160,3 +2270,3,130170,3 +2271,3,130180,3 +2272,3,130190,3 +2273,3,130200,3 +2274,3,130420,3 +2275,3,130510,3 +2276,3,130520,3 +2277,3,130530,3 +2278,3,130540,3 +2279,4,140000,3 +2280,4,150010,3 +2281,4,150020,3 +2282,4,150030,3 +2283,4,150040,3 +2285,3,108000060,1 +2286,3,108000070,1 +2287,3,108000080,1 +2288,3,107000050,1 +2289,4,108000100,1 +2290,4,105000090,1 +2291,5,108000110,1 +2292,1,108000000,2 +2293,2,108000001,2 +2294,3,108000002,2 +2295,4,108000003,2 +2296,5,108000004,2 +2297,1,120000000,2 +2298,2,120000001,2 +2299,3,120000002,2 +2300,4,120000003,2 +2301,5,120000004,2 +2302,1,120020,3 +2303,1,120030,3 +2304,1,120040,3 +2305,1,120050,3 +2306,2,120021,3 +2307,2,120031,3 +2308,2,120041,3 +2309,2,120051,3 +2310,3,120022,3 +2311,3,120032,3 +2312,3,120042,3 +2313,3,120052,3 +2314,4,120023,3 +2315,4,120033,3 +2316,4,120043,3 +2317,4,120053,3 +2318,5,120024,3 +2319,5,120034,3 +2320,5,120044,3 +2321,5,120054,3 +2322,3,120240,3 +2323,3,120250,3 +2324,3,120260,3 +2325,3,120270,3 +2326,3,120300,3 +2327,3,120310,3 +2328,3,120320,3 +2329,3,120330,3 +2330,3,120340,3 +2331,3,120350,3 +2332,3,120360,3 +2333,3,120370,3 +2334,3,120380,3 +2335,3,120390,3 +2336,3,120400,3 +2337,3,120410,3 +2338,3,120420,3 +2339,3,120430,3 +2340,3,120450,3 +2341,3,120460,3 +2342,4,140000,3 +2343,4,150010,3 +2344,4,150020,3 +2345,4,150030,3 +2346,4,150040,3 +2348,3,104000060,1 +2349,3,104000070,1 +2350,3,104000080,1 +2351,3,102000050,1 +2352,4,104000100,1 +2353,4,104000110,1 +2354,4,107000090,1 +2355,5,104000120,1 +2356,1,111000000,2 +2357,2,111000001,2 +2358,3,111000002,2 +2359,4,111000003,2 +2360,5,111000004,2 +2361,1,120000000,2 +2362,2,120000001,2 +2363,3,120000002,2 +2364,4,120000003,2 +2365,5,120000004,2 +2366,1,110020,3 +2367,1,110030,3 +2368,1,110040,3 +2369,1,110050,3 +2370,2,110021,3 +2371,2,110031,3 +2372,2,110041,3 +2373,2,110051,3 +2374,3,110022,3 +2375,3,110032,3 +2376,3,110042,3 +2377,3,110052,3 +2378,4,110023,3 +2379,4,110033,3 +2380,4,110043,3 +2381,4,110053,3 +2382,5,110024,3 +2383,5,110034,3 +2384,5,110044,3 +2385,5,110054,3 +2386,3,110060,3 +2387,3,110070,3 +2388,3,110080,3 +2389,3,110090,3 +2390,3,110100,3 +2391,3,110110,3 +2392,3,110120,3 +2393,3,110130,3 +2394,3,110140,3 +2395,3,110150,3 +2396,3,110160,3 +2397,3,110170,3 +2398,3,110180,3 +2399,3,110190,3 +2400,3,110200,3 +2401,3,110210,3 +2402,3,110220,3 +2403,3,110230,3 +2404,3,110240,3 +2405,3,110250,3 +2406,3,110260,3 +2407,3,110270,3 +2408,4,140000,3 +2409,4,150010,3 +2410,4,150020,3 +2411,4,150030,3 +2412,4,150040,3 +2414,3,105000060,1 +2415,3,105000070,1 +2416,3,105000080,1 +2417,3,104000050,1 +2418,3,106000050,1 +2419,4,105000100,1 +2420,4,105000110,1 +2421,4,108000090,1 +2422,5,105000120,1 +2423,1,109000000,2 +2424,2,109000001,2 +2425,3,109000002,2 +2426,4,109000003,2 +2427,5,109000004,2 +2428,1,112000000,2 +2429,2,112000001,2 +2430,3,112000002,2 +2431,4,112000003,2 +2432,5,112000004,2 +2433,3,170004,3 +2435,3,104000060,1 +2436,3,104000070,1 +2437,3,104000080,1 +2438,3,102000050,1 +2439,4,104000100,1 +2440,4,104000110,1 +2441,4,107000090,1 +2442,5,104000120,1 +2443,1,111000000,2 +2444,2,111000001,2 +2445,3,111000002,2 +2446,4,111000003,2 +2447,5,111000004,2 +2448,1,120000000,2 +2449,2,120000001,2 +2450,3,120000002,2 +2451,4,120000003,2 +2452,5,120000004,2 +2453,1,130020,3 +2454,1,130030,3 +2455,1,130040,3 +2456,1,130050,3 +2457,2,130021,3 +2458,2,130031,3 +2459,2,130041,3 +2460,2,130051,3 +2461,3,130022,3 +2462,3,130032,3 +2463,3,130042,3 +2464,3,130052,3 +2465,4,130023,3 +2466,4,130033,3 +2467,4,130043,3 +2468,4,130053,3 +2469,5,130024,3 +2470,5,130034,3 +2471,5,130044,3 +2472,5,130054,3 +2473,3,130060,3 +2474,3,130070,3 +2475,3,130080,3 +2476,3,130090,3 +2477,3,130100,3 +2478,3,130110,3 +2479,3,130120,3 +2480,3,130130,3 +2481,3,130140,3 +2482,3,130150,3 +2483,3,130160,3 +2484,3,130170,3 +2485,3,130180,3 +2486,3,130190,3 +2487,3,130200,3 +2488,3,130420,3 +2489,3,130510,3 +2490,3,130520,3 +2491,3,130530,3 +2492,3,130540,3 +2493,4,140000,3 +2494,4,150010,3 +2495,4,150020,3 +2496,4,150030,3 +2497,4,150040,3 +2500,1,101000000,2 +2501,2,101000001,2 +2502,3,101000002,2 +2503,4,101000008,2 +2504,5,101000004,2 +2505,1,102000000,2 +2506,2,102000001,2 +2507,3,102000002,2 +2508,4,102000003,2 +2509,5,102000004,2 +2510,1,103000000,2 +2511,2,103000001,2 +2512,3,103000002,2 +2513,4,103000003,2 +2514,5,103000004,2 +2515,1,105000000,2 +2516,2,105000001,2 +2517,3,105000002,2 +2518,4,105000003,2 +2519,5,105000004,2 +2520,1,108000000,2 +2521,2,108000001,2 +2522,3,108000002,2 +2523,4,108000003,2 +2524,5,108000004,2 +2525,1,109000000,2 +2526,2,109000001,2 +2527,3,109000002,2 +2528,4,109000003,2 +2529,5,109000004,2 +2530,1,111000000,2 +2531,2,111000001,2 +2532,3,111000002,2 +2533,4,111000003,2 +2534,5,111000004,2 +2535,1,112000000,2 +2536,2,112000001,2 +2537,3,112000002,2 +2538,4,112000003,2 +2539,5,112000004,2 +2540,1,120000000,2 +2541,2,120000001,2 +2542,3,120000002,2 +2543,4,120000003,2 +2544,5,120000004,2 +2545,1,101000010,1 +2546,2,101000020,1 +2547,2,101000030,1 +2548,3,101000040,1 +2549,3,101000050,1 +2550,3,101000060,1 +2551,3,101000070,1 +2552,3,101000080,1 +2553,1,102000010,1 +2554,2,102000020,1 +2555,2,102000030,1 +2556,2,102000040,1 +2557,3,102000050,1 +2558,3,102000060,1 +2559,3,102000070,1 +2560,3,102000080,1 +2561,1,103000010,1 +2562,2,103000020,1 +2563,2,103000030,1 +2564,2,103000040,1 +2565,3,103000050,1 +2566,3,103000060,1 +2567,3,103000070,1 +2568,3,103000080,1 +2569,1,104000010,1 +2570,2,104000020,1 +2571,2,104000030,1 +2572,2,104000040,1 +2573,3,104000050,1 +2574,3,104000060,1 +2575,3,104000070,1 +2576,3,104000080,1 +2577,1,105000010,1 +2578,2,105000020,1 +2579,2,105000030,1 +2580,2,105000040,1 +2581,3,105000050,1 +2582,3,105000060,1 +2583,3,105000070,1 +2584,3,105000080,1 +2585,1,106000010,1 +2586,2,106000020,1 +2587,2,106000030,1 +2588,2,106000040,1 +2589,3,106000050,1 +2590,3,106000060,1 +2591,3,106000070,1 +2592,3,106000080,1 +2593,1,107000010,1 +2594,2,107000020,1 +2595,2,107000030,1 +2596,2,107000040,1 +2597,3,107000050,1 +2598,3,107000060,1 +2599,3,107000070,1 +2600,3,107000080,1 +2601,1,108000010,1 +2602,2,108000020,1 +2603,2,108000030,1 +2604,2,108000040,1 +2605,3,108000050,1 +2606,3,108000060,1 +2607,3,108000070,1 +2608,3,108000080,1 +2609,2,180001,3 +2611,1,101000000,2 +2612,2,101000001,2 +2613,3,101000002,2 +2614,4,101000008,2 +2615,5,101000004,2 +2616,1,102000000,2 +2617,2,102000001,2 +2618,3,102000002,2 +2619,4,102000003,2 +2620,5,102000004,2 +2621,1,103000000,2 +2622,2,103000001,2 +2623,3,103000002,2 +2624,4,103000003,2 +2625,5,103000004,2 +2626,1,105000000,2 +2627,2,105000001,2 +2628,3,105000002,2 +2629,4,105000003,2 +2630,5,105000004,2 +2631,1,108000000,2 +2632,2,108000001,2 +2633,3,108000002,2 +2634,4,108000003,2 +2635,5,108000004,2 +2636,1,109000000,2 +2637,2,109000001,2 +2638,3,109000002,2 +2639,4,109000003,2 +2640,5,109000004,2 +2641,1,111000000,2 +2642,2,111000001,2 +2643,3,111000002,2 +2644,4,111000003,2 +2645,5,111000004,2 +2646,1,112000000,2 +2647,2,112000001,2 +2648,3,112000002,2 +2649,4,112000003,2 +2650,5,112000004,2 +2651,1,120000000,2 +2652,2,120000001,2 +2653,3,120000002,2 +2654,4,120000003,2 +2655,5,120000004,2 +2656,1,101000010,1 +2657,2,101000020,1 +2658,2,101000030,1 +2659,3,101000040,1 +2660,3,101000050,1 +2661,3,101000060,1 +2662,3,101000070,1 +2663,3,101000080,1 +2664,1,102000010,1 +2665,2,102000020,1 +2666,2,102000030,1 +2667,2,102000040,1 +2668,3,102000050,1 +2669,3,102000060,1 +2670,3,102000070,1 +2671,3,102000080,1 +2672,1,103000010,1 +2673,2,103000020,1 +2674,2,103000030,1 +2675,2,103000040,1 +2676,3,103000050,1 +2677,3,103000060,1 +2678,3,103000070,1 +2679,3,103000080,1 +2680,1,104000010,1 +2681,2,104000020,1 +2682,2,104000030,1 +2683,2,104000040,1 +2684,3,104000050,1 +2685,3,104000060,1 +2686,3,104000070,1 +2687,3,104000080,1 +2688,1,105000010,1 +2689,2,105000020,1 +2690,2,105000030,1 +2691,2,105000040,1 +2692,3,105000050,1 +2693,3,105000060,1 +2694,3,105000070,1 +2695,3,105000080,1 +2696,1,106000010,1 +2697,2,106000020,1 +2698,2,106000030,1 +2699,2,106000040,1 +2700,3,106000050,1 +2701,3,106000060,1 +2702,3,106000070,1 +2703,3,106000080,1 +2704,1,107000010,1 +2705,2,107000020,1 +2706,2,107000030,1 +2707,2,107000040,1 +2708,3,107000050,1 +2709,3,107000060,1 +2710,3,107000070,1 +2711,3,107000080,1 +2712,1,108000010,1 +2713,2,108000020,1 +2714,2,108000030,1 +2715,2,108000040,1 +2716,3,108000050,1 +2717,3,108000060,1 +2718,3,108000070,1 +2719,3,108000080,1 +2720,1,109000010,1 +2721,2,109000020,1 +2722,2,109000030,1 +2723,2,109000040,1 +2724,3,109000050,1 +2725,3,109000060,1 +2726,3,109000070,1 +2727,3,109000080,1 +2728,2,180001,3 +2731,1,101000000,2 +2732,2,101000001,2 +2733,3,101000002,2 +2734,4,101000008,2 +2735,5,101000004,2 +2736,1,102000000,2 +2737,2,102000001,2 +2738,3,102000002,2 +2739,4,102000003,2 +2740,5,102000004,2 +2741,1,103000000,2 +2742,2,103000001,2 +2743,3,103000002,2 +2744,4,103000003,2 +2745,5,103000004,2 +2746,1,105000000,2 +2747,2,105000001,2 +2748,3,105000002,2 +2749,4,105000003,2 +2750,5,105000004,2 +2751,1,107000000,2 +2752,2,107000001,2 +2753,3,107000002,2 +2754,4,107000003,2 +2755,5,107000004,2 +2756,1,108000000,2 +2757,2,108000001,2 +2758,3,108000002,2 +2759,4,108000003,2 +2760,5,108000004,2 +2761,1,109000000,2 +2762,2,109000001,2 +2763,3,109000002,2 +2764,4,109000003,2 +2765,5,109000004,2 +2766,1,111000000,2 +2767,2,111000001,2 +2768,3,111000002,2 +2769,4,111000003,2 +2770,5,111000004,2 +2771,1,112000000,2 +2772,2,112000001,2 +2773,3,112000002,2 +2774,4,112000003,2 +2775,5,112000004,2 +2776,1,120000000,2 +2777,2,120000001,2 +2778,3,120000002,2 +2779,4,120000003,2 +2780,5,120000004,2 +2781,1,101000010,1 +2782,2,101000020,1 +2783,2,101000030,1 +2784,3,101000040,1 +2785,3,101000050,1 +2786,3,101000060,1 +2787,3,101000070,1 +2788,3,101000080,1 +2789,1,102000010,1 +2790,2,102000020,1 +2791,2,102000030,1 +2792,2,102000040,1 +2793,3,102000050,1 +2794,3,102000060,1 +2795,3,102000070,1 +2796,3,102000080,1 +2797,1,103000010,1 +2798,2,103000020,1 +2799,2,103000030,1 +2800,2,103000040,1 +2801,3,103000050,1 +2802,3,103000060,1 +2803,3,103000070,1 +2804,3,103000080,1 +2805,1,104000010,1 +2806,2,104000020,1 +2807,2,104000030,1 +2808,2,104000040,1 +2809,3,104000050,1 +2810,3,104000060,1 +2811,3,104000070,1 +2812,3,104000080,1 +2813,1,105000010,1 +2814,2,105000020,1 +2815,2,105000030,1 +2816,2,105000040,1 +2817,3,105000050,1 +2818,3,105000060,1 +2819,3,105000070,1 +2820,3,105000080,1 +2821,1,106000010,1 +2822,2,106000020,1 +2823,2,106000030,1 +2824,2,106000040,1 +2825,3,106000050,1 +2826,3,106000060,1 +2827,3,106000070,1 +2828,3,106000080,1 +2829,1,107000010,1 +2830,2,107000020,1 +2831,2,107000030,1 +2832,2,107000040,1 +2833,3,107000050,1 +2834,3,107000060,1 +2835,3,107000070,1 +2836,3,107000080,1 +2837,1,108000010,1 +2838,2,108000020,1 +2839,2,108000030,1 +2840,2,108000040,1 +2841,3,108000050,1 +2842,3,108000060,1 +2843,3,108000070,1 +2844,3,108000080,1 +2845,1,109000010,1 +2846,2,109000020,1 +2847,2,109000030,1 +2848,2,109000040,1 +2849,3,109000050,1 +2850,3,109000060,1 +2851,3,109000070,1 +2852,3,109000080,1 +2853,1,110000010,1 +2854,2,110000020,1 +2855,2,110000030,1 +2856,2,110000040,1 +2857,3,110000050,1 +2858,3,110000060,1 +2859,3,110000070,1 +2860,3,110000080,1 +2861,2,180001,3 +2863,1,107000000,2 +2864,2,107000001,2 +2865,3,107000002,2 +2866,4,107000003,2 +2867,5,107000004,2 +2868,1,120000000,2 +2869,2,120000001,2 +2870,3,120000002,2 +2871,4,120000003,2 +2872,5,120000004,2 +2874,3,110000070,1 +2875,3,110000080,1 +2876,4,110000100,1 +2877,5,110000110,1 +2878,1,107000000,2 +2879,2,107000001,2 +2880,3,107000002,2 +2881,4,107000003,2 +2882,5,107000004,2 +2883,1,120000000,2 +2884,2,120000001,2 +2885,3,120000002,2 +2886,4,120000003,2 +2887,5,120000004,2 +2888,3,120023,3 +2889,3,120033,3 +2890,3,120043,3 +2891,3,120053,3 +2892,4,120024,3 +2893,4,120034,3 +2894,4,120044,3 +2895,4,120054,3 +2896,3,120240,3 +2897,3,120250,3 +2898,3,120260,3 +2899,3,120270,3 +2900,3,120300,3 +2901,3,120310,3 +2902,3,120320,3 +2903,3,120330,3 +2904,3,120340,3 +2905,3,120350,3 +2906,3,120360,3 +2907,3,120370,3 +2908,3,120380,3 +2909,3,120390,3 +2910,3,120400,3 +2911,3,120410,3 +2912,3,120420,3 +2913,3,120430,3 +2914,3,120450,3 +2915,3,120460,3 +2916,3,120550,3 +2917,3,120560,3 +2918,3,120570,3 +2919,3,120990,3 +2920,3,121000,3 +2921,3,121010,3 +2922,3,121020,3 +2923,4,140000,3 +2924,4,150010,3 +2925,4,150020,3 +2926,4,150030,3 +2927,4,150040,3 +2929,3,108000060,1 +2930,3,108000070,1 +2931,3,108000080,1 +2932,3,107000050,1 +2933,4,108000100,1 +2934,4,105000090,1 +2935,5,108000110,1 +2936,1,108000000,2 +2937,2,108000001,2 +2938,3,108000002,2 +2939,4,108000003,2 +2940,5,108000004,2 +2941,1,120000000,2 +2942,2,120000001,2 +2943,3,120000002,2 +2944,4,120000003,2 +2945,5,120000004,2 +2946,3,170004,3 +2948,3,102000060,1 +2949,3,102000070,1 +2950,3,102000080,1 +2951,3,103000050,1 +2952,3,105000050,1 +2953,4,102000100,1 +2954,4,102000110,1 +2955,4,106000090,1 +2956,4,109000090,1 +2957,5,102000120,1 +2958,1,102000000,2 +2959,2,102000001,2 +2960,3,102000002,2 +2961,4,102000003,2 +2962,5,102000004,2 +2963,1,112000000,2 +2964,2,112000001,2 +2965,3,112000002,2 +2966,4,112000003,2 +2967,5,112000004,2 +2968,3,170004,3 +2970,3,101000050,1 +2971,3,101000060,1 +2972,3,101000080,1 +2973,3,103000060,1 +2974,3,103000070,1 +2975,3,103000080,1 +2976,3,101000040,1 +2977,3,109000060,1 +2978,3,109000070,1 +2979,3,109000080,1 +2980,3,110000050,1 +2981,4,101000090,1 +2982,4,102000090,1 +2983,4,103000100,1 +2984,4,101000100,1 +2985,4,101000110,1 +2986,4,109000100,1 +2987,5,101000120,1 +2988,5,103000120,1 +2989,5,109000110,1 +2990,1,101000000,2 +2991,2,101000001,2 +2992,3,101000002,2 +2993,4,101000008,2 +2994,5,101000004,2 +2995,1,112000000,2 +2996,2,112000001,2 +2997,3,112000002,2 +2998,4,112000003,2 +2999,5,112000004,2 +3000,3,120023,3 +3001,3,120033,3 +3002,3,120043,3 +3003,3,120053,3 +3004,4,120024,3 +3005,4,120034,3 +3006,4,120044,3 +3007,4,120054,3 +3008,3,120240,3 +3009,3,120250,3 +3010,3,120260,3 +3011,3,120270,3 +3012,3,120300,3 +3013,3,120310,3 +3014,3,120320,3 +3015,3,120330,3 +3016,3,120340,3 +3017,3,120350,3 +3018,3,120360,3 +3019,3,120370,3 +3020,3,120380,3 +3021,3,120390,3 +3022,3,120400,3 +3023,3,120410,3 +3024,3,120420,3 +3025,3,120430,3 +3026,3,120450,3 +3027,3,120460,3 +3028,3,120550,3 +3029,3,120560,3 +3030,3,120570,3 +3031,3,120990,3 +3032,3,121000,3 +3033,3,121010,3 +3034,3,121020,3 +3035,4,140000,3 +3036,4,150010,3 +3037,4,150020,3 +3038,4,150030,3 +3039,4,150040,3 +3041,3,105000060,1 +3042,3,105000070,1 +3043,3,105000080,1 +3044,3,104000050,1 +3045,3,106000050,1 +3046,4,105000100,1 +3047,4,105000110,1 +3048,4,108000090,1 +3049,4,110000090,1 +3050,5,105000120,1 +3051,1,109000000,2 +3052,2,109000001,2 +3053,3,109000002,2 +3054,4,109000003,2 +3055,5,109000004,2 +3056,1,112000000,2 +3057,2,112000001,2 +3058,3,112000002,2 +3059,4,112000003,2 +3060,5,112000004,2 +3061,3,170004,3 +3063,3,107000060,1 +3064,3,107000070,1 +3065,3,107000080,1 +3066,3,108000050,1 +3067,3,109000050,1 +3068,4,107000100,1 +3069,4,103000090,1 +3070,5,107000110,1 +3071,1,105000000,2 +3072,2,105000001,2 +3073,3,105000002,2 +3074,4,105000003,2 +3075,5,105000004,2 +3076,1,120000000,2 +3077,2,120000001,2 +3078,3,120000002,2 +3079,4,120000003,2 +3080,5,120000004,2 +3081,3,130023,3 +3082,3,130033,3 +3083,3,130043,3 +3084,3,130053,3 +3085,4,130024,3 +3086,4,130034,3 +3087,4,130044,3 +3088,4,130054,3 +3089,3,130060,3 +3090,3,130070,3 +3091,3,130080,3 +3092,3,130090,3 +3093,3,130100,3 +3094,3,130110,3 +3095,3,130120,3 +3096,3,130130,3 +3097,3,130140,3 +3098,3,130150,3 +3099,3,130160,3 +3100,3,130170,3 +3101,3,130180,3 +3102,3,130190,3 +3103,3,130200,3 +3104,3,130420,3 +3105,3,130510,3 +3106,3,130520,3 +3107,3,130530,3 +3108,3,130540,3 +3109,3,130660,3 +3110,4,140000,3 +3111,4,150010,3 +3112,4,150020,3 +3113,4,150030,3 +3114,4,150040,3 +3116,3,106000060,1 +3117,3,106000070,1 +3118,3,106000080,1 +3119,3,101000070,1 +3120,3,110000060,1 +3121,4,106000100,1 +3122,4,106000110,1 +3123,4,104000090,1 +3124,5,106000120,1 +3125,1,103000000,2 +3126,2,103000001,2 +3127,3,103000002,2 +3128,4,103000003,2 +3129,5,103000004,2 +3130,1,112000000,2 +3131,2,112000001,2 +3132,3,112000002,2 +3133,4,112000003,2 +3134,5,112000004,2 +3135,3,170004,3 +3137,3,104000060,1 +3138,3,104000070,1 +3139,3,104000080,1 +3140,3,102000050,1 +3141,4,104000100,1 +3142,4,104000110,1 +3143,4,107000090,1 +3144,5,104000120,1 +3145,1,111000000,2 +3146,2,111000001,2 +3147,3,111000002,2 +3148,4,111000003,2 +3149,5,111000004,2 +3150,1,120000000,2 +3151,2,120000001,2 +3152,3,120000002,2 +3153,4,120000003,2 +3154,5,120000004,2 +3155,3,110023,3 +3156,3,110033,3 +3157,3,110043,3 +3158,3,110053,3 +3159,4,110024,3 +3160,4,110034,3 +3161,4,110044,3 +3162,4,110054,3 +3163,3,110060,3 +3164,3,110070,3 +3165,3,110080,3 +3166,3,110090,3 +3167,3,110100,3 +3168,3,110110,3 +3169,3,110120,3 +3170,3,110130,3 +3171,3,110140,3 +3172,3,110150,3 +3173,3,110160,3 +3174,3,110170,3 +3175,3,110180,3 +3176,3,110190,3 +3177,3,110200,3 +3178,3,110210,3 +3179,3,110220,3 +3180,3,110230,3 +3181,3,110240,3 +3182,3,110250,3 +3183,3,110260,3 +3184,3,110270,3 +3185,3,110620,3 +3186,3,110670,3 +3187,4,140000,3 +3188,4,150010,3 +3189,4,150020,3 +3190,4,150030,3 +3191,4,150040,3 +3193,3,101000050,1 +3194,3,101000060,1 +3195,3,101000080,1 +3196,3,103000060,1 +3197,3,103000070,1 +3198,3,103000080,1 +3199,3,101000040,1 +3200,3,109000060,1 +3201,3,109000070,1 +3202,3,109000080,1 +3203,3,110000050,1 +3204,4,101000090,1 +3205,4,102000090,1 +3206,4,103000100,1 +3207,4,101000100,1 +3208,4,101000110,1 +3209,4,109000100,1 +3210,5,101000120,1 +3211,5,103000120,1 +3212,5,109000110,1 +3213,1,101000000,2 +3214,2,101000001,2 +3215,3,101000002,2 +3216,4,101000008,2 +3217,5,101000004,2 +3218,1,120000000,2 +3219,2,120000001,2 +3220,3,120000002,2 +3221,4,120000003,2 +3222,5,120000004,2 +3223,3,170004,3 +3225,3,110000070,1 +3226,3,110000080,1 +3227,4,110000100,1 +3228,5,110000110,1 +3229,1,107000000,2 +3230,2,107000001,2 +3231,3,107000002,2 +3232,4,107000003,2 +3233,5,107000004,2 +3234,1,120000000,2 +3235,2,120000001,2 +3236,3,120000002,2 +3237,4,120000003,2 +3238,5,120000004,2 +3239,3,180004,3 +3241,3,105000060,1 +3242,3,105000070,1 +3243,3,105000080,1 +3244,3,104000050,1 +3245,3,106000050,1 +3246,4,105000100,1 +3247,4,105000110,1 +3248,4,108000090,1 +3249,4,110000090,1 +3250,5,105000120,1 +3251,1,109000000,2 +3252,2,109000001,2 +3253,3,109000002,2 +3254,4,109000003,2 +3255,5,109000004,2 +3256,1,112000000,2 +3257,2,112000001,2 +3258,3,112000002,2 +3259,4,112000003,2 +3260,5,112000004,2 +3261,3,170004,3 +3263,3,108000060,1 +3264,3,108000070,1 +3265,3,108000080,1 +3266,3,107000050,1 +3267,4,108000100,1 +3268,4,105000090,1 +3269,5,108000110,1 +3270,1,108000000,2 +3271,2,108000001,2 +3272,3,108000002,2 +3273,4,108000003,2 +3274,5,108000004,2 +3275,1,120000000,2 +3276,2,120000001,2 +3277,3,120000002,2 +3278,4,120000003,2 +3279,5,120000004,2 +3280,4,120024,3 +3281,4,120034,3 +3282,4,120044,3 +3283,4,120054,3 +3284,3,120240,3 +3285,3,120250,3 +3286,3,120260,3 +3287,3,120270,3 +3288,3,120300,3 +3289,3,120310,3 +3290,3,120320,3 +3291,3,120330,3 +3292,3,120340,3 +3293,3,120350,3 +3294,3,120360,3 +3295,3,120370,3 +3296,3,120380,3 +3297,3,120390,3 +3298,3,120400,3 +3299,3,120410,3 +3300,3,120420,3 +3301,3,120430,3 +3302,3,120450,3 +3303,3,120460,3 +3304,3,120550,3 +3305,3,120560,3 +3306,3,120570,3 +3307,3,120990,3 +3308,3,121000,3 +3309,3,121010,3 +3310,3,121020,3 +3311,4,140000,3 +3312,4,150010,3 +3313,4,150020,3 +3314,4,150030,3 +3315,4,150040,3 +3317,3,104000060,1 +3318,3,104000070,1 +3319,3,104000080,1 +3320,3,102000050,1 +3321,4,104000100,1 +3322,4,104000110,1 +3323,4,107000090,1 +3324,5,104000120,1 +3325,1,111000000,2 +3326,2,111000001,2 +3327,3,111000002,2 +3328,4,111000003,2 +3329,5,111000004,2 +3330,1,120000000,2 +3331,2,120000001,2 +3332,3,120000002,2 +3333,4,120000003,2 +3334,5,120000004,2 +3335,4,110024,3 +3336,4,110034,3 +3337,4,110044,3 +3338,4,110054,3 +3339,3,110060,3 +3340,3,110070,3 +3341,3,110080,3 +3342,3,110090,3 +3343,3,110100,3 +3344,3,110110,3 +3345,3,110120,3 +3346,3,110130,3 +3347,3,110140,3 +3348,3,110150,3 +3349,3,110160,3 +3350,3,110170,3 +3351,3,110180,3 +3352,3,110190,3 +3353,3,110200,3 +3354,3,110210,3 +3355,3,110220,3 +3356,3,110230,3 +3357,3,110240,3 +3358,3,110250,3 +3359,3,110260,3 +3360,3,110270,3 +3361,3,110620,3 +3362,3,110670,3 +3363,4,140000,3 +3364,4,150010,3 +3365,4,150020,3 +3366,4,150030,3 +3367,4,150040,3 +3369,3,101000050,1 +3370,3,101000060,1 +3371,3,101000080,1 +3372,3,103000060,1 +3373,3,103000070,1 +3374,3,103000080,1 +3375,3,101000040,1 +3376,3,109000060,1 +3377,3,109000070,1 +3378,3,109000080,1 +3379,3,110000050,1 +3380,4,101000090,1 +3381,4,102000090,1 +3382,4,103000100,1 +3383,4,101000100,1 +3384,4,101000110,1 +3385,4,109000100,1 +3386,5,101000120,1 +3387,5,103000120,1 +3388,5,109000110,1 +3389,1,101000000,2 +3390,2,101000001,2 +3391,3,101000002,2 +3392,4,101000008,2 +3393,5,101000004,2 +3394,1,112000000,2 +3395,2,112000001,2 +3396,3,112000002,2 +3397,4,112000003,2 +3398,5,112000004,2 +3399,3,170004,3 +3401,3,107000060,1 +3402,3,107000070,1 +3403,3,107000080,1 +3404,3,108000050,1 +3405,3,109000050,1 +3406,4,107000100,1 +3407,4,103000090,1 +3408,5,107000110,1 +3409,1,105000000,2 +3410,2,105000001,2 +3411,3,105000002,2 +3412,4,105000003,2 +3413,5,105000004,2 +3414,1,120000000,2 +3415,2,120000001,2 +3416,3,120000002,2 +3417,4,120000003,2 +3418,5,120000004,2 +3419,4,120024,3 +3420,4,120034,3 +3421,4,120044,3 +3422,4,120054,3 +3423,3,120240,3 +3424,3,120250,3 +3425,3,120260,3 +3426,3,120270,3 +3427,3,120300,3 +3428,3,120310,3 +3429,3,120320,3 +3430,3,120330,3 +3431,3,120340,3 +3432,3,120350,3 +3433,3,120360,3 +3434,3,120370,3 +3435,3,120380,3 +3436,3,120390,3 +3437,3,120400,3 +3438,3,120410,3 +3439,3,120420,3 +3440,3,120430,3 +3441,3,120450,3 +3442,3,120460,3 +3443,3,120550,3 +3444,3,120560,3 +3445,3,120570,3 +3446,3,120990,3 +3447,3,121000,3 +3448,3,121010,3 +3449,3,121020,3 +3450,4,140000,3 +3451,4,150010,3 +3452,4,150020,3 +3453,4,150030,3 +3454,4,150040,3 +3456,3,108000060,1 +3457,3,108000070,1 +3458,3,108000080,1 +3459,3,107000050,1 +3460,4,108000100,1 +3461,4,105000090,1 +3462,5,108000110,1 +3463,1,108000000,2 +3464,2,108000001,2 +3465,3,108000002,2 +3466,4,108000003,2 +3467,5,108000004,2 +3468,1,120000000,2 +3469,2,120000001,2 +3470,3,120000002,2 +3471,4,120000003,2 +3472,5,120000004,2 +3473,3,170004,3 +3475,3,102000060,1 +3476,3,102000070,1 +3477,3,102000080,1 +3478,3,103000050,1 +3479,3,105000050,1 +3480,4,102000100,1 +3481,4,102000110,1 +3482,4,106000090,1 +3483,4,109000090,1 +3484,5,102000120,1 +3485,1,102000000,2 +3486,2,102000001,2 +3487,3,102000002,2 +3488,4,102000003,2 +3489,5,102000004,2 +3490,1,112000000,2 +3491,2,112000001,2 +3492,3,112000002,2 +3493,4,112000003,2 +3494,5,112000004,2 +3495,3,180004,3 +3497,3,106000060,1 +3498,3,106000070,1 +3499,3,106000080,1 +3500,3,101000070,1 +3501,3,110000060,1 +3502,4,106000100,1 +3503,4,106000110,1 +3504,4,104000090,1 +3505,5,106000120,1 +3506,1,103000000,2 +3507,2,103000001,2 +3508,3,103000002,2 +3509,4,103000003,2 +3510,5,103000004,2 +3511,1,112000000,2 +3512,2,112000001,2 +3513,3,112000002,2 +3514,4,112000003,2 +3515,5,112000004,2 +3516,4,130024,3 +3517,4,130034,3 +3518,4,130044,3 +3519,4,130054,3 +3520,3,130060,3 +3521,3,130070,3 +3522,3,130080,3 +3523,3,130090,3 +3524,3,130100,3 +3525,3,130110,3 +3526,3,130120,3 +3527,3,130130,3 +3528,3,130140,3 +3529,3,130150,3 +3530,3,130160,3 +3531,3,130170,3 +3532,3,130180,3 +3533,3,130190,3 +3534,3,130200,3 +3535,3,130420,3 +3536,3,130510,3 +3537,3,130520,3 +3538,3,130530,3 +3539,3,130540,3 +3540,3,130660,3 +3541,4,140000,3 +3542,4,150010,3 +3543,4,150020,3 +3544,4,150030,3 +3545,4,150040,3 +3547,3,110000070,1 +3548,3,110000080,1 +3549,4,110000100,1 +3550,5,110000110,1 +3551,1,107000000,2 +3552,2,107000001,2 +3553,3,107000002,2 +3554,4,107000003,2 +3555,5,107000004,2 +3556,1,120000000,2 +3557,2,120000001,2 +3558,3,120000002,2 +3559,4,120000003,2 +3560,5,120000004,2 +3561,3,170004,3 +3563,3,105000060,1 +3564,3,105000070,1 +3565,3,105000080,1 +3566,3,104000050,1 +3567,3,106000050,1 +3568,4,105000100,1 +3569,4,105000110,1 +3570,4,108000090,1 +3571,4,110000090,1 +3572,5,105000120,1 +3573,1,109000000,2 +3574,2,109000001,2 +3575,3,109000002,2 +3576,4,109000003,2 +3577,5,109000004,2 +3578,1,112000000,2 +3579,2,112000001,2 +3580,3,112000002,2 +3581,4,112000003,2 +3582,5,112000004,2 +3583,4,120024,3 +3584,4,120034,3 +3585,4,120044,3 +3586,4,120054,3 +3587,3,120240,3 +3588,3,120250,3 +3589,3,120260,3 +3590,3,120270,3 +3591,3,120300,3 +3592,3,120310,3 +3593,3,120320,3 +3594,3,120330,3 +3595,3,120340,3 +3596,3,120350,3 +3597,3,120360,3 +3598,3,120370,3 +3599,3,120380,3 +3600,3,120390,3 +3601,3,120400,3 +3602,3,120410,3 +3603,3,120420,3 +3604,3,120430,3 +3605,3,120450,3 +3606,3,120460,3 +3607,3,120550,3 +3608,3,120560,3 +3609,3,120570,3 +3610,3,120990,3 +3611,3,121000,3 +3612,3,121010,3 +3613,3,121020,3 +3614,4,140000,3 +3615,4,150010,3 +3616,4,150020,3 +3617,4,150030,3 +3618,4,150040,3 +3620,1,170000,3 +3621,2,170001,3 +3622,3,170002,3 +3623,4,170003,3 +3624,5,170004,3 +3625,1,180000,3 +3626,2,180001,3 +3627,3,180002,3 +3628,4,180003,3 +3629,5,180004,3 +3630,4,140000,3 +3631,1,201000010,8 +3632,1,292000010,8 +3633,1,299000040,8 +3634,1,299000070,8 +3635,1,299000110,8 +3636,1,299000120,8 +3637,1,299000140,8 +3638,2,202000010,8 +3639,2,290000010,8 +3640,2,299000010,8 +3641,2,299000150,8 +3642,2,299000190,8 +3643,2,299000200,8 +3644,2,299000210,8 +3645,3,298000050,8 +3646,3,298000060,8 +3647,3,299000060,8 +3648,3,299000170,8 +3649,5,150010,3 +3650,5,150020,3 +3651,5,150030,3 +3652,5,150040,3 +3654,3,105000060,1 +3655,3,105000070,1 +3656,3,105000080,1 +3657,3,104000050,1 +3658,3,106000050,1 +3659,4,105000100,1 +3660,4,105000110,1 +3661,4,108000090,1 +3662,4,110000090,1 +3663,5,105000120,1 +3664,1,109000000,2 +3665,2,109000001,2 +3666,3,109000002,2 +3667,4,109000003,2 +3668,5,109000004,2 +3669,1,112000000,2 +3670,2,112000001,2 +3671,3,112000002,2 +3672,4,112000003,2 +3673,5,112000004,2 +3674,3,170004,3 +3676,3,108000060,1 +3677,3,108000070,1 +3678,3,108000080,1 +3679,3,107000050,1 +3680,4,108000100,1 +3681,4,105000090,1 +3682,5,108000110,1 +3683,1,108000000,2 +3684,2,108000001,2 +3685,3,108000002,2 +3686,4,108000003,2 +3687,5,108000004,2 +3688,1,120000000,2 +3689,2,120000001,2 +3690,3,120000002,2 +3691,4,120000003,2 +3692,5,120000004,2 +3693,3,180004,3 +3695,3,106000060,1 +3696,3,106000070,1 +3697,3,106000080,1 +3698,3,101000070,1 +3699,3,110000060,1 +3700,4,106000100,1 +3701,4,106000110,1 +3702,4,104000090,1 +3703,5,106000120,1 +3704,1,103000000,2 +3705,2,103000001,2 +3706,3,103000002,2 +3707,4,103000003,2 +3708,5,103000004,2 +3709,1,112000000,2 +3710,2,112000001,2 +3711,3,112000002,2 +3712,4,112000003,2 +3713,5,112000004,2 +3714,3,170004,3 +3716,3,104000170,1 +3717,1,115000000,2 +3718,2,115000001,2 +3719,3,115000002,2 +3720,4,115000003,2 +3721,5,115000004,2 +3722,1,120000000,2 +3723,2,120000001,2 +3724,3,120000002,2 +3725,4,120000003,2 +3726,5,120000004,2 +3727,4,120024,3 +3728,4,120034,3 +3729,4,120044,3 +3730,4,120054,3 +3731,3,120241,3 +3732,3,120251,3 +3733,3,120261,3 +3734,3,120271,3 +3735,3,120300,3 +3736,3,120310,3 +3737,3,120320,3 +3738,3,120330,3 +3739,3,120340,3 +3740,3,120350,3 +3741,3,120360,3 +3742,3,120370,3 +3743,3,120380,3 +3744,3,120390,3 +3745,3,120400,3 +3746,3,120410,3 +3747,3,120420,3 +3748,3,120430,3 +3749,3,120450,3 +3750,3,120460,3 +3751,3,120550,3 +3752,3,120560,3 +3753,3,120570,3 +3754,3,120990,3 +3755,3,121000,3 +3756,3,121010,3 +3757,3,121020,3 +3758,4,140000,3 +3759,4,150010,3 +3760,4,150020,3 +3761,4,150030,3 +3762,4,150040,3 +3764,3,102000060,1 +3765,3,102000070,1 +3766,3,102000080,1 +3767,3,103000050,1 +3768,3,105000050,1 +3769,4,102000100,1 +3770,4,102000110,1 +3771,4,106000090,1 +3772,4,109000090,1 +3773,5,102000120,1 +3774,1,102000000,2 +3775,2,102000001,2 +3776,3,102000002,2 +3777,4,102000003,2 +3778,5,102000004,2 +3779,1,112000000,2 +3780,2,112000001,2 +3781,3,112000002,2 +3782,4,112000003,2 +3783,5,112000004,2 +3784,4,110024,3 +3785,4,110034,3 +3786,4,110044,3 +3787,4,110054,3 +3788,3,110060,3 +3789,3,110070,3 +3790,3,110080,3 +3791,3,110090,3 +3792,3,110100,3 +3793,3,110110,3 +3794,3,110120,3 +3795,3,110130,3 +3796,3,110140,3 +3797,3,110150,3 +3798,3,110160,3 +3799,3,110170,3 +3800,3,110180,3 +3801,3,110190,3 +3802,3,110200,3 +3803,3,110210,3 +3804,3,110220,3 +3805,3,110230,3 +3806,3,110240,3 +3807,3,110250,3 +3808,3,110260,3 +3809,3,110270,3 +3810,3,110620,3 +3811,3,110670,3 +3812,4,140000,3 +3813,4,150010,3 +3814,4,150020,3 +3815,4,150030,3 +3816,4,150040,3 +3818,3,104000060,1 +3819,3,104000070,1 +3820,3,104000080,1 +3821,3,102000050,1 +3822,4,104000100,1 +3823,4,104000110,1 +3824,4,107000090,1 +3825,5,104000120,1 +3826,1,111000000,2 +3827,2,111000001,2 +3828,3,111000002,2 +3829,4,111000003,2 +3830,5,111000004,2 +3831,1,120000000,2 +3832,2,120000001,2 +3833,3,120000002,2 +3834,4,120000003,2 +3835,5,120000004,2 +3836,4,110024,3 +3837,4,110034,3 +3838,4,110044,3 +3839,4,110054,3 +3840,3,110060,3 +3841,3,110070,3 +3842,3,110080,3 +3843,3,110090,3 +3844,3,110100,3 +3845,3,110110,3 +3846,3,110120,3 +3847,3,110130,3 +3848,3,110140,3 +3849,3,110150,3 +3850,3,110160,3 +3851,3,110170,3 +3852,3,110180,3 +3853,3,110190,3 +3854,3,110200,3 +3855,3,110210,3 +3856,3,110220,3 +3857,3,110230,3 +3858,3,110240,3 +3859,3,110250,3 +3860,3,110260,3 +3861,3,110270,3 +3862,3,110620,3 +3863,3,110670,3 +3864,4,140000,3 +3865,4,150010,3 +3866,4,150020,3 +3867,4,150030,3 +3868,4,150040,3 +3870,3,110000070,1 +3871,3,110000080,1 +3872,4,110000100,1 +3873,5,110000110,1 +3874,1,107000000,2 +3875,2,107000001,2 +3876,3,107000002,2 +3877,4,107000003,2 +3878,5,107000004,2 +3879,1,120000000,2 +3880,2,120000001,2 +3881,3,120000002,2 +3882,4,120000003,2 +3883,5,120000004,2 +3884,4,130024,3 +3885,4,130034,3 +3886,4,130044,3 +3887,4,130054,3 +3888,3,130060,3 +3889,3,130070,3 +3890,3,130080,3 +3891,3,130090,3 +3892,3,130100,3 +3893,3,130110,3 +3894,3,130120,3 +3895,3,130130,3 +3896,3,130140,3 +3897,3,130150,3 +3898,3,130160,3 +3899,3,130170,3 +3900,3,130180,3 +3901,3,130190,3 +3902,3,130200,3 +3903,3,130420,3 +3904,3,130510,3 +3905,3,130520,3 +3906,3,130530,3 +3907,3,130540,3 +3908,3,130660,3 +3909,4,140000,3 +3910,4,150010,3 +3911,4,150020,3 +3912,4,150030,3 +3913,4,150040,3 +3915,3,101000050,1 +3916,3,101000060,1 +3917,3,101000080,1 +3918,3,103000060,1 +3919,3,103000070,1 +3920,3,103000080,1 +3921,3,101000040,1 +3922,3,109000060,1 +3923,3,109000070,1 +3924,3,109000080,1 +3925,3,110000050,1 +3926,4,101000090,1 +3927,4,102000090,1 +3928,4,103000100,1 +3929,4,101000100,1 +3930,4,101000110,1 +3931,4,109000100,1 +3932,5,101000120,1 +3933,5,101000160,1 +3934,5,103000120,1 +3935,5,109000110,1 +3936,1,101000000,2 +3937,2,101000001,2 +3938,3,101000002,2 +3939,4,101000008,2 +3940,5,101000004,2 +3941,1,120000000,2 +3942,2,120000001,2 +3943,3,120000002,2 +3944,4,120000003,2 +3945,5,120000004,2 +3946,3,170004,3 +3948,3,107000060,1 +3949,3,107000070,1 +3950,3,107000080,1 +3951,3,108000050,1 +3952,3,109000050,1 +3953,4,107000100,1 +3954,4,103000090,1 +3955,5,107000110,1 +3956,1,105000000,2 +3957,2,105000001,2 +3958,3,105000002,2 +3959,4,105000003,2 +3960,5,105000004,2 +3961,1,120000000,2 +3962,2,120000001,2 +3963,3,120000002,2 +3964,4,120000003,2 +3965,5,120000004,2 +3966,3,170004,3 +3968,3,104000170,1 +3969,1,115000000,2 +3970,2,115000001,2 +3971,3,115000002,2 +3972,4,115000003,2 +3973,5,115000004,2 +3974,1,120000000,2 +3975,2,120000001,2 +3976,3,120000002,2 +3977,4,120000003,2 +3978,5,120000004,2 +3979,4,120024,3 +3980,4,120034,3 +3981,4,120044,3 +3982,4,120054,3 +3983,3,120241,3 +3984,3,120251,3 +3985,3,120261,3 +3986,3,120271,3 +3987,3,120300,3 +3988,3,120310,3 +3989,3,120320,3 +3990,3,120330,3 +3991,3,120340,3 +3992,3,120350,3 +3993,3,120360,3 +3994,3,120370,3 +3995,3,120380,3 +3996,3,120390,3 +3997,3,120400,3 +3998,3,120410,3 +3999,3,120420,3 +4000,3,120430,3 +4001,3,120450,3 +4002,3,120460,3 +4003,3,120550,3 +4004,3,120560,3 +4005,3,120570,3 +4006,3,120990,3 +4007,3,121000,3 +4008,3,121010,3 +4009,3,121020,3 +4010,4,140000,3 +4011,4,150010,3 +4012,4,150020,3 +4013,4,150030,3 +4014,4,150040,3 +4016,1,101000000,2 +4017,2,101000001,2 +4018,3,101000002,2 +4019,4,101000008,2 +4020,5,101000012,2 +4021,1,120000000,2 +4022,2,120000001,2 +4023,3,120000002,2 +4024,4,120000003,2 +4025,5,120000004,2 +4027,1,101000000,2 +4028,2,101000001,2 +4029,3,101000002,2 +4030,4,101000008,2 +4031,5,101000011,2 +4032,1,120000000,2 +4033,2,120000001,2 +4034,3,120000002,2 +4035,4,120000003,2 +4036,5,120000004,2 +4038,3,101000050,1 +4039,3,101000060,1 +4040,3,101000080,1 +4041,3,103000060,1 +4042,3,103000070,1 +4043,3,103000080,1 +4044,3,101000040,1 +4045,3,109000060,1 +4046,3,109000070,1 +4047,3,109000080,1 +4048,3,110000050,1 +4049,4,101000090,1 +4050,4,102000090,1 +4051,4,103000100,1 +4052,4,101000100,1 +4053,4,101000110,1 +4054,4,109000100,1 +4055,5,101000120,1 +4056,5,101000160,1 +4057,5,103000120,1 +4058,5,109000110,1 +4059,1,101000000,2 +4060,2,101000001,2 +4061,3,101000002,2 +4062,4,101000008,2 +4063,5,101000004,2 +4064,1,120000000,2 +4065,2,120000001,2 +4066,3,120000002,2 +4067,4,120000003,2 +4068,5,120000004,2 +4069,4,120024,3 +4070,4,120034,3 +4071,4,120044,3 +4072,4,120054,3 +4073,3,120241,3 +4074,3,120251,3 +4075,3,120261,3 +4076,3,120271,3 +4077,3,120300,3 +4078,3,120310,3 +4079,3,120320,3 +4080,3,120330,3 +4081,3,120340,3 +4082,3,120350,3 +4083,3,120360,3 +4084,3,120370,3 +4085,3,120380,3 +4086,3,120390,3 +4087,3,120400,3 +4088,3,120410,3 +4089,3,120420,3 +4090,3,120430,3 +4091,3,120450,3 +4092,3,120460,3 +4093,3,120550,3 +4094,3,120560,3 +4095,3,120570,3 +4096,3,120990,3 +4097,3,121000,3 +4098,3,121010,3 +4099,3,121020,3 +4100,4,140000,3 +4101,4,150010,3 +4102,4,150020,3 +4103,4,150030,3 +4104,4,150040,3 +4106,3,108000060,1 +4107,3,108000070,1 +4108,3,108000080,1 +4109,3,107000050,1 +4110,4,108000100,1 +4111,4,105000090,1 +4112,5,108000110,1 +4113,1,108000000,2 +4114,2,108000001,2 +4115,3,108000002,2 +4116,4,108000003,2 +4117,5,108000004,2 +4118,1,120000000,2 +4119,2,120000001,2 +4120,3,120000002,2 +4121,4,120000003,2 +4122,5,120000004,2 +4123,3,170004,3 +4125,3,102000060,1 +4126,3,102000070,1 +4127,3,102000080,1 +4128,3,103000050,1 +4129,3,105000050,1 +4130,4,102000100,1 +4131,4,102000110,1 +4132,4,106000090,1 +4133,4,109000090,1 +4134,5,102000120,1 +4135,1,102000000,2 +4136,2,102000001,2 +4137,3,102000002,2 +4138,4,102000003,2 +4139,5,102000004,2 +4140,1,112000000,2 +4141,2,112000001,2 +4142,3,112000002,2 +4143,4,112000003,2 +4144,5,112000004,2 +4145,4,110024,3 +4146,4,110034,3 +4147,4,110044,3 +4148,4,110054,3 +4149,3,110060,3 +4150,3,110070,3 +4151,3,110080,3 +4152,3,110090,3 +4153,3,110100,3 +4154,3,110110,3 +4155,3,110120,3 +4156,3,110130,3 +4157,3,110140,3 +4158,3,110150,3 +4159,3,110160,3 +4160,3,110170,3 +4161,3,110180,3 +4162,3,110190,3 +4163,3,110200,3 +4164,3,110210,3 +4165,3,110220,3 +4166,3,110230,3 +4167,3,110240,3 +4168,3,110250,3 +4169,3,110260,3 +4170,3,110270,3 +4171,3,110620,3 +4172,3,110670,3 +4173,4,140000,3 +4174,4,150010,3 +4175,4,150020,3 +4176,4,150030,3 +4177,4,150040,3 +4179,3,104000170,1 +4180,4,104000180,1 +4181,5,104000190,1 +4182,1,115000000,2 +4183,2,115000001,2 +4184,3,115000002,2 +4185,4,115000003,2 +4186,5,115000004,2 +4187,1,120000000,2 +4188,2,120000001,2 +4189,3,120000002,2 +4190,4,120000003,2 +4191,5,120000004,2 +4192,4,120024,3 +4193,4,120034,3 +4194,4,120044,3 +4195,4,120054,3 +4196,3,120241,3 +4197,3,120251,3 +4198,3,120261,3 +4199,3,120271,3 +4200,3,120300,3 +4201,3,120310,3 +4202,3,120320,3 +4203,3,120330,3 +4204,3,120340,3 +4205,3,120350,3 +4206,3,120360,3 +4207,3,120370,3 +4208,3,120380,3 +4209,3,120390,3 +4210,3,120400,3 +4211,3,120410,3 +4212,3,120420,3 +4213,3,120430,3 +4214,3,120450,3 +4215,3,120460,3 +4216,3,120550,3 +4217,3,120560,3 +4218,3,120570,3 +4219,3,120990,3 +4220,3,121000,3 +4221,3,121010,3 +4222,3,121020,3 +4223,4,140000,3 +4224,4,150010,3 +4225,4,150020,3 +4226,4,150030,3 +4227,4,150040,3 +4229,3,110000070,1 +4230,3,110000080,1 +4231,4,110000100,1 +4232,5,110000110,1 +4233,1,107000000,2 +4234,2,107000001,2 +4235,3,107000002,2 +4236,4,107000003,2 +4237,5,107000004,2 +4238,1,120000000,2 +4239,2,120000001,2 +4240,3,120000002,2 +4241,4,120000003,2 +4242,5,120000004,2 +4243,4,120024,3 +4244,4,120034,3 +4245,4,120044,3 +4246,4,120054,3 +4247,3,120241,3 +4248,3,120251,3 +4249,3,120261,3 +4250,3,120271,3 +4251,3,120300,3 +4252,3,120310,3 +4253,3,120320,3 +4254,3,120330,3 +4255,3,120340,3 +4256,3,120350,3 +4257,3,120360,3 +4258,3,120370,3 +4259,3,120380,3 +4260,3,120390,3 +4261,3,120400,3 +4262,3,120410,3 +4263,3,120420,3 +4264,3,120430,3 +4265,3,120450,3 +4266,3,120460,3 +4267,3,120550,3 +4268,3,120560,3 +4269,3,120570,3 +4270,3,120990,3 +4271,3,121000,3 +4272,3,121010,3 +4273,3,121020,3 +4274,4,140000,3 +4275,4,150010,3 +4276,4,150020,3 +4277,4,150030,3 +4278,4,150040,3 +4280,3,106000060,1 +4281,3,106000070,1 +4282,3,106000080,1 +4283,3,101000070,1 +4284,3,110000060,1 +4285,4,106000100,1 +4286,4,106000110,1 +4287,4,104000090,1 +4288,5,106000120,1 +4289,1,103000000,2 +4290,2,103000001,2 +4291,3,103000002,2 +4292,4,103000003,2 +4293,5,103000004,2 +4294,1,112000000,2 +4295,2,112000001,2 +4296,3,112000002,2 +4297,4,112000003,2 +4298,5,112000004,2 +4299,3,170004,3 +4301,3,105000060,1 +4302,3,105000070,1 +4303,3,105000080,1 +4304,3,104000050,1 +4305,3,106000050,1 +4306,4,105000100,1 +4307,4,105000110,1 +4308,4,108000090,1 +4309,4,110000090,1 +4310,5,105000120,1 +4311,1,109000000,2 +4312,2,109000001,2 +4313,3,109000002,2 +4314,4,109000003,2 +4315,5,109000004,2 +4316,1,112000000,2 +4317,2,112000001,2 +4318,3,112000002,2 +4319,4,112000003,2 +4320,5,112000004,2 +4321,4,130024,3 +4322,4,130034,3 +4323,4,130044,3 +4324,4,130054,3 +4325,3,130060,3 +4326,3,130070,3 +4327,3,130080,3 +4328,3,130090,3 +4329,3,130100,3 +4330,3,130110,3 +4331,3,130120,3 +4332,3,130130,3 +4333,3,130140,3 +4334,3,130150,3 +4335,3,130160,3 +4336,3,130170,3 +4337,3,130180,3 +4338,3,130190,3 +4339,3,130200,3 +4340,3,130420,3 +4341,3,130510,3 +4342,3,130520,3 +4343,3,130530,3 +4344,3,130540,3 +4345,3,130660,3 +4346,4,140000,3 +4347,4,150010,3 +4348,4,150020,3 +4349,4,150030,3 +4350,4,150040,3 +4352,3,101000050,1 +4353,3,101000060,1 +4354,3,101000080,1 +4355,3,103000060,1 +4356,3,103000070,1 +4357,3,103000080,1 +4358,3,101000040,1 +4359,3,109000060,1 +4360,3,109000070,1 +4361,3,109000080,1 +4362,3,110000050,1 +4363,4,101000090,1 +4364,4,102000090,1 +4365,4,103000100,1 +4366,4,101000100,1 +4367,4,101000110,1 +4368,4,109000100,1 +4369,5,101000120,1 +4370,5,103000120,1 +4371,5,109000110,1 +4372,1,101000000,2 +4373,2,101000001,2 +4374,3,101000002,2 +4375,4,101000008,2 +4376,5,101000004,2 +4377,1,112000000,2 +4378,2,112000001,2 +4379,3,112000002,2 +4380,4,112000003,2 +4381,5,112000004,2 +4382,3,180004,3 +4384,3,104000060,1 +4385,3,104000070,1 +4386,3,104000080,1 +4387,3,102000050,1 +4388,4,104000100,1 +4389,4,104000110,1 +4390,4,107000090,1 +4391,5,104000120,1 +4392,1,111000000,2 +4393,2,111000001,2 +4394,3,111000002,2 +4395,4,111000003,2 +4396,5,111000004,2 +4397,1,120000000,2 +4398,2,120000001,2 +4399,3,120000002,2 +4400,4,120000003,2 +4401,5,120000004,2 +4402,4,110024,3 +4403,4,110034,3 +4404,4,110044,3 +4405,4,110054,3 +4406,3,110060,3 +4407,3,110070,3 +4408,3,110080,3 +4409,3,110090,3 +4410,3,110100,3 +4411,3,110110,3 +4412,3,110120,3 +4413,3,110130,3 +4414,3,110140,3 +4415,3,110150,3 +4416,3,110160,3 +4417,3,110170,3 +4418,3,110180,3 +4419,3,110190,3 +4420,3,110200,3 +4421,3,110210,3 +4422,3,110220,3 +4423,3,110230,3 +4424,3,110240,3 +4425,3,110250,3 +4426,3,110260,3 +4427,3,110270,3 +4428,3,110620,3 +4429,3,110670,3 +4430,4,140000,3 +4431,4,150010,3 +4432,4,150020,3 +4433,4,150030,3 +4434,4,150040,3 +4436,3,107000060,1 +4437,3,107000070,1 +4438,3,107000080,1 +4439,3,108000050,1 +4440,3,109000050,1 +4441,4,107000100,1 +4442,4,103000090,1 +4443,5,107000110,1 +4444,1,105000000,2 +4445,2,105000001,2 +4446,3,105000002,2 +4447,4,105000003,2 +4448,5,105000004,2 +4449,1,120000000,2 +4450,2,120000001,2 +4451,3,120000002,2 +4452,4,120000003,2 +4453,5,120000004,2 +4454,4,130024,3 +4455,4,130034,3 +4456,4,130044,3 +4457,4,130054,3 +4458,3,130060,3 +4459,3,130070,3 +4460,3,130080,3 +4461,3,130090,3 +4462,3,130100,3 +4463,3,130110,3 +4464,3,130120,3 +4465,3,130130,3 +4466,3,130140,3 +4467,3,130150,3 +4468,3,130160,3 +4469,3,130170,3 +4470,3,130180,3 +4471,3,130190,3 +4472,3,130200,3 +4473,3,130420,3 +4474,3,130510,3 +4475,3,130520,3 +4476,3,130530,3 +4477,3,130540,3 +4478,3,130660,3 +4479,4,140000,3 +4480,4,150010,3 +4481,4,150020,3 +4482,4,150030,3 +4483,4,150040,3 +4485,1,109000010,1 +4486,2,109000020,1 +4487,2,109000030,1 +4488,2,109000040,1 +4489,3,109000050,1 +4490,3,109000060,1 +4491,3,109000070,1 +4492,3,109000080,1 +4493,4,109000090,1 +4494,4,109000100,1 +4495,5,109000110,1 +4496,1,170000,3 +4497,2,170001,3 +4498,3,170002,3 +4499,4,170003,3 +4500,5,170004,3 +4501,1,180000,3 +4502,2,180001,3 +4503,3,180002,3 +4504,4,180003,3 +4505,5,180004,3 +4506,1,201000010,8 +4507,1,292000010,8 +4508,1,299000040,8 +4509,1,299000070,8 +4510,1,299000110,8 +4511,1,299000120,8 +4512,1,299000140,8 +4513,2,202000010,8 +4514,2,290000010,8 +4515,2,299000010,8 +4516,2,299000150,8 +4517,2,299000190,8 +4518,2,299000200,8 +4519,2,299000210,8 +4520,3,298000050,8 +4521,3,298000060,8 +4522,3,299000060,8 +4523,3,299000170,8 +4524,4,140000,3 +4525,5,150010,3 +4526,5,150020,3 +4527,5,150030,3 +4528,5,150040,3 +4530,1,109000010,1 +4531,2,109000020,1 +4532,2,109000030,1 +4533,2,109000040,1 +4534,3,109000050,1 +4535,3,109000060,1 +4536,3,109000070,1 +4537,3,109000080,1 +4538,4,109000090,1 +4539,4,109000100,1 +4540,5,109000110,1 +4541,1,170000,3 +4542,2,170001,3 +4543,3,170002,3 +4544,4,170003,3 +4545,5,170004,3 +4546,1,180000,3 +4547,2,180001,3 +4548,3,180002,3 +4549,4,180003,3 +4550,5,180004,3 +4551,1,201000010,8 +4552,1,292000010,8 +4553,1,299000040,8 +4554,1,299000070,8 +4555,1,299000110,8 +4556,1,299000120,8 +4557,1,299000140,8 +4558,2,202000010,8 +4559,2,290000010,8 +4560,2,299000010,8 +4561,2,299000150,8 +4562,2,299000190,8 +4563,2,299000200,8 +4564,2,299000210,8 +4565,3,298000050,8 +4566,3,298000060,8 +4567,3,299000060,8 +4568,3,299000170,8 +4569,4,140000,3 +4570,5,150010,3 +4571,5,150020,3 +4572,5,150030,3 +4573,5,150040,3 +4575,3,109000050,1 +4576,3,109000060,1 +4577,3,109000070,1 +4578,3,109000080,1 +4579,4,109000090,1 +4580,4,109000100,1 +4581,5,109000110,1 +4582,1,170000,3 +4583,2,170001,3 +4584,3,170002,3 +4585,4,170003,3 +4586,5,170004,3 +4587,1,180000,3 +4588,2,180001,3 +4589,3,180002,3 +4590,4,180003,3 +4591,5,180004,3 +4592,1,201000010,8 +4593,1,292000010,8 +4594,1,299000040,8 +4595,1,299000070,8 +4596,1,299000110,8 +4597,1,299000120,8 +4598,1,299000140,8 +4599,2,202000010,8 +4600,2,290000010,8 +4601,2,299000010,8 +4602,2,299000150,8 +4603,2,299000190,8 +4604,2,299000200,8 +4605,2,299000210,8 +4606,3,298000050,8 +4607,3,298000060,8 +4608,3,299000060,8 +4609,3,299000170,8 +4610,4,140000,3 +4611,5,150010,3 +4612,5,150020,3 +4613,5,150030,3 +4614,5,150040,3 +4616,3,109000050,1 +4617,3,109000060,1 +4618,3,109000070,1 +4619,3,109000080,1 +4620,4,109000090,1 +4621,4,109000100,1 +4622,5,109000110,1 +4623,1,170000,3 +4624,2,170001,3 +4625,3,170002,3 +4626,4,170003,3 +4627,5,170004,3 +4628,1,180000,3 +4629,2,180001,3 +4630,3,180002,3 +4631,4,180003,3 +4632,5,180004,3 +4633,1,201000010,8 +4634,1,292000010,8 +4635,1,299000040,8 +4636,1,299000070,8 +4637,1,299000110,8 +4638,1,299000120,8 +4639,1,299000140,8 +4640,2,202000010,8 +4641,2,290000010,8 +4642,2,299000010,8 +4643,2,299000150,8 +4644,2,299000190,8 +4645,2,299000200,8 +4646,2,299000210,8 +4647,3,298000050,8 +4648,3,298000060,8 +4649,3,299000060,8 +4650,3,299000170,8 +4651,4,140000,3 +4652,5,150010,3 +4653,5,150020,3 +4654,5,150030,3 +4655,5,150040,3 +4657,3,109000050,1 +4658,3,109000060,1 +4659,3,109000070,1 +4660,3,109000080,1 +4661,4,109000090,1 +4662,4,109000100,1 +4663,5,109000110,1 +4664,1,170000,3 +4665,2,170001,3 +4666,3,170002,3 +4667,4,170003,3 +4668,5,170004,3 +4669,1,180000,3 +4670,2,180001,3 +4671,3,180002,3 +4672,4,180003,3 +4673,5,180004,3 +4674,1,201000010,8 +4675,1,292000010,8 +4676,1,299000040,8 +4677,1,299000070,8 +4678,1,299000110,8 +4679,1,299000120,8 +4680,1,299000140,8 +4681,2,202000010,8 +4682,2,290000010,8 +4683,2,299000010,8 +4684,2,299000150,8 +4685,2,299000190,8 +4686,2,299000200,8 +4687,2,299000210,8 +4688,3,298000050,8 +4689,3,298000060,8 +4690,3,299000060,8 +4691,3,299000170,8 +4692,4,140000,3 +4693,5,150010,3 +4694,5,150020,3 +4695,5,150030,3 +4696,5,150040,3 +4697,5,190000,3 +4698,5,200000,3 +4699,5,210000,3 +4701,3,102000060,1 +4702,3,102000070,1 +4703,3,102000080,1 +4704,3,103000050,1 +4705,3,105000050,1 +4706,4,102000100,1 +4707,4,102000110,1 +4708,4,106000090,1 +4709,4,109000090,1 +4710,5,102000120,1 +4711,1,102000000,2 +4712,2,102000001,2 +4713,3,102000002,2 +4714,4,102000003,2 +4715,5,102000004,2 +4716,1,112000000,2 +4717,2,112000001,2 +4718,3,112000002,2 +4719,4,112000003,2 +4720,5,112000004,2 +4721,3,170004,3 +4722,4,170005,3 +4724,3,110000070,1 +4725,3,110000080,1 +4726,4,110000100,1 +4727,5,110000110,1 +4728,1,107000000,2 +4729,2,107000001,2 +4730,3,107000002,2 +4731,4,107000003,2 +4732,5,107000004,2 +4733,1,120000000,2 +4734,2,120000001,2 +4735,3,120000002,2 +4736,4,120000003,2 +4737,5,120000004,2 +4738,4,120024,3 +4739,4,120034,3 +4740,4,120044,3 +4741,4,120054,3 +4742,3,120241,3 +4743,3,120251,3 +4744,3,120261,3 +4745,3,120271,3 +4746,3,120300,3 +4747,3,120310,3 +4748,3,120320,3 +4749,3,120330,3 +4750,3,120340,3 +4751,3,120350,3 +4752,3,120360,3 +4753,3,120370,3 +4754,3,120380,3 +4755,3,120390,3 +4756,3,120400,3 +4757,3,120410,3 +4758,3,120420,3 +4759,3,120430,3 +4760,3,120450,3 +4761,3,120460,3 +4762,3,120550,3 +4763,3,120560,3 +4764,3,120570,3 +4765,3,120990,3 +4766,3,121000,3 +4767,3,121010,3 +4768,3,121020,3 +4769,4,140000,3 +4770,4,150010,3 +4771,4,150020,3 +4772,4,150030,3 +4773,4,150040,3 +4775,3,105000060,1 +4776,3,105000070,1 +4777,3,105000080,1 +4778,3,104000050,1 +4779,3,106000050,1 +4780,4,105000100,1 +4781,4,105000110,1 +4782,4,108000090,1 +4783,4,110000090,1 +4784,5,105000120,1 +4785,1,109000000,2 +4786,2,109000001,2 +4787,3,109000002,2 +4788,4,109000003,2 +4789,5,109000004,2 +4790,1,112000000,2 +4791,2,112000001,2 +4792,3,112000002,2 +4793,4,112000003,2 +4794,5,112000004,2 +4795,4,130024,3 +4796,4,130034,3 +4797,4,130044,3 +4798,4,130054,3 +4799,3,130060,3 +4800,3,130070,3 +4801,3,130080,3 +4802,3,130090,3 +4803,3,130100,3 +4804,3,130110,3 +4805,3,130120,3 +4806,3,130130,3 +4807,3,130140,3 +4808,3,130150,3 +4809,3,130160,3 +4810,3,130170,3 +4811,3,130180,3 +4812,3,130190,3 +4813,3,130200,3 +4814,3,130420,3 +4815,3,130510,3 +4816,3,130520,3 +4817,3,130530,3 +4818,3,130540,3 +4819,3,130660,3 +4820,4,140000,3 +4821,4,150010,3 +4822,4,150020,3 +4823,4,150030,3 +4824,4,150040,3 +4826,3,104000060,1 +4827,3,104000070,1 +4828,3,104000080,1 +4829,3,102000050,1 +4830,4,104000100,1 +4831,4,104000110,1 +4832,4,107000090,1 +4833,5,104000120,1 +4834,1,111000000,2 +4835,2,111000001,2 +4836,3,111000002,2 +4837,4,111000003,2 +4838,5,111000004,2 +4839,1,120000000,2 +4840,2,120000001,2 +4841,3,120000002,2 +4842,4,120000003,2 +4843,5,120000004,2 +4844,4,110024,3 +4845,4,110034,3 +4846,4,110044,3 +4847,4,110054,3 +4848,3,110060,3 +4849,3,110070,3 +4850,3,110080,3 +4851,3,110090,3 +4852,3,110100,3 +4853,3,110110,3 +4854,3,110120,3 +4855,3,110130,3 +4856,3,110140,3 +4857,3,110150,3 +4858,3,110160,3 +4859,3,110170,3 +4860,3,110180,3 +4861,3,110190,3 +4862,3,110200,3 +4863,3,110210,3 +4864,3,110220,3 +4865,3,110230,3 +4866,3,110240,3 +4867,3,110250,3 +4868,3,110260,3 +4869,3,110270,3 +4870,3,110620,3 +4871,3,110670,3 +4872,4,140000,3 +4873,4,150010,3 +4874,4,150020,3 +4875,4,150030,3 +4876,4,150040,3 +4878,3,107000060,1 +4879,3,107000070,1 +4880,3,107000080,1 +4881,3,108000050,1 +4882,3,109000050,1 +4883,4,107000100,1 +4884,4,103000090,1 +4885,5,107000110,1 +4886,1,105000000,2 +4887,2,105000001,2 +4888,3,105000002,2 +4889,4,105000003,2 +4890,5,105000004,2 +4891,1,120000000,2 +4892,2,120000001,2 +4893,3,120000002,2 +4894,4,120000003,2 +4895,5,120000004,2 +4896,3,180004,3 +4897,4,180005,3 +4899,3,106000060,1 +4900,3,106000070,1 +4901,3,106000080,1 +4902,3,101000070,1 +4903,3,110000060,1 +4904,4,106000100,1 +4905,4,106000110,1 +4906,4,104000090,1 +4907,5,106000120,1 +4908,1,103000000,2 +4909,2,103000001,2 +4910,3,103000002,2 +4911,4,103000003,2 +4912,5,103000004,2 +4913,1,112000000,2 +4914,2,112000001,2 +4915,3,112000002,2 +4916,4,112000003,2 +4917,5,112000004,2 +4918,3,170004,3 +4919,4,170005,3 +4921,3,112000020,1 +4922,3,112000030,1 +4923,4,112000040,1 +4924,5,112000060,1 +4925,1,101000000,2 +4926,2,101000001,2 +4927,3,101000002,2 +4928,4,101000008,2 +4929,5,101000004,2 +4930,1,112000000,2 +4931,2,112000001,2 +4932,3,112000002,2 +4933,4,112000003,2 +4934,5,112000004,2 +4935,3,170004,3 +4936,4,170005,3 +4938,3,104000170,1 +4939,4,104000180,1 +4940,5,104000190,1 +4941,1,115000000,2 +4942,2,115000001,2 +4943,3,115000002,2 +4944,4,115000003,2 +4945,5,115000004,2 +4946,1,120000000,2 +4947,2,120000001,2 +4948,3,120000002,2 +4949,4,120000003,2 +4950,5,120000004,2 +4951,4,120024,3 +4952,4,120034,3 +4953,4,120044,3 +4954,4,120054,3 +4955,3,120241,3 +4956,3,120251,3 +4957,3,120261,3 +4958,3,120271,3 +4959,3,120300,3 +4960,3,120310,3 +4961,3,120320,3 +4962,3,120330,3 +4963,3,120340,3 +4964,3,120350,3 +4965,3,120360,3 +4966,3,120370,3 +4967,3,120380,3 +4968,3,120390,3 +4969,3,120400,3 +4970,3,120410,3 +4971,3,120420,3 +4972,3,120430,3 +4973,3,120450,3 +4974,3,120460,3 +4975,3,120550,3 +4976,3,120560,3 +4977,3,120570,3 +4978,3,120990,3 +4979,3,121000,3 +4980,3,121010,3 +4981,3,121020,3 +4982,4,140000,3 +4983,4,150010,3 +4984,4,150020,3 +4985,4,150030,3 +4986,4,150040,3 +4988,3,111000010,1 +4989,3,111000020,1 +4990,3,111000030,1 +4991,4,111000040,1 +4992,5,111000060,1 +4993,1,101000000,2 +4994,2,101000001,2 +4995,3,101000002,2 +4996,4,101000008,2 +4997,5,101000004,2 +4998,1,120000000,2 +4999,2,120000001,2 +5000,3,120000002,2 +5001,4,120000003,2 +5002,5,120000004,2 +5003,4,110024,3 +5004,4,110034,3 +5005,4,110044,3 +5006,4,110054,3 +5007,3,110060,3 +5008,3,110070,3 +5009,3,110080,3 +5010,3,110090,3 +5011,3,110100,3 +5012,3,110110,3 +5013,3,110120,3 +5014,3,110130,3 +5015,3,110140,3 +5016,3,110150,3 +5017,3,110160,3 +5018,3,110170,3 +5019,3,110180,3 +5020,3,110190,3 +5021,3,110200,3 +5022,3,110210,3 +5023,3,110220,3 +5024,3,110230,3 +5025,3,110240,3 +5026,3,110250,3 +5027,3,110260,3 +5028,3,110270,3 +5029,3,110620,3 +5030,3,110670,3 +5031,4,140000,3 +5032,4,150010,3 +5033,4,150020,3 +5034,4,150030,3 +5035,4,150040,3 +5037,3,108000060,1 +5038,3,108000070,1 +5039,3,108000080,1 +5040,3,107000050,1 +5041,3,112000010,1 +5042,4,108000100,1 +5043,4,105000090,1 +5044,5,108000110,1 +5045,1,108000000,2 +5046,2,108000001,2 +5047,3,108000002,2 +5048,4,108000003,2 +5049,5,108000004,2 +5050,1,120000000,2 +5051,2,120000001,2 +5052,3,120000002,2 +5053,4,120000003,2 +5054,5,120000004,2 +5055,4,130024,3 +5056,4,130034,3 +5057,4,130044,3 +5058,4,130054,3 +5059,3,130060,3 +5060,3,130070,3 +5061,3,130080,3 +5062,3,130090,3 +5063,3,130100,3 +5064,3,130110,3 +5065,3,130120,3 +5066,3,130130,3 +5067,3,130140,3 +5068,3,130150,3 +5069,3,130160,3 +5070,3,130170,3 +5071,3,130180,3 +5072,3,130190,3 +5073,3,130200,3 +5074,3,130420,3 +5075,3,130510,3 +5076,3,130520,3 +5077,3,130530,3 +5078,3,130540,3 +5079,3,130660,3 +5080,4,140000,3 +5081,4,150010,3 +5082,4,150020,3 +5083,4,150030,3 +5084,4,150040,3 +5086,3,111000010,1 +5087,3,111000020,1 +5088,3,111000030,1 +5089,4,111000040,1 +5090,5,111000060,1 +5091,1,170002,3 +5092,2,170003,3 +5093,3,170004,3 +5094,1,180002,3 +5095,2,180003,3 +5096,3,180004,3 +5097,1,201000010,8 +5098,1,292000010,8 +5099,1,299000040,8 +5100,1,299000070,8 +5101,1,299000110,8 +5102,1,299000120,8 +5103,1,299000140,8 +5104,2,202000010,8 +5105,2,290000010,8 +5106,2,299000010,8 +5107,2,299000150,8 +5108,2,299000190,8 +5109,2,299000200,8 +5110,2,299000210,8 +5111,3,298000050,8 +5112,3,298000060,8 +5113,3,299000060,8 +5114,3,299000170,8 +5115,4,140000,3 +5116,5,150010,3 +5117,5,150020,3 +5118,5,150030,3 +5119,5,150040,3 +5121,3,111000010,1 +5122,3,111000020,1 +5123,3,111000030,1 +5124,4,111000040,1 +5125,5,111000060,1 +5126,2,170003,3 +5127,3,170004,3 +5128,2,180003,3 +5129,3,180004,3 +5130,1,201000010,8 +5131,1,292000010,8 +5132,1,299000040,8 +5133,1,299000070,8 +5134,1,299000110,8 +5135,1,299000120,8 +5136,1,299000140,8 +5137,2,202000010,8 +5138,2,290000010,8 +5139,2,299000010,8 +5140,2,299000150,8 +5141,2,299000190,8 +5142,2,299000200,8 +5143,2,299000210,8 +5144,3,298000050,8 +5145,3,298000060,8 +5146,3,299000060,8 +5147,3,299000170,8 +5148,4,140000,3 +5149,5,150010,3 +5150,5,150020,3 +5151,5,150030,3 +5152,5,150040,3 +5154,3,111000010,1 +5155,3,111000020,1 +5156,3,111000030,1 +5157,4,111000040,1 +5158,5,111000060,1 +5159,3,170004,3 +5160,3,180004,3 +5161,1,201000010,8 +5162,1,292000010,8 +5163,1,299000040,8 +5164,1,299000070,8 +5165,1,299000110,8 +5166,1,299000120,8 +5167,1,299000140,8 +5168,2,202000010,8 +5169,2,290000010,8 +5170,2,299000010,8 +5171,2,299000150,8 +5172,2,299000190,8 +5173,2,299000200,8 +5174,2,299000210,8 +5175,3,298000050,8 +5176,3,298000060,8 +5177,3,299000060,8 +5178,3,299000170,8 +5179,4,140000,3 +5180,5,150010,3 +5181,5,150020,3 +5182,5,150030,3 +5183,5,150040,3 +5185,3,111000010,1 +5186,3,111000020,1 +5187,3,111000030,1 +5188,4,111000040,1 +5189,5,111000060,1 +5190,3,170004,3 +5191,3,180004,3 +5192,1,201000010,8 +5193,1,292000010,8 +5194,1,299000040,8 +5195,1,299000070,8 +5196,1,299000110,8 +5197,1,299000120,8 +5198,1,299000140,8 +5199,2,202000010,8 +5200,2,290000010,8 +5201,2,299000010,8 +5202,2,299000150,8 +5203,2,299000190,8 +5204,2,299000200,8 +5205,2,299000210,8 +5206,3,298000050,8 +5207,3,298000060,8 +5208,3,299000060,8 +5209,3,299000170,8 +5210,4,140000,3 +5211,5,150010,3 +5212,5,150020,3 +5213,5,150030,3 +5214,5,150040,3 +5216,3,111000010,1 +5217,3,111000020,1 +5218,3,111000030,1 +5219,4,111000040,1 +5220,5,111000060,1 +5221,3,170004,3 +5222,3,180004,3 +5223,1,201000010,8 +5224,1,292000010,8 +5225,1,299000040,8 +5226,1,299000070,8 +5227,1,299000110,8 +5228,1,299000120,8 +5229,1,299000140,8 +5230,2,202000010,8 +5231,2,290000010,8 +5232,2,299000010,8 +5233,2,299000150,8 +5234,2,299000190,8 +5235,2,299000200,8 +5236,2,299000210,8 +5237,3,298000050,8 +5238,3,298000060,8 +5239,3,299000060,8 +5240,3,299000170,8 +5241,4,140000,3 +5242,5,150010,3 +5243,5,150020,3 +5244,5,150030,3 +5245,5,150040,3 +5246,5,190000,3 +5247,5,200000,3 +5248,5,210000,3 +5250,3,101000050,1 +5251,3,101000060,1 +5252,3,101000080,1 +5253,3,101000040,1 +5254,3,109000060,1 +5255,3,109000070,1 +5256,3,109000080,1 +5257,3,105000060,1 +5258,3,105000070,1 +5259,3,105000080,1 +5260,3,104000050,1 +5261,3,106000050,1 +5262,4,101000090,1 +5263,4,101000100,1 +5264,4,101000110,1 +5265,4,109000100,1 +5266,4,105000100,1 +5267,4,105000110,1 +5268,4,108000090,1 +5269,4,110000090,1 +5270,5,101000120,1 +5271,5,109000110,1 +5272,5,105000120,1 +5273,1,101000000,2 +5274,2,101000001,2 +5275,3,101000002,2 +5276,4,101000008,2 +5277,5,101000004,2 +5278,1,109000000,2 +5279,2,109000001,2 +5280,3,109000002,2 +5281,4,109000003,2 +5282,5,109000004,2 +5283,3,170004,3 +5284,4,170005,3 +5285,3,180004,3 +5286,4,180005,3 +5287,4,140000,3 +5288,4,150010,3 +5289,4,150020,3 +5290,4,150030,3 +5291,4,150040,3 +5293,3,102000060,1 +5294,3,102000070,1 +5295,3,102000080,1 +5296,3,103000050,1 +5297,3,105000050,1 +5298,3,107000060,1 +5299,3,107000070,1 +5300,3,107000080,1 +5301,3,108000050,1 +5302,3,109000050,1 +5303,3,103000060,1 +5304,3,103000070,1 +5305,3,103000080,1 +5306,3,110000050,1 +5307,4,102000100,1 +5308,4,102000110,1 +5309,4,106000090,1 +5310,4,109000090,1 +5311,4,107000100,1 +5312,4,103000090,1 +5313,4,102000090,1 +5314,4,103000100,1 +5315,5,102000120,1 +5316,5,107000110,1 +5317,5,103000120,1 +5318,1,102000000,2 +5319,2,102000001,2 +5320,3,102000002,2 +5321,4,102000003,2 +5322,5,102000004,2 +5323,1,105000000,2 +5324,2,105000001,2 +5325,3,105000002,2 +5326,4,105000003,2 +5327,5,105000004,2 +5328,1,112000000,2 +5329,2,112000001,2 +5330,3,112000002,2 +5331,4,112000003,2 +5332,5,112000004,2 +5333,4,110024,3 +5334,4,110034,3 +5335,4,110044,3 +5336,4,110054,3 +5337,3,110060,3 +5338,3,110070,3 +5339,3,110080,3 +5340,3,110090,3 +5341,3,110100,3 +5342,3,110110,3 +5343,3,110120,3 +5344,3,110130,3 +5345,3,110140,3 +5346,3,110150,3 +5347,3,110160,3 +5348,3,110170,3 +5349,3,110180,3 +5350,3,110190,3 +5351,3,110200,3 +5352,3,110210,3 +5353,3,110220,3 +5354,3,110230,3 +5355,3,110240,3 +5356,3,110250,3 +5357,3,110260,3 +5358,3,110270,3 +5359,3,110620,3 +5360,3,110670,3 +5361,4,140000,3 +5362,4,150010,3 +5363,4,150020,3 +5364,4,150030,3 +5365,4,150040,3 +5367,3,106000060,1 +5368,3,106000070,1 +5369,3,106000080,1 +5370,3,101000070,1 +5371,3,110000060,1 +5372,3,104000060,1 +5373,3,104000070,1 +5374,3,104000080,1 +5375,3,102000050,1 +5376,3,104000170,1 +5377,3,104000260,1 +5378,4,106000100,1 +5379,4,106000110,1 +5380,4,104000090,1 +5381,4,104000100,1 +5382,4,104000110,1 +5383,4,107000090,1 +5384,4,104000180,1 +5385,4,104000270,1 +5386,5,106000120,1 +5387,5,104000120,1 +5388,5,104000190,1 +5389,1,103000000,2 +5390,2,103000001,2 +5391,3,103000002,2 +5392,4,103000003,2 +5393,5,103000004,2 +5394,1,111000000,2 +5395,2,111000001,2 +5396,3,111000002,2 +5397,4,111000003,2 +5398,5,111000004,2 +5399,1,115000000,2 +5400,2,115000001,2 +5401,3,115000002,2 +5402,4,115000003,2 +5403,5,115000004,2 +5404,4,120024,3 +5405,4,120034,3 +5406,4,120044,3 +5407,4,120054,3 +5408,3,120241,3 +5409,3,120251,3 +5410,3,120261,3 +5411,3,120271,3 +5412,3,120300,3 +5413,3,120310,3 +5414,3,120320,3 +5415,3,120330,3 +5416,3,120340,3 +5417,3,120350,3 +5418,3,120360,3 +5419,3,120370,3 +5420,3,120380,3 +5421,3,120390,3 +5422,3,120400,3 +5423,3,120410,3 +5424,3,120420,3 +5425,3,120430,3 +5426,3,120450,3 +5427,3,120460,3 +5428,3,120550,3 +5429,3,120560,3 +5430,3,120570,3 +5431,3,120990,3 +5432,3,121000,3 +5433,3,121010,3 +5434,3,121020,3 +5435,4,140000,3 +5436,4,150010,3 +5437,4,150020,3 +5438,4,150030,3 +5439,4,150040,3 +5441,3,111000010,1 +5442,3,111000020,1 +5443,3,111000030,1 +5444,3,112000020,1 +5445,3,112000030,1 +5446,3,108000060,1 +5447,3,108000070,1 +5448,3,108000080,1 +5449,3,107000050,1 +5450,3,112000010,1 +5451,3,110000070,1 +5452,3,110000080,1 +5453,4,111000040,1 +5454,4,112000040,1 +5455,4,108000100,1 +5456,4,105000090,1 +5457,4,110000100,1 +5458,5,111000060,1 +5459,5,112000060,1 +5460,5,108000110,1 +5461,5,110000110,1 +5462,1,108000000,2 +5463,2,108000001,2 +5464,3,108000002,2 +5465,4,108000003,2 +5466,5,108000004,2 +5467,1,107000000,2 +5468,2,107000001,2 +5469,3,107000002,2 +5470,4,107000003,2 +5471,5,107000004,2 +5472,1,120000000,2 +5473,2,120000001,2 +5474,3,120000002,2 +5475,4,120000003,2 +5476,5,120000004,2 +5477,4,130024,3 +5478,4,130034,3 +5479,4,130044,3 +5480,4,130054,3 +5481,3,130060,3 +5482,3,130070,3 +5483,3,130080,3 +5484,3,130090,3 +5485,3,130100,3 +5486,3,130110,3 +5487,3,130120,3 +5488,3,130130,3 +5489,3,130140,3 +5490,3,130150,3 +5491,3,130160,3 +5492,3,130170,3 +5493,3,130180,3 +5494,3,130190,3 +5495,3,130200,3 +5496,3,130420,3 +5497,3,130510,3 +5498,3,130520,3 +5499,3,130530,3 +5500,3,130540,3 +5501,3,130660,3 +5502,4,140000,3 +5503,4,150010,3 +5504,4,150020,3 +5505,4,150030,3 +5506,4,150040,3 +5508,1,101000000,2 +5509,2,101000001,2 +5510,3,101000002,2 +5511,4,101000008,2 +5512,5,101000004,2 +5513,1,120000000,2 +5514,2,120000001,2 +5515,3,120000002,2 +5516,4,120000003,2 +5517,5,120000004,2 +5519,1,101000010,1 +5520,1,102000010,1 +5521,1,103000010,1 +5522,1,104000010,1 +5523,1,105000010,1 +5524,1,106000010,1 +5525,1,107000010,1 +5526,1,108000010,1 +5527,1,109000010,1 +5528,1,110000010,1 +5529,2,101000020,1 +5530,2,101000030,1 +5531,2,102000020,1 +5532,2,102000030,1 +5533,2,102000040,1 +5534,2,103000020,1 +5535,2,103000030,1 +5536,2,103000040,1 +5537,2,104000020,1 +5538,2,104000030,1 +5539,2,104000040,1 +5540,2,105000020,1 +5541,2,105000030,1 +5542,2,105000040,1 +5543,2,106000020,1 +5544,2,106000030,1 +5545,2,106000040,1 +5546,2,107000020,1 +5547,2,107000030,1 +5548,2,107000040,1 +5549,2,108000020,1 +5550,2,108000030,1 +5551,2,108000040,1 +5552,2,109000020,1 +5553,2,109000030,1 +5554,2,109000040,1 +5555,2,110000020,1 +5556,2,110000030,1 +5557,2,110000040,1 +5558,3,101000050,1 +5559,3,101000060,1 +5560,3,101000080,1 +5561,3,101000040,1 +5562,3,109000060,1 +5563,3,109000070,1 +5564,3,109000080,1 +5565,3,105000060,1 +5566,3,105000070,1 +5567,3,105000080,1 +5568,3,104000050,1 +5569,3,106000050,1 +5570,3,102000060,1 +5571,3,102000070,1 +5572,3,102000080,1 +5573,3,103000050,1 +5574,3,105000050,1 +5575,3,107000060,1 +5576,3,107000070,1 +5577,3,107000080,1 +5578,3,108000050,1 +5579,3,109000050,1 +5580,3,103000060,1 +5581,3,103000070,1 +5582,3,103000080,1 +5583,3,110000050,1 +5584,3,106000060,1 +5585,3,106000070,1 +5586,3,106000080,1 +5587,3,101000070,1 +5588,3,110000060,1 +5589,3,104000060,1 +5590,3,104000070,1 +5591,3,104000080,1 +5592,3,102000050,1 +5593,3,104000170,1 +5594,3,104000260,1 +5595,3,111000010,1 +5596,3,111000020,1 +5597,3,111000030,1 +5598,3,112000020,1 +5599,3,112000030,1 +5600,3,108000060,1 +5601,3,108000070,1 +5602,3,108000080,1 +5603,3,107000050,1 +5604,3,112000010,1 +5605,3,110000070,1 +5606,3,110000080,1 +5607,4,101000090,1 +5608,4,101000100,1 +5609,4,101000110,1 +5610,4,109000100,1 +5611,4,105000100,1 +5612,4,105000110,1 +5613,4,108000090,1 +5614,4,110000090,1 +5615,4,102000100,1 +5616,4,102000110,1 +5617,4,106000090,1 +5618,4,109000090,1 +5619,4,107000100,1 +5620,4,103000090,1 +5621,4,102000090,1 +5622,4,103000100,1 +5623,4,106000100,1 +5624,4,106000110,1 +5625,4,104000090,1 +5626,4,104000100,1 +5627,4,104000110,1 +5628,4,107000090,1 +5629,4,104000180,1 +5630,4,111000040,1 +5631,4,112000040,1 +5632,4,108000100,1 +5633,4,105000090,1 +5634,4,110000100,1 +5635,5,101000120,1 +5636,5,109000110,1 +5637,5,105000120,1 +5638,5,102000120,1 +5639,5,107000110,1 +5640,5,103000120,1 +5641,5,106000120,1 +5642,5,104000120,1 +5643,5,104000190,1 +5644,5,111000060,1 +5645,5,112000060,1 +5646,5,108000110,1 +5647,5,110000110,1 +5648,1,170002,3 +5649,2,170003,3 +5650,3,170004,3 +5651,1,180002,3 +5652,2,180003,3 +5653,3,180004,3 +5654,1,201000010,8 +5655,1,292000010,8 +5656,1,299000040,8 +5657,1,299000070,8 +5658,1,299000110,8 +5659,1,299000120,8 +5660,1,299000140,8 +5661,2,202000010,8 +5662,2,290000010,8 +5663,2,299000010,8 +5664,2,299000150,8 +5665,2,299000190,8 +5666,2,299000200,8 +5667,2,299000210,8 +5668,3,298000050,8 +5669,3,298000060,8 +5670,3,299000060,8 +5671,3,299000170,8 +5672,5,297000100,8 +5673,5,291000020,8 +5674,4,140000,3 +5675,5,150010,3 +5676,5,150020,3 +5677,5,150030,3 +5678,5,150040,3 +5680,1,101000010,1 +5681,1,102000010,1 +5682,1,103000010,1 +5683,1,104000010,1 +5684,1,105000010,1 +5685,1,106000010,1 +5686,1,107000010,1 +5687,1,108000010,1 +5688,1,109000010,1 +5689,1,110000010,1 +5690,2,101000020,1 +5691,2,101000030,1 +5692,2,102000020,1 +5693,2,102000030,1 +5694,2,102000040,1 +5695,2,103000020,1 +5696,2,103000030,1 +5697,2,103000040,1 +5698,2,104000020,1 +5699,2,104000030,1 +5700,2,104000040,1 +5701,2,105000020,1 +5702,2,105000030,1 +5703,2,105000040,1 +5704,2,106000020,1 +5705,2,106000030,1 +5706,2,106000040,1 +5707,2,107000020,1 +5708,2,107000030,1 +5709,2,107000040,1 +5710,2,108000020,1 +5711,2,108000030,1 +5712,2,108000040,1 +5713,2,109000020,1 +5714,2,109000030,1 +5715,2,109000040,1 +5716,2,110000020,1 +5717,2,110000030,1 +5718,2,110000040,1 +5719,3,101000050,1 +5720,3,101000060,1 +5721,3,101000080,1 +5722,3,101000040,1 +5723,3,109000060,1 +5724,3,109000070,1 +5725,3,109000080,1 +5726,3,105000060,1 +5727,3,105000070,1 +5728,3,105000080,1 +5729,3,104000050,1 +5730,3,106000050,1 +5731,3,102000060,1 +5732,3,102000070,1 +5733,3,102000080,1 +5734,3,103000050,1 +5735,3,105000050,1 +5736,3,107000060,1 +5737,3,107000070,1 +5738,3,107000080,1 +5739,3,108000050,1 +5740,3,109000050,1 +5741,3,103000060,1 +5742,3,103000070,1 +5743,3,103000080,1 +5744,3,110000050,1 +5745,3,106000060,1 +5746,3,106000070,1 +5747,3,106000080,1 +5748,3,101000070,1 +5749,3,110000060,1 +5750,3,104000060,1 +5751,3,104000070,1 +5752,3,104000080,1 +5753,3,102000050,1 +5754,3,104000170,1 +5755,3,104000260,1 +5756,3,111000010,1 +5757,3,111000020,1 +5758,3,111000030,1 +5759,3,112000020,1 +5760,3,112000030,1 +5761,3,108000060,1 +5762,3,108000070,1 +5763,3,108000080,1 +5764,3,107000050,1 +5765,3,112000010,1 +5766,3,110000070,1 +5767,3,110000080,1 +5768,4,101000090,1 +5769,4,101000100,1 +5770,4,101000110,1 +5771,4,109000100,1 +5772,4,105000100,1 +5773,4,105000110,1 +5774,4,108000090,1 +5775,4,110000090,1 +5776,4,102000100,1 +5777,4,102000110,1 +5778,4,106000090,1 +5779,4,109000090,1 +5780,4,107000100,1 +5781,4,103000090,1 +5782,4,102000090,1 +5783,4,103000100,1 +5784,4,106000100,1 +5785,4,106000110,1 +5786,4,104000090,1 +5787,4,104000100,1 +5788,4,104000110,1 +5789,4,107000090,1 +5790,4,104000180,1 +5791,4,111000040,1 +5792,4,112000040,1 +5793,4,108000100,1 +5794,4,105000090,1 +5795,4,110000100,1 +5796,5,101000120,1 +5797,5,109000110,1 +5798,5,105000120,1 +5799,5,102000120,1 +5800,5,107000110,1 +5801,5,103000120,1 +5802,5,106000120,1 +5803,5,104000120,1 +5804,5,104000190,1 +5805,5,111000060,1 +5806,5,112000060,1 +5807,5,108000110,1 +5808,5,110000110,1 +5809,2,170003,3 +5810,3,170004,3 +5811,2,180003,3 +5812,3,180004,3 +5813,1,201000010,8 +5814,1,292000010,8 +5815,1,299000040,8 +5816,1,299000070,8 +5817,1,299000110,8 +5818,1,299000120,8 +5819,1,299000140,8 +5820,2,202000010,8 +5821,2,290000010,8 +5822,2,299000010,8 +5823,2,299000150,8 +5824,2,299000190,8 +5825,2,299000200,8 +5826,2,299000210,8 +5827,3,298000050,8 +5828,3,298000060,8 +5829,3,299000060,8 +5830,3,299000170,8 +5831,5,297000100,8 +5832,5,291000020,8 +5833,4,140000,3 +5834,5,150010,3 +5835,5,150020,3 +5836,5,150030,3 +5837,5,150040,3 +5839,3,101000050,1 +5840,3,101000060,1 +5841,3,101000080,1 +5842,3,101000040,1 +5843,3,109000060,1 +5844,3,109000070,1 +5845,3,109000080,1 +5846,3,105000060,1 +5847,3,105000070,1 +5848,3,105000080,1 +5849,3,104000050,1 +5850,3,106000050,1 +5851,3,102000060,1 +5852,3,102000070,1 +5853,3,102000080,1 +5854,3,103000050,1 +5855,3,105000050,1 +5856,3,107000060,1 +5857,3,107000070,1 +5858,3,107000080,1 +5859,3,108000050,1 +5860,3,109000050,1 +5861,3,103000060,1 +5862,3,103000070,1 +5863,3,103000080,1 +5864,3,110000050,1 +5865,3,106000060,1 +5866,3,106000070,1 +5867,3,106000080,1 +5868,3,101000070,1 +5869,3,110000060,1 +5870,3,104000060,1 +5871,3,104000070,1 +5872,3,104000080,1 +5873,3,102000050,1 +5874,3,104000170,1 +5875,3,104000260,1 +5876,3,111000010,1 +5877,3,111000020,1 +5878,3,111000030,1 +5879,3,112000020,1 +5880,3,112000030,1 +5881,3,108000060,1 +5882,3,108000070,1 +5883,3,108000080,1 +5884,3,107000050,1 +5885,3,112000010,1 +5886,3,110000070,1 +5887,3,110000080,1 +5888,4,101000090,1 +5889,4,101000100,1 +5890,4,101000110,1 +5891,4,109000100,1 +5892,4,105000100,1 +5893,4,105000110,1 +5894,4,108000090,1 +5895,4,110000090,1 +5896,4,102000100,1 +5897,4,102000110,1 +5898,4,106000090,1 +5899,4,109000090,1 +5900,4,107000100,1 +5901,4,103000090,1 +5902,4,102000090,1 +5903,4,103000100,1 +5904,4,106000100,1 +5905,4,106000110,1 +5906,4,104000090,1 +5907,4,104000100,1 +5908,4,104000110,1 +5909,4,107000090,1 +5910,4,104000180,1 +5911,4,111000040,1 +5912,4,112000040,1 +5913,4,108000100,1 +5914,4,105000090,1 +5915,4,110000100,1 +5916,5,101000120,1 +5917,5,109000110,1 +5918,5,105000120,1 +5919,5,102000120,1 +5920,5,107000110,1 +5921,5,103000120,1 +5922,5,106000120,1 +5923,5,104000120,1 +5924,5,104000190,1 +5925,5,111000060,1 +5926,5,112000060,1 +5927,5,108000110,1 +5928,5,110000110,1 +5929,3,170004,3 +5930,3,180004,3 +5931,1,201000010,8 +5932,1,292000010,8 +5933,1,299000040,8 +5934,1,299000070,8 +5935,1,299000110,8 +5936,1,299000120,8 +5937,1,299000140,8 +5938,2,202000010,8 +5939,2,290000010,8 +5940,2,299000010,8 +5941,2,299000150,8 +5942,2,299000190,8 +5943,2,299000200,8 +5944,2,299000210,8 +5945,3,298000050,8 +5946,3,298000060,8 +5947,3,299000060,8 +5948,3,299000170,8 +5949,5,297000100,8 +5950,5,291000020,8 +5951,4,140000,3 +5952,5,150010,3 +5953,5,150020,3 +5954,5,150030,3 +5955,5,150040,3 +5957,3,101000050,1 +5958,3,101000060,1 +5959,3,101000080,1 +5960,3,101000040,1 +5961,3,109000060,1 +5962,3,109000070,1 +5963,3,109000080,1 +5964,3,105000060,1 +5965,3,105000070,1 +5966,3,105000080,1 +5967,3,104000050,1 +5968,3,106000050,1 +5969,3,102000060,1 +5970,3,102000070,1 +5971,3,102000080,1 +5972,3,103000050,1 +5973,3,105000050,1 +5974,3,107000060,1 +5975,3,107000070,1 +5976,3,107000080,1 +5977,3,108000050,1 +5978,3,109000050,1 +5979,3,103000060,1 +5980,3,103000070,1 +5981,3,103000080,1 +5982,3,110000050,1 +5983,3,106000060,1 +5984,3,106000070,1 +5985,3,106000080,1 +5986,3,101000070,1 +5987,3,110000060,1 +5988,3,104000060,1 +5989,3,104000070,1 +5990,3,104000080,1 +5991,3,102000050,1 +5992,3,104000170,1 +5993,3,104000260,1 +5994,3,111000010,1 +5995,3,111000020,1 +5996,3,111000030,1 +5997,3,112000020,1 +5998,3,112000030,1 +5999,3,108000060,1 +6000,3,108000070,1 +6001,3,108000080,1 +6002,3,107000050,1 +6003,3,112000010,1 +6004,3,110000070,1 +6005,3,110000080,1 +6006,4,101000090,1 +6007,4,101000100,1 +6008,4,101000110,1 +6009,4,109000100,1 +6010,4,105000100,1 +6011,4,105000110,1 +6012,4,108000090,1 +6013,4,110000090,1 +6014,4,102000100,1 +6015,4,102000110,1 +6016,4,106000090,1 +6017,4,109000090,1 +6018,4,107000100,1 +6019,4,103000090,1 +6020,4,102000090,1 +6021,4,103000100,1 +6022,4,106000100,1 +6023,4,106000110,1 +6024,4,104000090,1 +6025,4,104000100,1 +6026,4,104000110,1 +6027,4,107000090,1 +6028,4,104000180,1 +6029,4,111000040,1 +6030,4,112000040,1 +6031,4,108000100,1 +6032,4,105000090,1 +6033,4,110000100,1 +6034,5,101000120,1 +6035,5,109000110,1 +6036,5,105000120,1 +6037,5,102000120,1 +6038,5,107000110,1 +6039,5,103000120,1 +6040,5,106000120,1 +6041,5,104000120,1 +6042,5,104000190,1 +6043,5,111000060,1 +6044,5,112000060,1 +6045,5,108000110,1 +6046,5,110000110,1 +6047,3,170004,3 +6048,3,180004,3 +6049,1,201000010,8 +6050,1,292000010,8 +6051,1,299000040,8 +6052,1,299000070,8 +6053,1,299000110,8 +6054,1,299000120,8 +6055,1,299000140,8 +6056,2,202000010,8 +6057,2,290000010,8 +6058,2,299000010,8 +6059,2,299000150,8 +6060,2,299000190,8 +6061,2,299000200,8 +6062,2,299000210,8 +6063,3,298000050,8 +6064,3,298000060,8 +6065,3,299000060,8 +6066,3,299000170,8 +6067,5,297000100,8 +6068,5,291000020,8 +6069,4,140000,3 +6070,5,150010,3 +6071,5,150020,3 +6072,5,150030,3 +6073,5,150040,3 +6075,3,101000050,1 +6076,3,101000060,1 +6077,3,101000080,1 +6078,3,101000040,1 +6079,3,109000060,1 +6080,3,109000070,1 +6081,3,109000080,1 +6082,3,105000060,1 +6083,3,105000070,1 +6084,3,105000080,1 +6085,3,104000050,1 +6086,3,106000050,1 +6087,3,102000060,1 +6088,3,102000070,1 +6089,3,102000080,1 +6090,3,103000050,1 +6091,3,105000050,1 +6092,3,107000060,1 +6093,3,107000070,1 +6094,3,107000080,1 +6095,3,108000050,1 +6096,3,109000050,1 +6097,3,103000060,1 +6098,3,103000070,1 +6099,3,103000080,1 +6100,3,110000050,1 +6101,3,106000060,1 +6102,3,106000070,1 +6103,3,106000080,1 +6104,3,101000070,1 +6105,3,110000060,1 +6106,3,104000060,1 +6107,3,104000070,1 +6108,3,104000080,1 +6109,3,102000050,1 +6110,3,104000170,1 +6111,3,104000260,1 +6112,3,111000010,1 +6113,3,111000020,1 +6114,3,111000030,1 +6115,3,112000020,1 +6116,3,112000030,1 +6117,3,108000060,1 +6118,3,108000070,1 +6119,3,108000080,1 +6120,3,107000050,1 +6121,3,112000010,1 +6122,3,110000070,1 +6123,3,110000080,1 +6124,4,101000090,1 +6125,4,101000100,1 +6126,4,101000110,1 +6127,4,109000100,1 +6128,4,105000100,1 +6129,4,105000110,1 +6130,4,108000090,1 +6131,4,110000090,1 +6132,4,102000100,1 +6133,4,102000110,1 +6134,4,106000090,1 +6135,4,109000090,1 +6136,4,107000100,1 +6137,4,103000090,1 +6138,4,102000090,1 +6139,4,103000100,1 +6140,4,106000100,1 +6141,4,106000110,1 +6142,4,104000090,1 +6143,4,104000100,1 +6144,4,104000110,1 +6145,4,107000090,1 +6146,4,104000180,1 +6147,4,111000040,1 +6148,4,112000040,1 +6149,4,108000100,1 +6150,4,105000090,1 +6151,4,110000100,1 +6152,5,101000120,1 +6153,5,109000110,1 +6154,5,105000120,1 +6155,5,102000120,1 +6156,5,107000110,1 +6157,5,103000120,1 +6158,5,106000120,1 +6159,5,104000120,1 +6160,5,104000190,1 +6161,5,111000060,1 +6162,5,112000060,1 +6163,5,108000110,1 +6164,5,110000110,1 +6165,3,170004,3 +6166,3,180004,3 +6167,1,201000010,8 +6168,1,292000010,8 +6169,1,299000040,8 +6170,1,299000070,8 +6171,1,299000110,8 +6172,1,299000120,8 +6173,1,299000140,8 +6174,2,202000010,8 +6175,2,290000010,8 +6176,2,299000010,8 +6177,2,299000150,8 +6178,2,299000190,8 +6179,2,299000200,8 +6180,2,299000210,8 +6181,3,298000050,8 +6182,3,298000060,8 +6183,3,299000060,8 +6184,3,299000170,8 +6185,5,297000100,8 +6186,5,291000020,8 +6187,4,140000,3 +6188,5,150010,3 +6189,5,150020,3 +6190,5,150030,3 +6191,5,150040,3 +6192,5,190000,3 +6193,5,200000,3 +6194,5,210000,3 +6196,3,111000010,1 +6197,3,111000020,1 +6198,3,111000030,1 +6199,3,112000020,1 +6200,3,112000030,1 +6201,3,108000060,1 +6202,3,108000070,1 +6203,3,108000080,1 +6204,3,107000050,1 +6205,3,112000010,1 +6206,3,110000070,1 +6207,3,110000080,1 +6208,4,111000040,1 +6209,4,112000040,1 +6210,4,108000100,1 +6211,4,105000090,1 +6212,4,110000100,1 +6213,5,111000060,1 +6214,5,112000060,1 +6215,5,108000110,1 +6216,5,110000110,1 +6217,1,108000000,2 +6218,2,108000001,2 +6219,3,108000002,2 +6220,4,108000003,2 +6221,5,108000004,2 +6222,1,107000000,2 +6223,2,107000001,2 +6224,3,107000002,2 +6225,4,107000003,2 +6226,5,107000004,2 +6227,1,120000000,2 +6228,2,120000001,2 +6229,3,120000002,2 +6230,4,120000003,2 +6231,5,120000004,2 +6232,3,170004,3 +6233,4,170005,3 +6234,3,180004,3 +6235,4,180005,3 +6236,4,140000,3 +6237,4,150010,3 +6238,4,150020,3 +6239,4,150030,3 +6240,4,150040,3 +6242,3,101000050,1 +6243,3,101000060,1 +6244,3,101000080,1 +6245,3,101000040,1 +6246,3,109000060,1 +6247,3,109000070,1 +6248,3,109000080,1 +6249,3,105000060,1 +6250,3,105000070,1 +6251,3,105000080,1 +6252,3,104000050,1 +6253,3,106000050,1 +6254,4,101000090,1 +6255,4,101000100,1 +6256,4,101000110,1 +6257,4,109000100,1 +6258,4,105000100,1 +6259,4,105000110,1 +6260,4,108000090,1 +6261,4,110000090,1 +6262,5,101000120,1 +6263,5,109000110,1 +6264,5,105000120,1 +6265,1,101000000,2 +6266,2,101000001,2 +6267,3,101000002,2 +6268,4,101000008,2 +6269,5,101000004,2 +6270,1,109000000,2 +6271,2,109000001,2 +6272,3,109000002,2 +6273,4,109000003,2 +6274,5,109000004,2 +6275,4,110024,3 +6276,4,110034,3 +6277,4,110044,3 +6278,4,110054,3 +6279,3,110060,3 +6280,3,110070,3 +6281,3,110080,3 +6282,3,110090,3 +6283,3,110100,3 +6284,3,110110,3 +6285,3,110120,3 +6286,3,110130,3 +6287,3,110140,3 +6288,3,110150,3 +6289,3,110160,3 +6290,3,110170,3 +6291,3,110180,3 +6292,3,110190,3 +6293,3,110200,3 +6294,3,110210,3 +6295,3,110220,3 +6296,3,110230,3 +6297,3,110240,3 +6298,3,110250,3 +6299,3,110260,3 +6300,3,110270,3 +6301,3,110620,3 +6302,3,110670,3 +6303,4,140000,3 +6304,4,150010,3 +6305,4,150020,3 +6306,4,150030,3 +6307,4,150040,3 +6309,3,102000060,1 +6310,3,102000070,1 +6311,3,102000080,1 +6312,3,103000050,1 +6313,3,105000050,1 +6314,3,107000060,1 +6315,3,107000070,1 +6316,3,107000080,1 +6317,3,108000050,1 +6318,3,109000050,1 +6319,3,103000060,1 +6320,3,103000070,1 +6321,3,103000080,1 +6322,3,110000050,1 +6323,4,102000100,1 +6324,4,102000110,1 +6325,4,106000090,1 +6326,4,109000090,1 +6327,4,107000100,1 +6328,4,103000090,1 +6329,4,102000090,1 +6330,4,103000100,1 +6331,5,102000120,1 +6332,5,107000110,1 +6333,5,103000120,1 +6334,1,102000000,2 +6335,2,102000001,2 +6336,3,102000002,2 +6337,4,102000003,2 +6338,5,102000004,2 +6339,1,105000000,2 +6340,2,105000001,2 +6341,3,105000002,2 +6342,4,105000003,2 +6343,5,105000004,2 +6344,1,112000000,2 +6345,2,112000001,2 +6346,3,112000002,2 +6347,4,112000003,2 +6348,5,112000004,2 +6349,4,120024,3 +6350,4,120034,3 +6351,4,120044,3 +6352,4,120054,3 +6353,3,120241,3 +6354,3,120251,3 +6355,3,120261,3 +6356,3,120271,3 +6357,3,120300,3 +6358,3,120310,3 +6359,3,120320,3 +6360,3,120330,3 +6361,3,120340,3 +6362,3,120350,3 +6363,3,120360,3 +6364,3,120370,3 +6365,3,120380,3 +6366,3,120390,3 +6367,3,120400,3 +6368,3,120410,3 +6369,3,120420,3 +6370,3,120430,3 +6371,3,120450,3 +6372,3,120460,3 +6373,3,120550,3 +6374,3,120560,3 +6375,3,120570,3 +6376,3,120990,3 +6377,3,121000,3 +6378,3,121010,3 +6379,3,121020,3 +6380,4,140000,3 +6381,4,150010,3 +6382,4,150020,3 +6383,4,150030,3 +6384,4,150040,3 +6386,3,106000060,1 +6387,3,106000070,1 +6388,3,106000080,1 +6389,3,101000070,1 +6390,3,110000060,1 +6391,3,104000060,1 +6392,3,104000070,1 +6393,3,104000080,1 +6394,3,102000050,1 +6395,3,104000170,1 +6396,3,104000260,1 +6397,4,106000100,1 +6398,4,106000110,1 +6399,4,104000090,1 +6400,4,104000100,1 +6401,4,104000110,1 +6402,4,107000090,1 +6403,4,104000180,1 +6404,5,106000120,1 +6405,5,104000120,1 +6406,5,104000190,1 +6407,1,103000000,2 +6408,2,103000001,2 +6409,3,103000002,2 +6410,4,103000003,2 +6411,5,103000004,2 +6412,1,111000000,2 +6413,2,111000001,2 +6414,3,111000002,2 +6415,4,111000003,2 +6416,5,111000004,2 +6417,1,115000000,2 +6418,2,115000001,2 +6419,3,115000002,2 +6420,4,115000003,2 +6421,5,115000004,2 +6422,4,130024,3 +6423,4,130034,3 +6424,4,130044,3 +6425,4,130054,3 +6426,3,130060,3 +6427,3,130070,3 +6428,3,130080,3 +6429,3,130090,3 +6430,3,130100,3 +6431,3,130110,3 +6432,3,130120,3 +6433,3,130130,3 +6434,3,130140,3 +6435,3,130150,3 +6436,3,130160,3 +6437,3,130170,3 +6438,3,130180,3 +6439,3,130190,3 +6440,3,130200,3 +6441,3,130420,3 +6442,3,130510,3 +6443,3,130520,3 +6444,3,130530,3 +6445,3,130540,3 +6446,3,130660,3 +6447,3,130790,3 +6448,3,130800,3 +6449,4,140000,3 +6450,4,150010,3 +6451,4,150020,3 +6452,4,150030,3 +6453,4,150040,3 +6455,1,101000010,1 +6456,1,102000010,1 +6457,1,103000010,1 +6458,1,104000010,1 +6459,1,105000010,1 +6460,1,106000010,1 +6461,1,107000010,1 +6462,1,108000010,1 +6463,1,109000010,1 +6464,1,110000010,1 +6465,2,101000020,1 +6466,2,101000030,1 +6467,2,102000020,1 +6468,2,102000030,1 +6469,2,102000040,1 +6470,2,103000020,1 +6471,2,103000030,1 +6472,2,103000040,1 +6473,2,104000020,1 +6474,2,104000030,1 +6475,2,104000040,1 +6476,2,105000020,1 +6477,2,105000030,1 +6478,2,105000040,1 +6479,2,106000020,1 +6480,2,106000030,1 +6481,2,106000040,1 +6482,2,107000020,1 +6483,2,107000030,1 +6484,2,107000040,1 +6485,2,108000020,1 +6486,2,108000030,1 +6487,2,108000040,1 +6488,2,109000020,1 +6489,2,109000030,1 +6490,2,109000040,1 +6491,2,110000020,1 +6492,2,110000030,1 +6493,2,110000040,1 +6494,2,118000010,1 +6495,3,101000050,1 +6496,3,101000060,1 +6497,3,101000080,1 +6498,3,101000040,1 +6499,3,109000060,1 +6500,3,109000070,1 +6501,3,109000080,1 +6502,3,105000060,1 +6503,3,105000070,1 +6504,3,105000080,1 +6505,3,104000050,1 +6506,3,106000050,1 +6507,3,102000060,1 +6508,3,102000070,1 +6509,3,102000080,1 +6510,3,103000050,1 +6511,3,105000050,1 +6512,3,107000060,1 +6513,3,107000070,1 +6514,3,107000080,1 +6515,3,108000050,1 +6516,3,109000050,1 +6517,3,103000060,1 +6518,3,103000070,1 +6519,3,103000080,1 +6520,3,110000050,1 +6521,3,106000060,1 +6522,3,106000070,1 +6523,3,106000080,1 +6524,3,101000070,1 +6525,3,110000060,1 +6526,3,104000060,1 +6527,3,104000070,1 +6528,3,104000080,1 +6529,3,102000050,1 +6530,3,104000170,1 +6531,3,104000260,1 +6532,3,111000010,1 +6533,3,111000020,1 +6534,3,111000030,1 +6535,3,112000020,1 +6536,3,112000030,1 +6537,3,108000060,1 +6538,3,108000070,1 +6539,3,108000080,1 +6540,3,107000050,1 +6541,3,112000010,1 +6542,3,110000070,1 +6543,3,110000080,1 +6544,3,118000020,1 +6545,3,118000030,1 +6546,3,118000040,1 +6547,4,101000090,1 +6548,4,101000100,1 +6549,4,101000110,1 +6550,4,109000100,1 +6551,4,105000100,1 +6552,4,105000110,1 +6553,4,108000090,1 +6554,4,110000090,1 +6555,4,102000100,1 +6556,4,102000110,1 +6557,4,106000090,1 +6558,4,109000090,1 +6559,4,107000100,1 +6560,4,103000090,1 +6561,4,102000090,1 +6562,4,103000100,1 +6563,4,106000100,1 +6564,4,106000110,1 +6565,4,104000090,1 +6566,4,104000100,1 +6567,4,104000110,1 +6568,4,107000090,1 +6569,4,104000180,1 +6570,4,111000040,1 +6571,4,112000040,1 +6572,4,108000100,1 +6573,4,105000090,1 +6574,4,110000100,1 +6575,4,118000050,1 +6576,4,118000060,1 +6577,5,101000120,1 +6578,5,109000110,1 +6579,5,105000120,1 +6580,5,102000120,1 +6581,5,107000110,1 +6582,5,103000120,1 +6583,5,106000120,1 +6584,5,104000120,1 +6585,5,104000190,1 +6586,5,111000060,1 +6587,5,112000060,1 +6588,5,108000110,1 +6589,5,110000110,1 +6590,5,118000070,1 +6591,1,170002,3 +6592,2,170003,3 +6593,3,170004,3 +6594,1,180002,3 +6595,2,180003,3 +6596,3,180004,3 +6597,1,201000010,8 +6598,1,292000010,8 +6599,1,299000040,8 +6600,1,299000070,8 +6601,1,299000110,8 +6602,1,299000120,8 +6603,1,299000140,8 +6604,2,202000010,8 +6605,2,290000010,8 +6606,2,299000010,8 +6607,2,299000150,8 +6608,2,299000190,8 +6609,2,299000200,8 +6610,2,299000210,8 +6611,3,298000050,8 +6612,3,298000060,8 +6613,3,299000060,8 +6614,3,299000170,8 +6615,5,297000100,8 +6616,5,291000020,8 +6617,4,140000,3 +6618,5,150010,3 +6619,5,150020,3 +6620,5,150030,3 +6621,5,150040,3 +6623,1,101000010,1 +6624,1,102000010,1 +6625,1,103000010,1 +6626,1,104000010,1 +6627,1,105000010,1 +6628,1,106000010,1 +6629,1,107000010,1 +6630,1,108000010,1 +6631,1,109000010,1 +6632,1,110000010,1 +6633,2,101000020,1 +6634,2,101000030,1 +6635,2,102000020,1 +6636,2,102000030,1 +6637,2,102000040,1 +6638,2,103000020,1 +6639,2,103000030,1 +6640,2,103000040,1 +6641,2,104000020,1 +6642,2,104000030,1 +6643,2,104000040,1 +6644,2,105000020,1 +6645,2,105000030,1 +6646,2,105000040,1 +6647,2,106000020,1 +6648,2,106000030,1 +6649,2,106000040,1 +6650,2,107000020,1 +6651,2,107000030,1 +6652,2,107000040,1 +6653,2,108000020,1 +6654,2,108000030,1 +6655,2,108000040,1 +6656,2,109000020,1 +6657,2,109000030,1 +6658,2,109000040,1 +6659,2,110000020,1 +6660,2,110000030,1 +6661,2,110000040,1 +6662,2,118000010,1 +6663,3,101000050,1 +6664,3,101000060,1 +6665,3,101000080,1 +6666,3,101000040,1 +6667,3,109000060,1 +6668,3,109000070,1 +6669,3,109000080,1 +6670,3,105000060,1 +6671,3,105000070,1 +6672,3,105000080,1 +6673,3,104000050,1 +6674,3,106000050,1 +6675,3,102000060,1 +6676,3,102000070,1 +6677,3,102000080,1 +6678,3,103000050,1 +6679,3,105000050,1 +6680,3,107000060,1 +6681,3,107000070,1 +6682,3,107000080,1 +6683,3,108000050,1 +6684,3,109000050,1 +6685,3,103000060,1 +6686,3,103000070,1 +6687,3,103000080,1 +6688,3,110000050,1 +6689,3,106000060,1 +6690,3,106000070,1 +6691,3,106000080,1 +6692,3,101000070,1 +6693,3,110000060,1 +6694,3,104000060,1 +6695,3,104000070,1 +6696,3,104000080,1 +6697,3,102000050,1 +6698,3,104000170,1 +6699,3,104000260,1 +6700,3,111000010,1 +6701,3,111000020,1 +6702,3,111000030,1 +6703,3,112000020,1 +6704,3,112000030,1 +6705,3,108000060,1 +6706,3,108000070,1 +6707,3,108000080,1 +6708,3,107000050,1 +6709,3,112000010,1 +6710,3,110000070,1 +6711,3,110000080,1 +6712,3,118000020,1 +6713,3,118000030,1 +6714,3,118000040,1 +6715,4,101000090,1 +6716,4,101000100,1 +6717,4,101000110,1 +6718,4,109000100,1 +6719,4,105000100,1 +6720,4,105000110,1 +6721,4,108000090,1 +6722,4,110000090,1 +6723,4,102000100,1 +6724,4,102000110,1 +6725,4,106000090,1 +6726,4,109000090,1 +6727,4,107000100,1 +6728,4,103000090,1 +6729,4,102000090,1 +6730,4,103000100,1 +6731,4,106000100,1 +6732,4,106000110,1 +6733,4,104000090,1 +6734,4,104000100,1 +6735,4,104000110,1 +6736,4,107000090,1 +6737,4,104000180,1 +6738,4,111000040,1 +6739,4,112000040,1 +6740,4,108000100,1 +6741,4,105000090,1 +6742,4,110000100,1 +6743,4,118000050,1 +6744,4,118000060,1 +6745,5,101000120,1 +6746,5,109000110,1 +6747,5,105000120,1 +6748,5,102000120,1 +6749,5,107000110,1 +6750,5,103000120,1 +6751,5,106000120,1 +6752,5,104000120,1 +6753,5,104000190,1 +6754,5,111000060,1 +6755,5,112000060,1 +6756,5,108000110,1 +6757,5,110000110,1 +6758,5,118000070,1 +6759,2,170003,3 +6760,3,170004,3 +6761,2,180003,3 +6762,3,180004,3 +6763,1,201000010,8 +6764,1,292000010,8 +6765,1,299000040,8 +6766,1,299000070,8 +6767,1,299000110,8 +6768,1,299000120,8 +6769,1,299000140,8 +6770,2,202000010,8 +6771,2,290000010,8 +6772,2,299000010,8 +6773,2,299000150,8 +6774,2,299000190,8 +6775,2,299000200,8 +6776,2,299000210,8 +6777,3,298000050,8 +6778,3,298000060,8 +6779,3,299000060,8 +6780,3,299000170,8 +6781,5,297000100,8 +6782,5,291000020,8 +6783,4,140000,3 +6784,5,150010,3 +6785,5,150020,3 +6786,5,150030,3 +6787,5,150040,3 +6789,3,101000050,1 +6790,3,101000060,1 +6791,3,101000080,1 +6792,3,101000040,1 +6793,3,109000060,1 +6794,3,109000070,1 +6795,3,109000080,1 +6796,3,105000060,1 +6797,3,105000070,1 +6798,3,105000080,1 +6799,3,104000050,1 +6800,3,106000050,1 +6801,3,102000060,1 +6802,3,102000070,1 +6803,3,102000080,1 +6804,3,103000050,1 +6805,3,105000050,1 +6806,3,107000060,1 +6807,3,107000070,1 +6808,3,107000080,1 +6809,3,108000050,1 +6810,3,109000050,1 +6811,3,103000060,1 +6812,3,103000070,1 +6813,3,103000080,1 +6814,3,110000050,1 +6815,3,106000060,1 +6816,3,106000070,1 +6817,3,106000080,1 +6818,3,101000070,1 +6819,3,110000060,1 +6820,3,104000060,1 +6821,3,104000070,1 +6822,3,104000080,1 +6823,3,102000050,1 +6824,3,104000170,1 +6825,3,104000260,1 +6826,3,111000010,1 +6827,3,111000020,1 +6828,3,111000030,1 +6829,3,112000020,1 +6830,3,112000030,1 +6831,3,108000060,1 +6832,3,108000070,1 +6833,3,108000080,1 +6834,3,107000050,1 +6835,3,112000010,1 +6836,3,110000070,1 +6837,3,110000080,1 +6838,3,118000020,1 +6839,3,118000030,1 +6840,3,118000040,1 +6841,4,101000090,1 +6842,4,101000100,1 +6843,4,101000110,1 +6844,4,109000100,1 +6845,4,105000100,1 +6846,4,105000110,1 +6847,4,108000090,1 +6848,4,110000090,1 +6849,4,102000100,1 +6850,4,102000110,1 +6851,4,106000090,1 +6852,4,109000090,1 +6853,4,107000100,1 +6854,4,103000090,1 +6855,4,102000090,1 +6856,4,103000100,1 +6857,4,106000100,1 +6858,4,106000110,1 +6859,4,104000090,1 +6860,4,104000100,1 +6861,4,104000110,1 +6862,4,107000090,1 +6863,4,104000180,1 +6864,4,111000040,1 +6865,4,112000040,1 +6866,4,108000100,1 +6867,4,105000090,1 +6868,4,110000100,1 +6869,4,118000050,1 +6870,4,118000060,1 +6871,5,101000120,1 +6872,5,109000110,1 +6873,5,105000120,1 +6874,5,102000120,1 +6875,5,107000110,1 +6876,5,103000120,1 +6877,5,106000120,1 +6878,5,104000120,1 +6879,5,104000190,1 +6880,5,111000060,1 +6881,5,112000060,1 +6882,5,108000110,1 +6883,5,110000110,1 +6884,5,118000070,1 +6885,3,170004,3 +6886,3,180004,3 +6887,1,201000010,8 +6888,1,292000010,8 +6889,1,299000040,8 +6890,1,299000070,8 +6891,1,299000110,8 +6892,1,299000120,8 +6893,1,299000140,8 +6894,2,202000010,8 +6895,2,290000010,8 +6896,2,299000010,8 +6897,2,299000150,8 +6898,2,299000190,8 +6899,2,299000200,8 +6900,2,299000210,8 +6901,3,298000050,8 +6902,3,298000060,8 +6903,3,299000060,8 +6904,3,299000170,8 +6905,5,297000100,8 +6906,5,291000020,8 +6907,4,140000,3 +6908,5,150010,3 +6909,5,150020,3 +6910,5,150030,3 +6911,5,150040,3 +6913,3,101000050,1 +6914,3,101000060,1 +6915,3,101000080,1 +6916,3,101000040,1 +6917,3,109000060,1 +6918,3,109000070,1 +6919,3,109000080,1 +6920,3,105000060,1 +6921,3,105000070,1 +6922,3,105000080,1 +6923,3,104000050,1 +6924,3,106000050,1 +6925,3,102000060,1 +6926,3,102000070,1 +6927,3,102000080,1 +6928,3,103000050,1 +6929,3,105000050,1 +6930,3,107000060,1 +6931,3,107000070,1 +6932,3,107000080,1 +6933,3,108000050,1 +6934,3,109000050,1 +6935,3,103000060,1 +6936,3,103000070,1 +6937,3,103000080,1 +6938,3,110000050,1 +6939,3,106000060,1 +6940,3,106000070,1 +6941,3,106000080,1 +6942,3,101000070,1 +6943,3,110000060,1 +6944,3,104000060,1 +6945,3,104000070,1 +6946,3,104000080,1 +6947,3,102000050,1 +6948,3,104000170,1 +6949,3,104000260,1 +6950,3,111000010,1 +6951,3,111000020,1 +6952,3,111000030,1 +6953,3,112000020,1 +6954,3,112000030,1 +6955,3,108000060,1 +6956,3,108000070,1 +6957,3,108000080,1 +6958,3,107000050,1 +6959,3,112000010,1 +6960,3,110000070,1 +6961,3,110000080,1 +6962,3,118000020,1 +6963,3,118000030,1 +6964,3,118000040,1 +6965,4,101000090,1 +6966,4,101000100,1 +6967,4,101000110,1 +6968,4,109000100,1 +6969,4,105000100,1 +6970,4,105000110,1 +6971,4,108000090,1 +6972,4,110000090,1 +6973,4,102000100,1 +6974,4,102000110,1 +6975,4,106000090,1 +6976,4,109000090,1 +6977,4,107000100,1 +6978,4,103000090,1 +6979,4,102000090,1 +6980,4,103000100,1 +6981,4,106000100,1 +6982,4,106000110,1 +6983,4,104000090,1 +6984,4,104000100,1 +6985,4,104000110,1 +6986,4,107000090,1 +6987,4,104000180,1 +6988,4,111000040,1 +6989,4,112000040,1 +6990,4,108000100,1 +6991,4,105000090,1 +6992,4,110000100,1 +6993,4,118000050,1 +6994,4,118000060,1 +6995,5,101000120,1 +6996,5,109000110,1 +6997,5,105000120,1 +6998,5,102000120,1 +6999,5,107000110,1 +7000,5,103000120,1 +7001,5,106000120,1 +7002,5,104000120,1 +7003,5,104000190,1 +7004,5,111000060,1 +7005,5,112000060,1 +7006,5,108000110,1 +7007,5,110000110,1 +7008,5,118000070,1 +7009,3,170004,3 +7010,3,180004,3 +7011,1,201000010,8 +7012,1,292000010,8 +7013,1,299000040,8 +7014,1,299000070,8 +7015,1,299000110,8 +7016,1,299000120,8 +7017,1,299000140,8 +7018,2,202000010,8 +7019,2,290000010,8 +7020,2,299000010,8 +7021,2,299000150,8 +7022,2,299000190,8 +7023,2,299000200,8 +7024,2,299000210,8 +7025,3,298000050,8 +7026,3,298000060,8 +7027,3,299000060,8 +7028,3,299000170,8 +7029,5,297000100,8 +7030,5,291000020,8 +7031,4,140000,3 +7032,5,150010,3 +7033,5,150020,3 +7034,5,150030,3 +7035,5,150040,3 +7037,3,101000050,1 +7038,3,101000060,1 +7039,3,101000080,1 +7040,3,101000040,1 +7041,3,109000060,1 +7042,3,109000070,1 +7043,3,109000080,1 +7044,3,105000060,1 +7045,3,105000070,1 +7046,3,105000080,1 +7047,3,104000050,1 +7048,3,106000050,1 +7049,3,102000060,1 +7050,3,102000070,1 +7051,3,102000080,1 +7052,3,103000050,1 +7053,3,105000050,1 +7054,3,107000060,1 +7055,3,107000070,1 +7056,3,107000080,1 +7057,3,108000050,1 +7058,3,109000050,1 +7059,3,103000060,1 +7060,3,103000070,1 +7061,3,103000080,1 +7062,3,110000050,1 +7063,3,106000060,1 +7064,3,106000070,1 +7065,3,106000080,1 +7066,3,101000070,1 +7067,3,110000060,1 +7068,3,104000060,1 +7069,3,104000070,1 +7070,3,104000080,1 +7071,3,102000050,1 +7072,3,104000170,1 +7073,3,104000260,1 +7074,3,111000010,1 +7075,3,111000020,1 +7076,3,111000030,1 +7077,3,112000020,1 +7078,3,112000030,1 +7079,3,108000060,1 +7080,3,108000070,1 +7081,3,108000080,1 +7082,3,107000050,1 +7083,3,112000010,1 +7084,3,110000070,1 +7085,3,110000080,1 +7086,3,118000020,1 +7087,3,118000030,1 +7088,3,118000040,1 +7089,4,101000090,1 +7090,4,101000100,1 +7091,4,101000110,1 +7092,4,109000100,1 +7093,4,105000100,1 +7094,4,105000110,1 +7095,4,108000090,1 +7096,4,110000090,1 +7097,4,102000100,1 +7098,4,102000110,1 +7099,4,106000090,1 +7100,4,109000090,1 +7101,4,107000100,1 +7102,4,103000090,1 +7103,4,102000090,1 +7104,4,103000100,1 +7105,4,106000100,1 +7106,4,106000110,1 +7107,4,104000090,1 +7108,4,104000100,1 +7109,4,104000110,1 +7110,4,107000090,1 +7111,4,104000180,1 +7112,4,111000040,1 +7113,4,112000040,1 +7114,4,108000100,1 +7115,4,105000090,1 +7116,4,110000100,1 +7117,4,118000050,1 +7118,4,118000060,1 +7119,5,101000120,1 +7120,5,109000110,1 +7121,5,105000120,1 +7122,5,102000120,1 +7123,5,107000110,1 +7124,5,103000120,1 +7125,5,106000120,1 +7126,5,104000120,1 +7127,5,104000190,1 +7128,5,111000060,1 +7129,5,112000060,1 +7130,5,108000110,1 +7131,5,110000110,1 +7132,5,118000070,1 +7133,3,170004,3 +7134,3,180004,3 +7135,1,201000010,8 +7136,1,292000010,8 +7137,1,299000040,8 +7138,1,299000070,8 +7139,1,299000110,8 +7140,1,299000120,8 +7141,1,299000140,8 +7142,2,202000010,8 +7143,2,290000010,8 +7144,2,299000010,8 +7145,2,299000150,8 +7146,2,299000190,8 +7147,2,299000200,8 +7148,2,299000210,8 +7149,3,298000050,8 +7150,3,298000060,8 +7151,3,299000060,8 +7152,3,299000170,8 +7153,5,297000100,8 +7154,5,291000020,8 +7155,4,140000,3 +7156,5,150010,3 +7157,5,150020,3 +7158,5,150030,3 +7159,5,150040,3 +7160,5,190000,3 +7161,5,200000,3 +7162,5,210000,3 +7164,3,118000020,1 +7165,3,118000030,1 +7166,3,118000040,1 +7167,3,106000060,1 +7168,3,106000070,1 +7169,3,106000080,1 +7170,3,101000070,1 +7171,3,110000060,1 +7172,3,104000060,1 +7173,3,104000070,1 +7174,3,104000080,1 +7175,3,102000050,1 +7176,3,104000170,1 +7177,3,104000260,1 +7178,4,118000050,1 +7179,4,118000060,1 +7180,4,106000100,1 +7181,4,106000110,1 +7182,4,104000090,1 +7183,4,104000100,1 +7184,4,104000110,1 +7185,4,104000270,1 +7186,4,107000090,1 +7187,4,104000180,1 +7188,5,118000070,1 +7189,5,106000120,1 +7190,5,104000120,1 +7191,5,104000190,1 +7192,1,103000000,2 +7193,2,103000001,2 +7194,3,103000002,2 +7195,4,103000003,2 +7196,5,103000004,2 +7197,1,111000000,2 +7198,2,111000001,2 +7199,3,111000002,2 +7200,4,111000003,2 +7201,5,111000004,2 +7202,1,115000000,2 +7203,2,115000001,2 +7204,3,115000002,2 +7205,4,115000003,2 +7206,5,115000004,2 +7207,3,170004,3 +7208,4,170005,3 +7209,3,180004,3 +7210,4,180005,3 +7211,4,140000,3 +7212,4,150010,3 +7213,4,150020,3 +7214,4,150030,3 +7215,4,150040,3 +7217,3,111000010,1 +7218,3,111000020,1 +7219,3,111000030,1 +7220,3,112000020,1 +7221,3,112000030,1 +7222,3,108000060,1 +7223,3,108000070,1 +7224,3,108000080,1 +7225,3,107000050,1 +7226,3,112000010,1 +7227,3,110000070,1 +7228,3,110000080,1 +7229,4,111000040,1 +7230,4,112000040,1 +7231,4,108000100,1 +7232,4,105000090,1 +7233,4,110000100,1 +7234,5,111000060,1 +7235,5,112000060,1 +7236,5,108000110,1 +7237,5,110000110,1 +7238,1,108000000,2 +7239,2,108000001,2 +7240,3,108000002,2 +7241,4,108000003,2 +7242,5,108000004,2 +7243,1,107000000,2 +7244,2,107000001,2 +7245,3,107000002,2 +7246,4,107000003,2 +7247,5,107000004,2 +7248,1,120000000,2 +7249,2,120000001,2 +7250,3,120000002,2 +7251,4,120000003,2 +7252,5,120000004,2 +7253,4,110024,3 +7254,4,110034,3 +7255,4,110044,3 +7256,4,110054,3 +7257,3,110060,3 +7258,3,110070,3 +7259,3,110080,3 +7260,3,110090,3 +7261,3,110100,3 +7262,3,110110,3 +7263,3,110120,3 +7264,3,110130,3 +7265,3,110140,3 +7266,3,110150,3 +7267,3,110160,3 +7268,3,110170,3 +7269,3,110180,3 +7270,3,110190,3 +7271,3,110200,3 +7272,3,110210,3 +7273,3,110220,3 +7274,3,110230,3 +7275,3,110240,3 +7276,3,110250,3 +7277,3,110260,3 +7278,3,110270,3 +7279,3,110620,3 +7280,3,110670,3 +7281,4,140000,3 +7282,4,150010,3 +7283,4,150020,3 +7284,4,150030,3 +7285,4,150040,3 +7287,3,101000050,1 +7288,3,101000060,1 +7289,3,101000080,1 +7290,3,101000040,1 +7291,3,109000060,1 +7292,3,109000070,1 +7293,3,109000080,1 +7294,3,105000060,1 +7295,3,105000070,1 +7296,3,105000080,1 +7297,3,104000050,1 +7298,3,106000050,1 +7299,4,101000090,1 +7300,4,101000100,1 +7301,4,101000110,1 +7302,4,109000100,1 +7303,4,105000100,1 +7304,4,105000110,1 +7305,4,108000090,1 +7306,4,110000090,1 +7307,5,101000120,1 +7308,5,109000110,1 +7309,5,105000120,1 +7310,1,101000000,2 +7311,2,101000001,2 +7312,3,101000002,2 +7313,4,101000008,2 +7314,5,101000004,2 +7315,1,109000000,2 +7316,2,109000001,2 +7317,3,109000002,2 +7318,4,109000003,2 +7319,5,109000004,2 +7320,4,120024,3 +7321,4,120034,3 +7322,4,120044,3 +7323,4,120054,3 +7324,3,120241,3 +7325,3,120251,3 +7326,3,120261,3 +7327,3,120271,3 +7328,3,120300,3 +7329,3,120310,3 +7330,3,120320,3 +7331,3,120330,3 +7332,3,120340,3 +7333,3,120350,3 +7334,3,120360,3 +7335,3,120370,3 +7336,3,120380,3 +7337,3,120390,3 +7338,3,120400,3 +7339,3,120410,3 +7340,3,120420,3 +7341,3,120430,3 +7342,3,120450,3 +7343,3,120460,3 +7344,3,120550,3 +7345,3,120560,3 +7346,3,120570,3 +7347,3,120990,3 +7348,3,121000,3 +7349,3,121010,3 +7350,3,121020,3 +7351,4,140000,3 +7352,4,150010,3 +7353,4,150020,3 +7354,4,150030,3 +7355,4,150040,3 +7357,3,102000060,1 +7358,3,102000070,1 +7359,3,102000080,1 +7360,3,103000050,1 +7361,3,105000050,1 +7362,3,107000060,1 +7363,3,107000070,1 +7364,3,107000080,1 +7365,3,108000050,1 +7366,3,109000050,1 +7367,3,103000060,1 +7368,3,103000070,1 +7369,3,103000080,1 +7370,3,110000050,1 +7371,4,102000100,1 +7372,4,102000110,1 +7373,4,106000090,1 +7374,4,109000090,1 +7375,4,107000100,1 +7376,4,103000090,1 +7377,4,102000090,1 +7378,4,103000100,1 +7379,5,102000120,1 +7380,5,107000110,1 +7381,5,103000120,1 +7382,1,102000000,2 +7383,2,102000001,2 +7384,3,102000002,2 +7385,4,102000003,2 +7386,5,102000004,2 +7387,1,105000000,2 +7388,2,105000001,2 +7389,3,105000002,2 +7390,4,105000003,2 +7391,5,105000004,2 +7392,1,112000000,2 +7393,2,112000001,2 +7394,3,112000002,2 +7395,4,112000003,2 +7396,5,112000004,2 +7397,4,130024,3 +7398,4,130034,3 +7399,4,130044,3 +7400,4,130054,3 +7401,3,130060,3 +7402,3,130070,3 +7403,3,130080,3 +7404,3,130090,3 +7405,3,130100,3 +7406,3,130110,3 +7407,3,130120,3 +7408,3,130130,3 +7409,3,130140,3 +7410,3,130150,3 +7411,3,130160,3 +7412,3,130170,3 +7413,3,130180,3 +7414,3,130190,3 +7415,3,130200,3 +7416,3,130420,3 +7417,3,130510,3 +7418,3,130520,3 +7419,3,130530,3 +7420,3,130540,3 +7421,3,130660,3 +7422,3,130790,3 +7423,3,130800,3 +7424,4,140000,3 +7425,4,150010,3 +7426,4,150020,3 +7427,4,150030,3 +7428,4,150040,3 +7430,1,101000010,1 +7431,1,102000010,1 +7432,1,103000010,1 +7433,1,104000010,1 +7434,1,105000010,1 +7435,1,106000010,1 +7436,1,107000010,1 +7437,1,108000010,1 +7438,1,109000010,1 +7439,1,110000010,1 +7440,2,101000020,1 +7441,2,101000030,1 +7442,2,102000020,1 +7443,2,102000030,1 +7444,2,102000040,1 +7445,2,103000020,1 +7446,2,103000030,1 +7447,2,103000040,1 +7448,2,104000020,1 +7449,2,104000030,1 +7450,2,104000040,1 +7451,2,105000020,1 +7452,2,105000030,1 +7453,2,105000040,1 +7454,2,106000020,1 +7455,2,106000030,1 +7456,2,106000040,1 +7457,2,107000020,1 +7458,2,107000030,1 +7459,2,107000040,1 +7460,2,108000020,1 +7461,2,108000030,1 +7462,2,108000040,1 +7463,2,109000020,1 +7464,2,109000030,1 +7465,2,109000040,1 +7466,2,110000020,1 +7467,2,110000030,1 +7468,2,110000040,1 +7469,2,118000010,1 +7470,3,101000050,1 +7471,3,101000060,1 +7472,3,101000080,1 +7473,3,101000040,1 +7474,3,109000060,1 +7475,3,109000070,1 +7476,3,109000080,1 +7477,3,105000060,1 +7478,3,105000070,1 +7479,3,105000080,1 +7480,3,104000050,1 +7481,3,106000050,1 +7482,3,102000060,1 +7483,3,102000070,1 +7484,3,102000080,1 +7485,3,103000050,1 +7486,3,105000050,1 +7487,3,107000060,1 +7488,3,107000070,1 +7489,3,107000080,1 +7490,3,108000050,1 +7491,3,109000050,1 +7492,3,103000060,1 +7493,3,103000070,1 +7494,3,103000080,1 +7495,3,110000050,1 +7496,3,106000060,1 +7497,3,106000070,1 +7498,3,106000080,1 +7499,3,101000070,1 +7500,3,110000060,1 +7501,3,104000060,1 +7502,3,104000070,1 +7503,3,104000080,1 +7504,3,102000050,1 +7505,3,104000170,1 +7506,3,104000260,1 +7507,3,111000010,1 +7508,3,111000020,1 +7509,3,111000030,1 +7510,3,112000020,1 +7511,3,112000030,1 +7512,3,108000060,1 +7513,3,108000070,1 +7514,3,108000080,1 +7515,3,107000050,1 +7516,3,112000010,1 +7517,3,110000070,1 +7518,3,110000080,1 +7519,3,118000020,1 +7520,3,118000030,1 +7521,3,118000040,1 +7522,4,101000090,1 +7523,4,101000100,1 +7524,4,101000110,1 +7525,4,109000100,1 +7526,4,105000100,1 +7527,4,105000110,1 +7528,4,108000090,1 +7529,4,110000090,1 +7530,4,102000100,1 +7531,4,102000110,1 +7532,4,106000090,1 +7533,4,109000090,1 +7534,4,107000100,1 +7535,4,103000090,1 +7536,4,102000090,1 +7537,4,103000100,1 +7538,4,106000100,1 +7539,4,106000110,1 +7540,4,104000090,1 +7541,4,104000100,1 +7542,4,104000110,1 +7543,4,107000090,1 +7544,4,104000180,1 +7545,4,111000040,1 +7546,4,112000040,1 +7547,4,108000100,1 +7548,4,105000090,1 +7549,4,110000100,1 +7550,4,118000050,1 +7551,4,118000060,1 +7552,5,101000120,1 +7553,5,109000110,1 +7554,5,105000120,1 +7555,5,102000120,1 +7556,5,107000110,1 +7557,5,103000120,1 +7558,5,106000120,1 +7559,5,104000120,1 +7560,5,104000190,1 +7561,5,111000060,1 +7562,5,112000060,1 +7563,5,108000110,1 +7564,5,110000110,1 +7565,5,118000070,1 +7566,1,170002,3 +7567,2,170003,3 +7568,3,170004,3 +7569,1,180002,3 +7570,2,180003,3 +7571,3,180004,3 +7572,1,201000010,8 +7573,1,292000010,8 +7574,1,299000040,8 +7575,1,299000070,8 +7576,1,299000110,8 +7577,1,299000120,8 +7578,1,299000140,8 +7579,2,202000010,8 +7580,2,290000010,8 +7581,2,299000010,8 +7582,2,299000150,8 +7583,2,299000190,8 +7584,2,299000200,8 +7585,2,299000210,8 +7586,3,298000050,8 +7587,3,298000060,8 +7588,3,299000060,8 +7589,3,299000170,8 +7590,3,290000120,8 +7591,3,291000050,8 +7592,3,292000020,8 +7593,4,299000670,8 +7594,4,299000680,8 +7595,4,204000010,8 +7596,5,297000100,8 +7597,5,291000020,8 +7598,5,297000130,8 +7599,5,297000140,8 +7600,5,203000010,8 +7601,4,140000,3 +7602,5,150010,3 +7603,5,150020,3 +7604,5,150030,3 +7605,5,150040,3 +7607,1,101000010,1 +7608,1,102000010,1 +7609,1,103000010,1 +7610,1,104000010,1 +7611,1,105000010,1 +7612,1,106000010,1 +7613,1,107000010,1 +7614,1,108000010,1 +7615,1,109000010,1 +7616,1,110000010,1 +7617,2,101000020,1 +7618,2,101000030,1 +7619,2,102000020,1 +7620,2,102000030,1 +7621,2,102000040,1 +7622,2,103000020,1 +7623,2,103000030,1 +7624,2,103000040,1 +7625,2,104000020,1 +7626,2,104000030,1 +7627,2,104000040,1 +7628,2,105000020,1 +7629,2,105000030,1 +7630,2,105000040,1 +7631,2,106000020,1 +7632,2,106000030,1 +7633,2,106000040,1 +7634,2,107000020,1 +7635,2,107000030,1 +7636,2,107000040,1 +7637,2,108000020,1 +7638,2,108000030,1 +7639,2,108000040,1 +7640,2,109000020,1 +7641,2,109000030,1 +7642,2,109000040,1 +7643,2,110000020,1 +7644,2,110000030,1 +7645,2,110000040,1 +7646,2,118000010,1 +7647,3,101000050,1 +7648,3,101000060,1 +7649,3,101000080,1 +7650,3,101000040,1 +7651,3,109000060,1 +7652,3,109000070,1 +7653,3,109000080,1 +7654,3,105000060,1 +7655,3,105000070,1 +7656,3,105000080,1 +7657,3,104000050,1 +7658,3,106000050,1 +7659,3,102000060,1 +7660,3,102000070,1 +7661,3,102000080,1 +7662,3,103000050,1 +7663,3,105000050,1 +7664,3,107000060,1 +7665,3,107000070,1 +7666,3,107000080,1 +7667,3,108000050,1 +7668,3,109000050,1 +7669,3,103000060,1 +7670,3,103000070,1 +7671,3,103000080,1 +7672,3,110000050,1 +7673,3,106000060,1 +7674,3,106000070,1 +7675,3,106000080,1 +7676,3,101000070,1 +7677,3,110000060,1 +7678,3,104000060,1 +7679,3,104000070,1 +7680,3,104000080,1 +7681,3,102000050,1 +7682,3,104000170,1 +7683,3,104000260,1 +7684,3,111000010,1 +7685,3,111000020,1 +7686,3,111000030,1 +7687,3,112000020,1 +7688,3,112000030,1 +7689,3,108000060,1 +7690,3,108000070,1 +7691,3,108000080,1 +7692,3,107000050,1 +7693,3,112000010,1 +7694,3,110000070,1 +7695,3,110000080,1 +7696,3,118000020,1 +7697,3,118000030,1 +7698,3,118000040,1 +7699,4,101000090,1 +7700,4,101000100,1 +7701,4,101000110,1 +7702,4,109000100,1 +7703,4,105000100,1 +7704,4,105000110,1 +7705,4,108000090,1 +7706,4,110000090,1 +7707,4,102000100,1 +7708,4,102000110,1 +7709,4,106000090,1 +7710,4,109000090,1 +7711,4,107000100,1 +7712,4,103000090,1 +7713,4,102000090,1 +7714,4,103000100,1 +7715,4,106000100,1 +7716,4,106000110,1 +7717,4,104000090,1 +7718,4,104000100,1 +7719,4,104000110,1 +7720,4,107000090,1 +7721,4,104000180,1 +7722,4,111000040,1 +7723,4,112000040,1 +7724,4,108000100,1 +7725,4,105000090,1 +7726,4,110000100,1 +7727,4,118000050,1 +7728,4,118000060,1 +7729,5,101000120,1 +7730,5,109000110,1 +7731,5,105000120,1 +7732,5,102000120,1 +7733,5,107000110,1 +7734,5,103000120,1 +7735,5,106000120,1 +7736,5,104000120,1 +7737,5,104000190,1 +7738,5,111000060,1 +7739,5,112000060,1 +7740,5,108000110,1 +7741,5,110000110,1 +7742,5,118000070,1 +7743,2,170003,3 +7744,3,170004,3 +7745,2,180003,3 +7746,3,180004,3 +7747,1,201000010,8 +7748,1,292000010,8 +7749,1,299000040,8 +7750,1,299000070,8 +7751,1,299000110,8 +7752,1,299000120,8 +7753,1,299000140,8 +7754,2,202000010,8 +7755,2,290000010,8 +7756,2,299000010,8 +7757,2,299000150,8 +7758,2,299000190,8 +7759,2,299000200,8 +7760,2,299000210,8 +7761,3,298000050,8 +7762,3,298000060,8 +7763,3,299000060,8 +7764,3,299000170,8 +7765,3,290000120,8 +7766,3,291000050,8 +7767,3,292000020,8 +7768,4,299000670,8 +7769,4,299000680,8 +7770,4,204000010,8 +7771,5,297000100,8 +7772,5,291000020,8 +7773,5,297000130,8 +7774,5,297000140,8 +7775,5,203000010,8 +7776,4,140000,3 +7777,5,150010,3 +7778,5,150020,3 +7779,5,150030,3 +7780,5,150040,3 +7782,3,101000050,1 +7783,3,101000060,1 +7784,3,101000080,1 +7785,3,101000040,1 +7786,3,109000060,1 +7787,3,109000070,1 +7788,3,109000080,1 +7789,3,105000060,1 +7790,3,105000070,1 +7791,3,105000080,1 +7792,3,104000050,1 +7793,3,106000050,1 +7794,3,102000060,1 +7795,3,102000070,1 +7796,3,102000080,1 +7797,3,103000050,1 +7798,3,105000050,1 +7799,3,107000060,1 +7800,3,107000070,1 +7801,3,107000080,1 +7802,3,108000050,1 +7803,3,109000050,1 +7804,3,103000060,1 +7805,3,103000070,1 +7806,3,103000080,1 +7807,3,110000050,1 +7808,3,106000060,1 +7809,3,106000070,1 +7810,3,106000080,1 +7811,3,101000070,1 +7812,3,110000060,1 +7813,3,104000060,1 +7814,3,104000070,1 +7815,3,104000080,1 +7816,3,102000050,1 +7817,3,104000170,1 +7818,3,104000260,1 +7819,3,111000010,1 +7820,3,111000020,1 +7821,3,111000030,1 +7822,3,112000020,1 +7823,3,112000030,1 +7824,3,108000060,1 +7825,3,108000070,1 +7826,3,108000080,1 +7827,3,107000050,1 +7828,3,112000010,1 +7829,3,110000070,1 +7830,3,110000080,1 +7831,3,118000020,1 +7832,3,118000030,1 +7833,3,118000040,1 +7834,4,101000090,1 +7835,4,101000100,1 +7836,4,101000110,1 +7837,4,109000100,1 +7838,4,105000100,1 +7839,4,105000110,1 +7840,4,108000090,1 +7841,4,110000090,1 +7842,4,102000100,1 +7843,4,102000110,1 +7844,4,106000090,1 +7845,4,109000090,1 +7846,4,107000100,1 +7847,4,103000090,1 +7848,4,102000090,1 +7849,4,103000100,1 +7850,4,106000100,1 +7851,4,106000110,1 +7852,4,104000090,1 +7853,4,104000100,1 +7854,4,104000110,1 +7855,4,107000090,1 +7856,4,104000180,1 +7857,4,111000040,1 +7858,4,112000040,1 +7859,4,108000100,1 +7860,4,105000090,1 +7861,4,110000100,1 +7862,4,118000050,1 +7863,4,118000060,1 +7864,5,101000120,1 +7865,5,109000110,1 +7866,5,105000120,1 +7867,5,102000120,1 +7868,5,107000110,1 +7869,5,103000120,1 +7870,5,106000120,1 +7871,5,104000120,1 +7872,5,104000190,1 +7873,5,111000060,1 +7874,5,112000060,1 +7875,5,108000110,1 +7876,5,110000110,1 +7877,5,118000070,1 +7878,3,170004,3 +7879,3,180004,3 +7880,1,201000010,8 +7881,1,292000010,8 +7882,1,299000040,8 +7883,1,299000070,8 +7884,1,299000110,8 +7885,1,299000120,8 +7886,1,299000140,8 +7887,2,202000010,8 +7888,2,290000010,8 +7889,2,299000010,8 +7890,2,299000150,8 +7891,2,299000190,8 +7892,2,299000200,8 +7893,2,299000210,8 +7894,3,298000050,8 +7895,3,298000060,8 +7896,3,299000060,8 +7897,3,299000170,8 +7898,3,290000120,8 +7899,3,291000050,8 +7900,3,292000020,8 +7901,4,299000670,8 +7902,4,299000680,8 +7903,4,204000010,8 +7904,5,297000100,8 +7905,5,291000020,8 +7906,5,297000130,8 +7907,5,297000140,8 +7908,5,203000010,8 +7909,4,140000,3 +7910,5,150010,3 +7911,5,150020,3 +7912,5,150030,3 +7913,5,150040,3 +7915,3,101000050,1 +7916,3,101000060,1 +7917,3,101000080,1 +7918,3,101000040,1 +7919,3,109000060,1 +7920,3,109000070,1 +7921,3,109000080,1 +7922,3,105000060,1 +7923,3,105000070,1 +7924,3,105000080,1 +7925,3,104000050,1 +7926,3,106000050,1 +7927,3,102000060,1 +7928,3,102000070,1 +7929,3,102000080,1 +7930,3,103000050,1 +7931,3,105000050,1 +7932,3,107000060,1 +7933,3,107000070,1 +7934,3,107000080,1 +7935,3,108000050,1 +7936,3,109000050,1 +7937,3,103000060,1 +7938,3,103000070,1 +7939,3,103000080,1 +7940,3,110000050,1 +7941,3,106000060,1 +7942,3,106000070,1 +7943,3,106000080,1 +7944,3,101000070,1 +7945,3,110000060,1 +7946,3,104000060,1 +7947,3,104000070,1 +7948,3,104000080,1 +7949,3,102000050,1 +7950,3,104000170,1 +7951,3,104000260,1 +7952,3,111000010,1 +7953,3,111000020,1 +7954,3,111000030,1 +7955,3,112000020,1 +7956,3,112000030,1 +7957,3,108000060,1 +7958,3,108000070,1 +7959,3,108000080,1 +7960,3,107000050,1 +7961,3,112000010,1 +7962,3,110000070,1 +7963,3,110000080,1 +7964,3,118000020,1 +7965,3,118000030,1 +7966,3,118000040,1 +7967,4,101000090,1 +7968,4,101000100,1 +7969,4,101000110,1 +7970,4,109000100,1 +7971,4,105000100,1 +7972,4,105000110,1 +7973,4,108000090,1 +7974,4,110000090,1 +7975,4,102000100,1 +7976,4,102000110,1 +7977,4,106000090,1 +7978,4,109000090,1 +7979,4,107000100,1 +7980,4,103000090,1 +7981,4,102000090,1 +7982,4,103000100,1 +7983,4,106000100,1 +7984,4,106000110,1 +7985,4,104000090,1 +7986,4,104000100,1 +7987,4,104000110,1 +7988,4,107000090,1 +7989,4,104000180,1 +7990,4,111000040,1 +7991,4,112000040,1 +7992,4,108000100,1 +7993,4,105000090,1 +7994,4,110000100,1 +7995,4,118000050,1 +7996,4,118000060,1 +7997,5,101000120,1 +7998,5,109000110,1 +7999,5,105000120,1 +8000,5,102000120,1 +8001,5,107000110,1 +8002,5,103000120,1 +8003,5,106000120,1 +8004,5,104000120,1 +8005,5,104000190,1 +8006,5,111000060,1 +8007,5,112000060,1 +8008,5,108000110,1 +8009,5,110000110,1 +8010,5,118000070,1 +8011,3,170004,3 +8012,3,180004,3 +8013,1,201000010,8 +8014,1,292000010,8 +8015,1,299000040,8 +8016,1,299000070,8 +8017,1,299000110,8 +8018,1,299000120,8 +8019,1,299000140,8 +8020,2,202000010,8 +8021,2,290000010,8 +8022,2,299000010,8 +8023,2,299000150,8 +8024,2,299000190,8 +8025,2,299000200,8 +8026,2,299000210,8 +8027,3,298000050,8 +8028,3,298000060,8 +8029,3,299000060,8 +8030,3,299000170,8 +8031,3,290000120,8 +8032,3,291000050,8 +8033,3,292000020,8 +8034,4,299000670,8 +8035,4,299000680,8 +8036,4,204000010,8 +8037,5,297000100,8 +8038,5,291000020,8 +8039,5,297000130,8 +8040,5,297000140,8 +8041,5,203000010,8 +8042,4,140000,3 +8043,5,150010,3 +8044,5,150020,3 +8045,5,150030,3 +8046,5,150040,3 +8048,3,101000050,1 +8049,3,101000060,1 +8050,3,101000080,1 +8051,3,101000040,1 +8052,3,109000060,1 +8053,3,109000070,1 +8054,3,109000080,1 +8055,3,105000060,1 +8056,3,105000070,1 +8057,3,105000080,1 +8058,3,104000050,1 +8059,3,106000050,1 +8060,3,102000060,1 +8061,3,102000070,1 +8062,3,102000080,1 +8063,3,103000050,1 +8064,3,105000050,1 +8065,3,107000060,1 +8066,3,107000070,1 +8067,3,107000080,1 +8068,3,108000050,1 +8069,3,109000050,1 +8070,3,103000060,1 +8071,3,103000070,1 +8072,3,103000080,1 +8073,3,110000050,1 +8074,3,106000060,1 +8075,3,106000070,1 +8076,3,106000080,1 +8077,3,101000070,1 +8078,3,110000060,1 +8079,3,104000060,1 +8080,3,104000070,1 +8081,3,104000080,1 +8082,3,102000050,1 +8083,3,104000170,1 +8084,3,104000260,1 +8085,3,111000010,1 +8086,3,111000020,1 +8087,3,111000030,1 +8088,3,112000020,1 +8089,3,112000030,1 +8090,3,108000060,1 +8091,3,108000070,1 +8092,3,108000080,1 +8093,3,107000050,1 +8094,3,112000010,1 +8095,3,110000070,1 +8096,3,110000080,1 +8097,3,118000020,1 +8098,3,118000030,1 +8099,3,118000040,1 +8100,4,101000090,1 +8101,4,101000100,1 +8102,4,101000110,1 +8103,4,109000100,1 +8104,4,105000100,1 +8105,4,105000110,1 +8106,4,108000090,1 +8107,4,110000090,1 +8108,4,102000100,1 +8109,4,102000110,1 +8110,4,106000090,1 +8111,4,109000090,1 +8112,4,107000100,1 +8113,4,103000090,1 +8114,4,102000090,1 +8115,4,103000100,1 +8116,4,106000100,1 +8117,4,106000110,1 +8118,4,104000090,1 +8119,4,104000100,1 +8120,4,104000110,1 +8121,4,107000090,1 +8122,4,104000180,1 +8123,4,111000040,1 +8124,4,112000040,1 +8125,4,108000100,1 +8126,4,105000090,1 +8127,4,110000100,1 +8128,4,118000050,1 +8129,4,118000060,1 +8130,5,101000120,1 +8131,5,109000110,1 +8132,5,105000120,1 +8133,5,102000120,1 +8134,5,107000110,1 +8135,5,103000120,1 +8136,5,106000120,1 +8137,5,104000120,1 +8138,5,104000190,1 +8139,5,111000060,1 +8140,5,112000060,1 +8141,5,108000110,1 +8142,5,110000110,1 +8143,5,118000070,1 +8144,3,170004,3 +8145,3,180004,3 +8146,1,201000010,8 +8147,1,292000010,8 +8148,1,299000040,8 +8149,1,299000070,8 +8150,1,299000110,8 +8151,1,299000120,8 +8152,1,299000140,8 +8153,2,202000010,8 +8154,2,290000010,8 +8155,2,299000010,8 +8156,2,299000150,8 +8157,2,299000190,8 +8158,2,299000200,8 +8159,2,299000210,8 +8160,3,298000050,8 +8161,3,298000060,8 +8162,3,299000060,8 +8163,3,299000170,8 +8164,3,290000120,8 +8165,3,291000050,8 +8166,3,292000020,8 +8167,4,299000670,8 +8168,4,299000680,8 +8169,4,204000010,8 +8170,5,297000100,8 +8171,5,291000020,8 +8172,5,297000130,8 +8173,5,297000140,8 +8174,5,203000010,8 +8175,4,140000,3 +8176,5,150010,3 +8177,5,150020,3 +8178,5,150030,3 +8179,5,150040,3 +8180,5,190000,3 +8181,5,200000,3 +8182,5,210000,3 +8184,3,102000060,1 +8185,3,102000070,1 +8186,3,102000080,1 +8187,3,103000050,1 +8188,3,105000050,1 +8189,3,107000060,1 +8190,3,107000070,1 +8191,3,107000080,1 +8192,3,108000050,1 +8193,3,109000050,1 +8194,3,103000060,1 +8195,3,103000070,1 +8196,3,103000080,1 +8197,3,110000050,1 +8198,4,102000100,1 +8199,4,102000110,1 +8200,4,106000090,1 +8201,4,109000090,1 +8202,4,107000100,1 +8203,4,103000090,1 +8204,4,102000090,1 +8205,4,103000100,1 +8206,5,102000120,1 +8207,5,107000110,1 +8208,5,103000120,1 +8209,1,102000000,2 +8210,2,102000001,2 +8211,3,102000002,2 +8212,4,102000003,2 +8213,5,102000004,2 +8214,1,105000000,2 +8215,2,105000001,2 +8216,3,105000002,2 +8217,4,105000003,2 +8218,5,105000004,2 +8219,1,112000000,2 +8220,2,112000001,2 +8221,3,112000002,2 +8222,4,112000003,2 +8223,5,112000004,2 +8224,3,170004,3 +8225,4,170005,3 +8226,3,180004,3 +8227,4,180005,3 +8228,4,140000,3 +8229,4,150010,3 +8230,4,150020,3 +8231,4,150030,3 +8232,4,150040,3 +8234,3,118000020,1 +8235,3,118000030,1 +8236,3,118000040,1 +8237,3,106000060,1 +8238,3,106000070,1 +8239,3,106000080,1 +8240,3,101000070,1 +8241,3,110000060,1 +8242,3,104000060,1 +8243,3,104000070,1 +8244,3,104000080,1 +8245,3,102000050,1 +8246,3,104000170,1 +8247,3,104000260,1 +8248,4,118000050,1 +8249,4,118000060,1 +8250,4,106000100,1 +8251,4,106000110,1 +8252,4,104000090,1 +8253,4,104000100,1 +8254,4,104000110,1 +8255,4,104000270,1 +8256,4,107000090,1 +8257,4,104000180,1 +8258,5,118000070,1 +8259,5,106000120,1 +8260,5,104000120,1 +8261,5,104000190,1 +8262,1,103000000,2 +8263,2,103000001,2 +8264,3,103000002,2 +8265,4,103000003,2 +8266,5,103000004,2 +8267,1,111000000,2 +8268,2,111000001,2 +8269,3,111000002,2 +8270,4,111000003,2 +8271,5,111000004,2 +8272,1,115000000,2 +8273,2,115000001,2 +8274,3,115000002,2 +8275,4,115000003,2 +8276,5,115000004,2 +8277,4,110024,3 +8278,4,110034,3 +8279,4,110044,3 +8280,4,110054,3 +8281,3,110060,3 +8282,3,110070,3 +8283,3,110080,3 +8284,3,110090,3 +8285,3,110100,3 +8286,3,110110,3 +8287,3,110120,3 +8288,3,110130,3 +8289,3,110140,3 +8290,3,110150,3 +8291,3,110160,3 +8292,3,110170,3 +8293,3,110180,3 +8294,3,110190,3 +8295,3,110200,3 +8296,3,110210,3 +8297,3,110220,3 +8298,3,110230,3 +8299,3,110240,3 +8300,3,110250,3 +8301,3,110260,3 +8302,3,110270,3 +8303,3,110620,3 +8304,3,110670,3 +8305,4,140000,3 +8306,4,150010,3 +8307,4,150020,3 +8308,4,150030,3 +8309,4,150040,3 +8311,3,111000010,1 +8312,3,111000020,1 +8313,3,111000030,1 +8314,3,112000020,1 +8315,3,112000030,1 +8316,3,108000060,1 +8317,3,108000070,1 +8318,3,108000080,1 +8319,3,107000050,1 +8320,3,112000010,1 +8321,3,110000070,1 +8322,3,110000080,1 +8323,4,111000040,1 +8324,4,112000040,1 +8325,4,108000100,1 +8326,4,105000090,1 +8327,4,110000100,1 +8328,5,111000060,1 +8329,5,112000060,1 +8330,5,108000110,1 +8331,5,110000110,1 +8332,1,108000000,2 +8333,2,108000001,2 +8334,3,108000002,2 +8335,4,108000003,2 +8336,5,108000004,2 +8337,1,107000000,2 +8338,2,107000001,2 +8339,3,107000002,2 +8340,4,107000003,2 +8341,5,107000004,2 +8342,1,120000000,2 +8343,2,120000001,2 +8344,3,120000002,2 +8345,4,120000003,2 +8346,5,120000004,2 +8347,4,120024,3 +8348,4,120034,3 +8349,4,120044,3 +8350,4,120054,3 +8351,3,120241,3 +8352,3,120251,3 +8353,3,120261,3 +8354,3,120271,3 +8355,3,120300,3 +8356,3,120310,3 +8357,3,120320,3 +8358,3,120330,3 +8359,3,120340,3 +8360,3,120350,3 +8361,3,120360,3 +8362,3,120370,3 +8363,3,120380,3 +8364,3,120390,3 +8365,3,120400,3 +8366,3,120410,3 +8367,3,120420,3 +8368,3,120430,3 +8369,3,120450,3 +8370,3,120460,3 +8371,3,120550,3 +8372,3,120560,3 +8373,3,120570,3 +8374,3,120990,3 +8375,3,121000,3 +8376,3,121010,3 +8377,3,121020,3 +8378,4,140000,3 +8379,4,150010,3 +8380,4,150020,3 +8381,4,150030,3 +8382,4,150040,3 +8384,3,101000050,1 +8385,3,101000060,1 +8386,3,101000080,1 +8387,3,101000040,1 +8388,3,109000060,1 +8389,3,109000070,1 +8390,3,109000080,1 +8391,3,105000060,1 +8392,3,105000070,1 +8393,3,105000080,1 +8394,3,104000050,1 +8395,3,106000050,1 +8396,4,101000090,1 +8397,4,101000100,1 +8398,4,101000110,1 +8399,4,109000100,1 +8400,4,105000100,1 +8401,4,105000110,1 +8402,4,108000090,1 +8403,4,110000090,1 +8404,5,101000120,1 +8405,5,109000110,1 +8406,5,105000120,1 +8407,1,101000000,2 +8408,2,101000001,2 +8409,3,101000002,2 +8410,4,101000008,2 +8411,5,101000004,2 +8412,1,109000000,2 +8413,2,109000001,2 +8414,3,109000002,2 +8415,4,109000003,2 +8416,5,109000004,2 +8417,4,130024,3 +8418,4,130034,3 +8419,4,130044,3 +8420,4,130054,3 +8421,3,130060,3 +8422,3,130070,3 +8423,3,130080,3 +8424,3,130090,3 +8425,3,130100,3 +8426,3,130110,3 +8427,3,130120,3 +8428,3,130130,3 +8429,3,130140,3 +8430,3,130150,3 +8431,3,130160,3 +8432,3,130170,3 +8433,3,130180,3 +8434,3,130190,3 +8435,3,130200,3 +8436,3,130420,3 +8437,3,130510,3 +8438,3,130520,3 +8439,3,130531,3 +8440,3,130540,3 +8441,3,130660,3 +8442,3,130790,3 +8443,3,130800,3 +8444,3,131130,3 +8445,4,140000,3 +8446,4,150010,3 +8447,4,150020,3 +8448,4,150030,3 +8449,4,150040,3 +8451,1,101000000,2 +8452,2,101000001,2 +8453,3,101000002,2 +8454,4,101000008,2 +8455,5,101000004,2 +8456,1,102000000,2 +8457,2,102000001,2 +8458,3,102000002,2 +8459,4,102000003,2 +8460,5,102000004,2 +8461,1,103000000,2 +8462,2,103000001,2 +8463,3,103000002,2 +8464,4,103000003,2 +8465,5,103000004,2 +8466,1,105000000,2 +8467,2,105000001,2 +8468,3,105000002,2 +8469,4,105000003,2 +8470,5,105000004,2 +8471,1,108000000,2 +8472,2,108000001,2 +8473,3,108000002,2 +8474,4,108000003,2 +8475,5,108000004,2 +8476,1,109000000,2 +8477,2,109000001,2 +8478,3,109000002,2 +8479,4,109000003,2 +8480,5,109000004,2 +8481,1,111000000,2 +8482,2,111000001,2 +8483,3,111000002,2 +8484,4,111000003,2 +8485,5,111000004,2 +8486,1,112000000,2 +8487,2,112000001,2 +8488,3,112000002,2 +8489,4,112000003,2 +8490,5,112000004,2 +8491,1,120000000,2 +8492,2,120000001,2 +8493,3,120000002,2 +8494,4,120000003,2 +8495,5,120000004,2 +8496,1,101000010,1 +8497,2,101000020,1 +8498,2,101000030,1 +8499,3,101000040,1 +8500,3,101000050,1 +8501,3,101000060,1 +8502,3,101000070,1 +8503,3,101000080,1 +8504,1,102000010,1 +8505,2,102000020,1 +8506,2,102000030,1 +8507,2,102000040,1 +8508,3,102000050,1 +8509,3,102000060,1 +8510,3,102000070,1 +8511,3,102000080,1 +8512,1,103000010,1 +8513,2,103000020,1 +8514,2,103000030,1 +8515,2,103000040,1 +8516,3,103000050,1 +8517,3,103000060,1 +8518,3,103000070,1 +8519,3,103000080,1 +8520,1,104000010,1 +8521,2,104000020,1 +8522,2,104000030,1 +8523,2,104000040,1 +8524,3,104000050,1 +8525,3,104000060,1 +8526,3,104000070,1 +8527,3,104000080,1 +8528,1,105000010,1 +8529,2,105000020,1 +8530,2,105000030,1 +8531,2,105000040,1 +8532,3,105000050,1 +8533,3,105000060,1 +8534,3,105000070,1 +8535,3,105000080,1 +8536,1,106000010,1 +8537,2,106000020,1 +8538,2,106000030,1 +8539,2,106000040,1 +8540,3,106000050,1 +8541,3,106000060,1 +8542,3,106000070,1 +8543,3,106000080,1 +8544,1,107000010,1 +8545,2,107000020,1 +8546,2,107000030,1 +8547,2,107000040,1 +8548,3,107000050,1 +8549,3,107000060,1 +8550,3,107000070,1 +8551,3,107000080,1 +8552,1,108000010,1 +8553,2,108000020,1 +8554,2,108000030,1 +8555,2,108000040,1 +8556,3,108000050,1 +8557,3,108000060,1 +8558,3,108000070,1 +8559,3,108000080,1 +8560,2,180000,3 +8561,2,170002,3 +8562,3,170003,3 +8563,4,170004,3 +8565,1,101000000,2 +8566,2,101000001,2 +8567,3,101000002,2 +8568,4,101000008,2 +8569,5,101000004,2 +8570,1,102000000,2 +8571,2,102000001,2 +8572,3,102000002,2 +8573,4,102000003,2 +8574,5,102000004,2 +8575,1,103000000,2 +8576,2,103000001,2 +8577,3,103000002,2 +8578,4,103000003,2 +8579,5,103000004,2 +8580,1,105000000,2 +8581,2,105000001,2 +8582,3,105000002,2 +8583,4,105000003,2 +8584,5,105000004,2 +8585,1,108000000,2 +8586,2,108000001,2 +8587,3,108000002,2 +8588,4,108000003,2 +8589,5,108000004,2 +8590,1,109000000,2 +8591,2,109000001,2 +8592,3,109000002,2 +8593,4,109000003,2 +8594,5,109000004,2 +8595,1,111000000,2 +8596,2,111000001,2 +8597,3,111000002,2 +8598,4,111000003,2 +8599,5,111000004,2 +8600,1,112000000,2 +8601,2,112000001,2 +8602,3,112000002,2 +8603,4,112000003,2 +8604,5,112000004,2 +8605,1,120000000,2 +8606,2,120000001,2 +8607,3,120000002,2 +8608,4,120000003,2 +8609,5,120000004,2 +8610,1,101000010,1 +8611,2,101000020,1 +8612,2,101000030,1 +8613,3,101000040,1 +8614,3,101000050,1 +8615,3,101000060,1 +8616,3,101000070,1 +8617,3,101000080,1 +8618,1,102000010,1 +8619,2,102000020,1 +8620,2,102000030,1 +8621,2,102000040,1 +8622,3,102000050,1 +8623,3,102000060,1 +8624,3,102000070,1 +8625,3,102000080,1 +8626,1,103000010,1 +8627,2,103000020,1 +8628,2,103000030,1 +8629,2,103000040,1 +8630,3,103000050,1 +8631,3,103000060,1 +8632,3,103000070,1 +8633,3,103000080,1 +8634,1,104000010,1 +8635,2,104000020,1 +8636,2,104000030,1 +8637,2,104000040,1 +8638,3,104000050,1 +8639,3,104000060,1 +8640,3,104000070,1 +8641,3,104000080,1 +8642,1,105000010,1 +8643,2,105000020,1 +8644,2,105000030,1 +8645,2,105000040,1 +8646,3,105000050,1 +8647,3,105000060,1 +8648,3,105000070,1 +8649,3,105000080,1 +8650,1,106000010,1 +8651,2,106000020,1 +8652,2,106000030,1 +8653,2,106000040,1 +8654,3,106000050,1 +8655,3,106000060,1 +8656,3,106000070,1 +8657,3,106000080,1 +8658,1,107000010,1 +8659,2,107000020,1 +8660,2,107000030,1 +8661,2,107000040,1 +8662,3,107000050,1 +8663,3,107000060,1 +8664,3,107000070,1 +8665,3,107000080,1 +8666,1,108000010,1 +8667,2,108000020,1 +8668,2,108000030,1 +8669,2,108000040,1 +8670,3,108000050,1 +8671,3,108000060,1 +8672,3,108000070,1 +8673,3,108000080,1 +8674,2,180000,3 +8675,2,170002,3 +8676,3,170003,3 +8677,4,170004,3 +8679,1,101000000,2 +8680,2,101000001,2 +8681,3,101000002,2 +8682,4,101000008,2 +8683,5,101000004,2 +8684,1,102000000,2 +8685,2,102000001,2 +8686,3,102000002,2 +8687,4,102000003,2 +8688,5,102000004,2 +8689,1,103000000,2 +8690,2,103000001,2 +8691,3,103000002,2 +8692,4,103000003,2 +8693,5,103000004,2 +8694,1,105000000,2 +8695,2,105000001,2 +8696,3,105000002,2 +8697,4,105000003,2 +8698,5,105000004,2 +8699,1,108000000,2 +8700,2,108000001,2 +8701,3,108000002,2 +8702,4,108000003,2 +8703,5,108000004,2 +8704,1,109000000,2 +8705,2,109000001,2 +8706,3,109000002,2 +8707,4,109000003,2 +8708,5,109000004,2 +8709,1,111000000,2 +8710,2,111000001,2 +8711,3,111000002,2 +8712,4,111000003,2 +8713,5,111000004,2 +8714,1,112000000,2 +8715,2,112000001,2 +8716,3,112000002,2 +8717,4,112000003,2 +8718,5,112000004,2 +8719,1,120000000,2 +8720,2,120000001,2 +8721,3,120000002,2 +8722,4,120000003,2 +8723,5,120000004,2 +8724,1,101000010,1 +8725,2,101000020,1 +8726,2,101000030,1 +8727,3,101000040,1 +8728,3,101000050,1 +8729,3,101000060,1 +8730,3,101000070,1 +8731,3,101000080,1 +8732,1,102000010,1 +8733,2,102000020,1 +8734,2,102000030,1 +8735,2,102000040,1 +8736,3,102000050,1 +8737,3,102000060,1 +8738,3,102000070,1 +8739,3,102000080,1 +8740,1,103000010,1 +8741,2,103000020,1 +8742,2,103000030,1 +8743,2,103000040,1 +8744,3,103000050,1 +8745,3,103000060,1 +8746,3,103000070,1 +8747,3,103000080,1 +8748,1,104000010,1 +8749,2,104000020,1 +8750,2,104000030,1 +8751,2,104000040,1 +8752,3,104000050,1 +8753,3,104000060,1 +8754,3,104000070,1 +8755,3,104000080,1 +8756,1,105000010,1 +8757,2,105000020,1 +8758,2,105000030,1 +8759,2,105000040,1 +8760,3,105000050,1 +8761,3,105000060,1 +8762,3,105000070,1 +8763,3,105000080,1 +8764,1,106000010,1 +8765,2,106000020,1 +8766,2,106000030,1 +8767,2,106000040,1 +8768,3,106000050,1 +8769,3,106000060,1 +8770,3,106000070,1 +8771,3,106000080,1 +8772,1,107000010,1 +8773,2,107000020,1 +8774,2,107000030,1 +8775,2,107000040,1 +8776,3,107000050,1 +8777,3,107000060,1 +8778,3,107000070,1 +8779,3,107000080,1 +8780,1,108000010,1 +8781,2,108000020,1 +8782,2,108000030,1 +8783,2,108000040,1 +8784,3,108000050,1 +8785,3,108000060,1 +8786,3,108000070,1 +8787,3,108000080,1 +8788,2,180001,3 +8789,2,170002,3 +8790,3,170003,3 +8791,4,170004,3 +8793,1,101000000,2 +8794,2,101000001,2 +8795,3,101000002,2 +8796,4,101000008,2 +8797,5,101000004,2 +8798,1,102000000,2 +8799,2,102000001,2 +8800,3,102000002,2 +8801,4,102000003,2 +8802,5,102000004,2 +8803,1,103000000,2 +8804,2,103000001,2 +8805,3,103000002,2 +8806,4,103000003,2 +8807,5,103000004,2 +8808,1,105000000,2 +8809,2,105000001,2 +8810,3,105000002,2 +8811,4,105000003,2 +8812,5,105000004,2 +8813,1,108000000,2 +8814,2,108000001,2 +8815,3,108000002,2 +8816,4,108000003,2 +8817,5,108000004,2 +8818,1,109000000,2 +8819,2,109000001,2 +8820,3,109000002,2 +8821,4,109000003,2 +8822,5,109000004,2 +8823,1,111000000,2 +8824,2,111000001,2 +8825,3,111000002,2 +8826,4,111000003,2 +8827,5,111000004,2 +8828,1,112000000,2 +8829,2,112000001,2 +8830,3,112000002,2 +8831,4,112000003,2 +8832,5,112000004,2 +8833,1,120000000,2 +8834,2,120000001,2 +8835,3,120000002,2 +8836,4,120000003,2 +8837,5,120000004,2 +8838,1,101000010,1 +8839,2,101000020,1 +8840,2,101000030,1 +8841,3,101000040,1 +8842,3,101000050,1 +8843,3,101000060,1 +8844,3,101000070,1 +8845,3,101000080,1 +8846,1,102000010,1 +8847,2,102000020,1 +8848,2,102000030,1 +8849,2,102000040,1 +8850,3,102000050,1 +8851,3,102000060,1 +8852,3,102000070,1 +8853,3,102000080,1 +8854,1,103000010,1 +8855,2,103000020,1 +8856,2,103000030,1 +8857,2,103000040,1 +8858,3,103000050,1 +8859,3,103000060,1 +8860,3,103000070,1 +8861,3,103000080,1 +8862,1,104000010,1 +8863,2,104000020,1 +8864,2,104000030,1 +8865,2,104000040,1 +8866,3,104000050,1 +8867,3,104000060,1 +8868,3,104000070,1 +8869,3,104000080,1 +8870,1,105000010,1 +8871,2,105000020,1 +8872,2,105000030,1 +8873,2,105000040,1 +8874,3,105000050,1 +8875,3,105000060,1 +8876,3,105000070,1 +8877,3,105000080,1 +8878,1,106000010,1 +8879,2,106000020,1 +8880,2,106000030,1 +8881,2,106000040,1 +8882,3,106000050,1 +8883,3,106000060,1 +8884,3,106000070,1 +8885,3,106000080,1 +8886,1,107000010,1 +8887,2,107000020,1 +8888,2,107000030,1 +8889,2,107000040,1 +8890,3,107000050,1 +8891,3,107000060,1 +8892,3,107000070,1 +8893,3,107000080,1 +8894,1,108000010,1 +8895,2,108000020,1 +8896,2,108000030,1 +8897,2,108000040,1 +8898,3,108000050,1 +8899,3,108000060,1 +8900,3,108000070,1 +8901,3,108000080,1 +8902,1,109000010,1 +8903,2,109000020,1 +8904,2,109000030,1 +8905,2,109000040,1 +8906,3,109000050,1 +8907,3,109000060,1 +8908,3,109000070,1 +8909,3,109000080,1 +8910,2,180001,3 +8911,2,170002,3 +8912,3,170003,3 +8913,4,170004,3 +8915,1,101000000,2 +8916,2,101000001,2 +8917,3,101000002,2 +8918,4,101000008,2 +8919,5,101000004,2 +8920,1,102000000,2 +8921,2,102000001,2 +8922,3,102000002,2 +8923,4,102000003,2 +8924,5,102000004,2 +8925,1,103000000,2 +8926,2,103000001,2 +8927,3,103000002,2 +8928,4,103000003,2 +8929,5,103000004,2 +8930,1,105000000,2 +8931,2,105000001,2 +8932,3,105000002,2 +8933,4,105000003,2 +8934,5,105000004,2 +8935,1,107000000,2 +8936,2,107000001,2 +8937,3,107000002,2 +8938,4,107000003,2 +8939,5,107000004,2 +8940,1,108000000,2 +8941,2,108000001,2 +8942,3,108000002,2 +8943,4,108000003,2 +8944,5,108000004,2 +8945,1,109000000,2 +8946,2,109000001,2 +8947,3,109000002,2 +8948,4,109000003,2 +8949,5,109000004,2 +8950,1,111000000,2 +8951,2,111000001,2 +8952,3,111000002,2 +8953,4,111000003,2 +8954,5,111000004,2 +8955,1,112000000,2 +8956,2,112000001,2 +8957,3,112000002,2 +8958,4,112000003,2 +8959,5,112000004,2 +8960,1,120000000,2 +8961,2,120000001,2 +8962,3,120000002,2 +8963,4,120000003,2 +8964,5,120000004,2 +8965,1,101000010,1 +8966,2,101000020,1 +8967,2,101000030,1 +8968,3,101000040,1 +8969,3,101000050,1 +8970,3,101000060,1 +8971,3,101000070,1 +8972,3,101000080,1 +8973,1,102000010,1 +8974,2,102000020,1 +8975,2,102000030,1 +8976,2,102000040,1 +8977,3,102000050,1 +8978,3,102000060,1 +8979,3,102000070,1 +8980,3,102000080,1 +8981,1,103000010,1 +8982,2,103000020,1 +8983,2,103000030,1 +8984,2,103000040,1 +8985,3,103000050,1 +8986,3,103000060,1 +8987,3,103000070,1 +8988,3,103000080,1 +8989,1,104000010,1 +8990,2,104000020,1 +8991,2,104000030,1 +8992,2,104000040,1 +8993,3,104000050,1 +8994,3,104000060,1 +8995,3,104000070,1 +8996,3,104000080,1 +8997,1,105000010,1 +8998,2,105000020,1 +8999,2,105000030,1 +9000,2,105000040,1 +9001,3,105000050,1 +9002,3,105000060,1 +9003,3,105000070,1 +9004,3,105000080,1 +9005,1,106000010,1 +9006,2,106000020,1 +9007,2,106000030,1 +9008,2,106000040,1 +9009,3,106000050,1 +9010,3,106000060,1 +9011,3,106000070,1 +9012,3,106000080,1 +9013,1,107000010,1 +9014,2,107000020,1 +9015,2,107000030,1 +9016,2,107000040,1 +9017,3,107000050,1 +9018,3,107000060,1 +9019,3,107000070,1 +9020,3,107000080,1 +9021,1,108000010,1 +9022,2,108000020,1 +9023,2,108000030,1 +9024,2,108000040,1 +9025,3,108000050,1 +9026,3,108000060,1 +9027,3,108000070,1 +9028,3,108000080,1 +9029,1,109000010,1 +9030,2,109000020,1 +9031,2,109000030,1 +9032,2,109000040,1 +9033,3,109000050,1 +9034,3,109000060,1 +9035,3,109000070,1 +9036,3,109000080,1 +9037,1,110000010,1 +9038,2,110000020,1 +9039,2,110000030,1 +9040,2,110000040,1 +9041,3,110000050,1 +9042,3,110000060,1 +9043,3,110000070,1 +9044,3,110000080,1 +9045,2,180001,3 +9046,2,170002,3 +9047,3,170003,3 +9048,4,170004,3 +9050,1,101000010,1 +9051,1,102000010,1 +9052,1,103000010,1 +9053,1,104000010,1 +9054,1,105000010,1 +9055,1,106000010,1 +9056,1,107000010,1 +9057,1,108000010,1 +9058,1,109000010,1 +9059,1,110000010,1 +9060,2,101000020,1 +9061,2,101000030,1 +9062,2,102000020,1 +9063,2,102000030,1 +9064,2,102000040,1 +9065,2,103000020,1 +9066,2,103000030,1 +9067,2,103000040,1 +9068,2,104000020,1 +9069,2,104000030,1 +9070,2,104000040,1 +9071,2,105000020,1 +9072,2,105000030,1 +9073,2,105000040,1 +9074,2,106000020,1 +9075,2,106000030,1 +9076,2,106000040,1 +9077,2,107000020,1 +9078,2,107000030,1 +9079,2,107000040,1 +9080,2,108000020,1 +9081,2,108000030,1 +9082,2,108000040,1 +9083,2,109000020,1 +9084,2,109000030,1 +9085,2,109000040,1 +9086,2,110000020,1 +9087,2,110000030,1 +9088,2,110000040,1 +9089,2,118000010,1 +9090,3,101000050,1 +9091,3,101000060,1 +9092,3,101000080,1 +9093,3,101000040,1 +9094,3,109000060,1 +9095,3,109000070,1 +9096,3,109000080,1 +9097,3,105000060,1 +9098,3,105000070,1 +9099,3,105000080,1 +9100,3,104000050,1 +9101,3,106000050,1 +9102,3,102000060,1 +9103,3,102000070,1 +9104,3,102000080,1 +9105,3,103000050,1 +9106,3,105000050,1 +9107,3,107000060,1 +9108,3,107000070,1 +9109,3,107000080,1 +9110,3,108000050,1 +9111,3,109000050,1 +9112,3,103000060,1 +9113,3,103000070,1 +9114,3,103000080,1 +9115,3,110000050,1 +9116,3,106000060,1 +9117,3,106000070,1 +9118,3,106000080,1 +9119,3,101000070,1 +9120,3,110000060,1 +9121,3,104000060,1 +9122,3,104000070,1 +9123,3,104000080,1 +9124,3,102000050,1 +9125,3,104000170,1 +9126,3,104000260,1 +9127,3,111000010,1 +9128,3,111000020,1 +9129,3,111000030,1 +9130,3,112000020,1 +9131,3,112000030,1 +9132,3,108000060,1 +9133,3,108000070,1 +9134,3,108000080,1 +9135,3,107000050,1 +9136,3,112000010,1 +9137,3,110000070,1 +9138,3,110000080,1 +9139,3,118000020,1 +9140,3,118000030,1 +9141,3,118000040,1 +9142,4,101000090,1 +9143,4,101000100,1 +9144,4,101000110,1 +9145,4,109000100,1 +9146,4,105000100,1 +9147,4,105000110,1 +9148,4,108000090,1 +9149,4,110000090,1 +9150,4,102000100,1 +9151,4,102000110,1 +9152,4,106000090,1 +9153,4,109000090,1 +9154,4,107000100,1 +9155,4,103000090,1 +9156,4,102000090,1 +9157,4,103000100,1 +9158,4,106000100,1 +9159,4,106000110,1 +9160,4,104000090,1 +9161,4,104000100,1 +9162,4,104000110,1 +9163,4,107000090,1 +9164,4,104000180,1 +9165,4,111000040,1 +9166,4,112000040,1 +9167,4,108000100,1 +9168,4,105000090,1 +9169,4,110000100,1 +9170,4,118000050,1 +9171,4,118000060,1 +9172,5,101000120,1 +9173,5,109000110,1 +9174,5,105000120,1 +9175,5,102000120,1 +9176,5,107000110,1 +9177,5,103000120,1 +9178,5,106000120,1 +9179,5,104000120,1 +9180,5,104000190,1 +9181,5,111000060,1 +9182,5,112000060,1 +9183,5,108000110,1 +9184,5,110000110,1 +9185,5,118000070,1 +9186,1,201000010,8 +9187,1,292000010,8 +9188,1,299000040,8 +9189,1,299000070,8 +9190,1,299000110,8 +9191,1,299000120,8 +9192,1,299000140,8 +9193,2,202000010,8 +9194,2,290000010,8 +9195,2,299000010,8 +9196,2,299000150,8 +9197,2,299000190,8 +9198,2,299000200,8 +9199,2,299000210,8 +9200,3,298000050,8 +9201,3,298000060,8 +9202,3,299000060,8 +9203,3,299000170,8 +9204,3,290000120,8 +9205,3,291000050,8 +9206,3,292000020,8 +9207,4,299000670,8 +9208,4,299000680,8 +9209,4,204000010,8 +9210,4,209000040,8 +9211,5,297000100,8 +9212,5,291000020,8 +9213,5,297000130,8 +9214,5,297000140,8 +9215,5,203000010,8 +9216,5,206000030,8 +9217,1,170002,3 +9218,1,180002,3 +9219,2,170003,3 +9220,2,180003,3 +9221,3,170004,3 +9222,3,180004,3 +9223,4,140000,3 +9224,5,150010,3 +9225,5,150020,3 +9226,5,150030,3 +9227,5,150040,3 +9229,1,101000010,1 +9230,1,102000010,1 +9231,1,103000010,1 +9232,1,104000010,1 +9233,1,105000010,1 +9234,1,106000010,1 +9235,1,107000010,1 +9236,1,108000010,1 +9237,1,109000010,1 +9238,1,110000010,1 +9239,2,101000020,1 +9240,2,101000030,1 +9241,2,102000020,1 +9242,2,102000030,1 +9243,2,102000040,1 +9244,2,103000020,1 +9245,2,103000030,1 +9246,2,103000040,1 +9247,2,104000020,1 +9248,2,104000030,1 +9249,2,104000040,1 +9250,2,105000020,1 +9251,2,105000030,1 +9252,2,105000040,1 +9253,2,106000020,1 +9254,2,106000030,1 +9255,2,106000040,1 +9256,2,107000020,1 +9257,2,107000030,1 +9258,2,107000040,1 +9259,2,108000020,1 +9260,2,108000030,1 +9261,2,108000040,1 +9262,2,109000020,1 +9263,2,109000030,1 +9264,2,109000040,1 +9265,2,110000020,1 +9266,2,110000030,1 +9267,2,110000040,1 +9268,2,118000010,1 +9269,3,101000050,1 +9270,3,101000060,1 +9271,3,101000080,1 +9272,3,101000040,1 +9273,3,109000060,1 +9274,3,109000070,1 +9275,3,109000080,1 +9276,3,105000060,1 +9277,3,105000070,1 +9278,3,105000080,1 +9279,3,104000050,1 +9280,3,106000050,1 +9281,3,102000060,1 +9282,3,102000070,1 +9283,3,102000080,1 +9284,3,103000050,1 +9285,3,105000050,1 +9286,3,107000060,1 +9287,3,107000070,1 +9288,3,107000080,1 +9289,3,108000050,1 +9290,3,109000050,1 +9291,3,103000060,1 +9292,3,103000070,1 +9293,3,103000080,1 +9294,3,110000050,1 +9295,3,106000060,1 +9296,3,106000070,1 +9297,3,106000080,1 +9298,3,101000070,1 +9299,3,110000060,1 +9300,3,104000060,1 +9301,3,104000070,1 +9302,3,104000080,1 +9303,3,102000050,1 +9304,3,104000170,1 +9305,3,104000260,1 +9306,3,111000010,1 +9307,3,111000020,1 +9308,3,111000030,1 +9309,3,112000020,1 +9310,3,112000030,1 +9311,3,108000060,1 +9312,3,108000070,1 +9313,3,108000080,1 +9314,3,107000050,1 +9315,3,112000010,1 +9316,3,110000070,1 +9317,3,110000080,1 +9318,3,118000020,1 +9319,3,118000030,1 +9320,3,118000040,1 +9321,4,101000090,1 +9322,4,101000100,1 +9323,4,101000110,1 +9324,4,109000100,1 +9325,4,105000100,1 +9326,4,105000110,1 +9327,4,108000090,1 +9328,4,110000090,1 +9329,4,102000100,1 +9330,4,102000110,1 +9331,4,106000090,1 +9332,4,109000090,1 +9333,4,107000100,1 +9334,4,103000090,1 +9335,4,102000090,1 +9336,4,103000100,1 +9337,4,106000100,1 +9338,4,106000110,1 +9339,4,104000090,1 +9340,4,104000100,1 +9341,4,104000110,1 +9342,4,107000090,1 +9343,4,104000180,1 +9344,4,111000040,1 +9345,4,112000040,1 +9346,4,108000100,1 +9347,4,105000090,1 +9348,4,110000100,1 +9349,4,118000050,1 +9350,4,118000060,1 +9351,5,101000120,1 +9352,5,109000110,1 +9353,5,105000120,1 +9354,5,102000120,1 +9355,5,107000110,1 +9356,5,103000120,1 +9357,5,106000120,1 +9358,5,104000120,1 +9359,5,104000190,1 +9360,5,111000060,1 +9361,5,112000060,1 +9362,5,108000110,1 +9363,5,110000110,1 +9364,5,118000070,1 +9365,1,201000010,8 +9366,1,292000010,8 +9367,1,299000040,8 +9368,1,299000070,8 +9369,1,299000110,8 +9370,1,299000120,8 +9371,1,299000140,8 +9372,2,202000010,8 +9373,2,290000010,8 +9374,2,299000010,8 +9375,2,299000150,8 +9376,2,299000190,8 +9377,2,299000200,8 +9378,2,299000210,8 +9379,3,298000050,8 +9380,3,298000060,8 +9381,3,299000060,8 +9382,3,299000170,8 +9383,3,290000120,8 +9384,3,291000050,8 +9385,3,292000020,8 +9386,4,299000670,8 +9387,4,299000680,8 +9388,4,204000010,8 +9389,4,209000040,8 +9390,5,297000100,8 +9391,5,291000020,8 +9392,5,297000130,8 +9393,5,297000140,8 +9394,5,203000010,8 +9395,5,206000030,8 +9396,2,170003,3 +9397,2,180003,3 +9398,3,170004,3 +9399,3,180004,3 +9400,4,140000,3 +9401,5,150010,3 +9402,5,150020,3 +9403,5,150030,3 +9404,5,150040,3 +9406,3,101000050,1 +9407,3,101000060,1 +9408,3,101000080,1 +9409,3,101000040,1 +9410,3,109000060,1 +9411,3,109000070,1 +9412,3,109000080,1 +9413,3,105000060,1 +9414,3,105000070,1 +9415,3,105000080,1 +9416,3,104000050,1 +9417,3,106000050,1 +9418,3,102000060,1 +9419,3,102000070,1 +9420,3,102000080,1 +9421,3,103000050,1 +9422,3,105000050,1 +9423,3,107000060,1 +9424,3,107000070,1 +9425,3,107000080,1 +9426,3,108000050,1 +9427,3,109000050,1 +9428,3,103000060,1 +9429,3,103000070,1 +9430,3,103000080,1 +9431,3,110000050,1 +9432,3,106000060,1 +9433,3,106000070,1 +9434,3,106000080,1 +9435,3,101000070,1 +9436,3,110000060,1 +9437,3,104000060,1 +9438,3,104000070,1 +9439,3,104000080,1 +9440,3,102000050,1 +9441,3,104000170,1 +9442,3,104000260,1 +9443,3,111000010,1 +9444,3,111000020,1 +9445,3,111000030,1 +9446,3,112000020,1 +9447,3,112000030,1 +9448,3,108000060,1 +9449,3,108000070,1 +9450,3,108000080,1 +9451,3,107000050,1 +9452,3,112000010,1 +9453,3,110000070,1 +9454,3,110000080,1 +9455,3,118000020,1 +9456,3,118000030,1 +9457,3,118000040,1 +9458,4,101000090,1 +9459,4,101000100,1 +9460,4,101000110,1 +9461,4,109000100,1 +9462,4,105000100,1 +9463,4,105000110,1 +9464,4,108000090,1 +9465,4,110000090,1 +9466,4,102000100,1 +9467,4,102000110,1 +9468,4,106000090,1 +9469,4,109000090,1 +9470,4,107000100,1 +9471,4,103000090,1 +9472,4,102000090,1 +9473,4,103000100,1 +9474,4,106000100,1 +9475,4,106000110,1 +9476,4,104000090,1 +9477,4,104000100,1 +9478,4,104000110,1 +9479,4,107000090,1 +9480,4,104000180,1 +9481,4,111000040,1 +9482,4,112000040,1 +9483,4,108000100,1 +9484,4,105000090,1 +9485,4,110000100,1 +9486,4,118000050,1 +9487,4,118000060,1 +9488,5,101000120,1 +9489,5,109000110,1 +9490,5,105000120,1 +9491,5,102000120,1 +9492,5,107000110,1 +9493,5,103000120,1 +9494,5,106000120,1 +9495,5,104000120,1 +9496,5,104000190,1 +9497,5,111000060,1 +9498,5,112000060,1 +9499,5,108000110,1 +9500,5,110000110,1 +9501,5,118000070,1 +9502,1,201000010,8 +9503,1,292000010,8 +9504,1,299000040,8 +9505,1,299000070,8 +9506,1,299000110,8 +9507,1,299000120,8 +9508,1,299000140,8 +9509,2,202000010,8 +9510,2,290000010,8 +9511,2,299000010,8 +9512,2,299000150,8 +9513,2,299000190,8 +9514,2,299000200,8 +9515,2,299000210,8 +9516,3,298000050,8 +9517,3,298000060,8 +9518,3,299000060,8 +9519,3,299000170,8 +9520,3,290000120,8 +9521,3,291000050,8 +9522,3,292000020,8 +9523,4,299000670,8 +9524,4,299000680,8 +9525,4,204000010,8 +9526,4,209000040,8 +9527,5,297000100,8 +9528,5,291000020,8 +9529,5,297000130,8 +9530,5,297000140,8 +9531,5,203000010,8 +9532,5,206000030,8 +9533,3,170004,3 +9534,3,180004,3 +9535,4,140000,3 +9536,5,150010,3 +9537,5,150020,3 +9538,5,150030,3 +9539,5,150040,3 +9541,3,101000050,1 +9542,3,101000060,1 +9543,3,101000080,1 +9544,3,101000040,1 +9545,3,109000060,1 +9546,3,109000070,1 +9547,3,109000080,1 +9548,3,105000060,1 +9549,3,105000070,1 +9550,3,105000080,1 +9551,3,104000050,1 +9552,3,106000050,1 +9553,3,102000060,1 +9554,3,102000070,1 +9555,3,102000080,1 +9556,3,103000050,1 +9557,3,105000050,1 +9558,3,107000060,1 +9559,3,107000070,1 +9560,3,107000080,1 +9561,3,108000050,1 +9562,3,109000050,1 +9563,3,103000060,1 +9564,3,103000070,1 +9565,3,103000080,1 +9566,3,110000050,1 +9567,3,106000060,1 +9568,3,106000070,1 +9569,3,106000080,1 +9570,3,101000070,1 +9571,3,110000060,1 +9572,3,104000060,1 +9573,3,104000070,1 +9574,3,104000080,1 +9575,3,102000050,1 +9576,3,104000170,1 +9577,3,104000260,1 +9578,3,111000010,1 +9579,3,111000020,1 +9580,3,111000030,1 +9581,3,112000020,1 +9582,3,112000030,1 +9583,3,108000060,1 +9584,3,108000070,1 +9585,3,108000080,1 +9586,3,107000050,1 +9587,3,112000010,1 +9588,3,110000070,1 +9589,3,110000080,1 +9590,3,118000020,1 +9591,3,118000030,1 +9592,3,118000040,1 +9593,4,101000090,1 +9594,4,101000100,1 +9595,4,101000110,1 +9596,4,109000100,1 +9597,4,105000100,1 +9598,4,105000110,1 +9599,4,108000090,1 +9600,4,110000090,1 +9601,4,102000100,1 +9602,4,102000110,1 +9603,4,106000090,1 +9604,4,109000090,1 +9605,4,107000100,1 +9606,4,103000090,1 +9607,4,102000090,1 +9608,4,103000100,1 +9609,4,106000100,1 +9610,4,106000110,1 +9611,4,104000090,1 +9612,4,104000100,1 +9613,4,104000110,1 +9614,4,107000090,1 +9615,4,104000180,1 +9616,4,111000040,1 +9617,4,112000040,1 +9618,4,108000100,1 +9619,4,105000090,1 +9620,4,110000100,1 +9621,4,118000050,1 +9622,4,118000060,1 +9623,5,101000120,1 +9624,5,109000110,1 +9625,5,105000120,1 +9626,5,102000120,1 +9627,5,107000110,1 +9628,5,103000120,1 +9629,5,106000120,1 +9630,5,104000120,1 +9631,5,104000190,1 +9632,5,111000060,1 +9633,5,112000060,1 +9634,5,108000110,1 +9635,5,110000110,1 +9636,5,118000070,1 +9637,1,201000010,8 +9638,1,292000010,8 +9639,1,299000040,8 +9640,1,299000070,8 +9641,1,299000110,8 +9642,1,299000120,8 +9643,1,299000140,8 +9644,2,202000010,8 +9645,2,290000010,8 +9646,2,299000010,8 +9647,2,299000150,8 +9648,2,299000190,8 +9649,2,299000200,8 +9650,2,299000210,8 +9651,3,298000050,8 +9652,3,298000060,8 +9653,3,299000060,8 +9654,3,299000170,8 +9655,3,290000120,8 +9656,3,291000050,8 +9657,3,292000020,8 +9658,4,299000670,8 +9659,4,299000680,8 +9660,4,204000010,8 +9661,4,209000040,8 +9662,5,297000100,8 +9663,5,291000020,8 +9664,5,297000130,8 +9665,5,297000140,8 +9666,5,203000010,8 +9667,5,206000030,8 +9668,3,170004,3 +9669,3,180004,3 +9670,4,140000,3 +9671,5,150010,3 +9672,5,150020,3 +9673,5,150030,3 +9674,5,150040,3 +9676,3,101000050,1 +9677,3,101000060,1 +9678,3,101000080,1 +9679,3,101000040,1 +9680,3,109000060,1 +9681,3,109000070,1 +9682,3,109000080,1 +9683,3,105000060,1 +9684,3,105000070,1 +9685,3,105000080,1 +9686,3,104000050,1 +9687,3,106000050,1 +9688,3,102000060,1 +9689,3,102000070,1 +9690,3,102000080,1 +9691,3,103000050,1 +9692,3,105000050,1 +9693,3,107000060,1 +9694,3,107000070,1 +9695,3,107000080,1 +9696,3,108000050,1 +9697,3,109000050,1 +9698,3,103000060,1 +9699,3,103000070,1 +9700,3,103000080,1 +9701,3,110000050,1 +9702,3,106000060,1 +9703,3,106000070,1 +9704,3,106000080,1 +9705,3,101000070,1 +9706,3,110000060,1 +9707,3,104000060,1 +9708,3,104000070,1 +9709,3,104000080,1 +9710,3,102000050,1 +9711,3,104000170,1 +9712,3,104000260,1 +9713,3,111000010,1 +9714,3,111000020,1 +9715,3,111000030,1 +9716,3,112000020,1 +9717,3,112000030,1 +9718,3,108000060,1 +9719,3,108000070,1 +9720,3,108000080,1 +9721,3,107000050,1 +9722,3,112000010,1 +9723,3,110000070,1 +9724,3,110000080,1 +9725,3,118000020,1 +9726,3,118000030,1 +9727,3,118000040,1 +9728,4,101000090,1 +9729,4,101000100,1 +9730,4,101000110,1 +9731,4,109000100,1 +9732,4,105000100,1 +9733,4,105000110,1 +9734,4,108000090,1 +9735,4,110000090,1 +9736,4,102000100,1 +9737,4,102000110,1 +9738,4,106000090,1 +9739,4,109000090,1 +9740,4,107000100,1 +9741,4,103000090,1 +9742,4,102000090,1 +9743,4,103000100,1 +9744,4,106000100,1 +9745,4,106000110,1 +9746,4,104000090,1 +9747,4,104000100,1 +9748,4,104000110,1 +9749,4,107000090,1 +9750,4,104000180,1 +9751,4,111000040,1 +9752,4,112000040,1 +9753,4,108000100,1 +9754,4,105000090,1 +9755,4,110000100,1 +9756,4,118000050,1 +9757,4,118000060,1 +9758,5,101000120,1 +9759,5,109000110,1 +9760,5,105000120,1 +9761,5,102000120,1 +9762,5,107000110,1 +9763,5,103000120,1 +9764,5,106000120,1 +9765,5,104000120,1 +9766,5,104000190,1 +9767,5,111000060,1 +9768,5,112000060,1 +9769,5,108000110,1 +9770,5,110000110,1 +9771,5,118000070,1 +9772,1,201000010,8 +9773,1,292000010,8 +9774,1,299000040,8 +9775,1,299000070,8 +9776,1,299000110,8 +9777,1,299000120,8 +9778,1,299000140,8 +9779,2,202000010,8 +9780,2,290000010,8 +9781,2,299000010,8 +9782,2,299000150,8 +9783,2,299000190,8 +9784,2,299000200,8 +9785,2,299000210,8 +9786,3,298000050,8 +9787,3,298000060,8 +9788,3,299000060,8 +9789,3,299000170,8 +9790,3,290000120,8 +9791,3,291000050,8 +9792,3,292000020,8 +9793,4,299000670,8 +9794,4,299000680,8 +9795,4,204000010,8 +9796,4,209000040,8 +9797,5,297000100,8 +9798,5,291000020,8 +9799,5,297000130,8 +9800,5,297000140,8 +9801,5,203000010,8 +9802,5,206000030,8 +9803,3,170004,3 +9804,3,180004,3 +9805,4,140000,3 +9806,5,150010,3 +9807,5,150020,3 +9808,5,150030,3 +9809,5,150040,3 +9810,5,190000,3 +9811,5,200000,3 +9812,5,210000,3 +9814,3,101000050,1 +9815,3,101000060,1 +9816,3,101000080,1 +9817,3,101000040,1 +9818,3,109000060,1 +9819,3,109000070,1 +9820,3,109000080,1 +9821,3,105000060,1 +9822,3,105000070,1 +9823,3,105000080,1 +9824,3,104000050,1 +9825,3,106000050,1 +9826,4,101000090,1 +9827,4,101000100,1 +9828,4,101000110,1 +9829,4,109000100,1 +9830,4,105000100,1 +9831,4,105000110,1 +9832,4,108000090,1 +9833,4,110000090,1 +9834,5,101000120,1 +9835,5,109000110,1 +9836,5,105000120,1 +9837,1,101000000,2 +9838,2,101000001,2 +9839,3,101000002,2 +9840,4,101000008,2 +9841,5,101000004,2 +9842,1,109000000,2 +9843,2,109000001,2 +9844,3,109000002,2 +9845,4,109000003,2 +9846,5,109000004,2 +9847,3,170004,3 +9848,4,170005,3 +9849,3,180004,3 +9850,4,180005,3 +9851,4,140000,3 +9852,4,150010,3 +9853,4,150020,3 +9854,4,150030,3 +9855,4,150040,3 +9857,3,102000060,1 +9858,3,102000070,1 +9859,3,102000080,1 +9860,3,103000050,1 +9861,3,105000050,1 +9862,3,107000060,1 +9863,3,107000070,1 +9864,3,107000080,1 +9865,3,108000050,1 +9866,3,109000050,1 +9867,3,103000060,1 +9868,3,103000070,1 +9869,3,103000080,1 +9870,3,110000050,1 +9871,4,102000100,1 +9872,4,102000350,1 +9873,4,102000110,1 +9874,4,106000090,1 +9875,4,109000090,1 +9876,4,107000100,1 +9877,4,103000090,1 +9878,4,102000090,1 +9879,4,103000100,1 +9880,5,102000120,1 +9881,5,107000110,1 +9882,5,103000120,1 +9883,1,102000000,2 +9884,2,102000001,2 +9885,3,102000002,2 +9886,4,102000003,2 +9887,5,102000004,2 +9888,1,105000000,2 +9889,2,105000001,2 +9890,3,105000002,2 +9891,4,105000003,2 +9892,5,105000004,2 +9893,1,112000000,2 +9894,2,112000001,2 +9895,3,112000002,2 +9896,4,112000003,2 +9897,5,112000004,2 +9898,4,110024,3 +9899,4,110034,3 +9900,4,110044,3 +9901,4,110054,3 +9902,3,110060,3 +9903,3,110070,3 +9904,3,110080,3 +9905,3,110090,3 +9906,3,110100,3 +9907,3,110110,3 +9908,3,110120,3 +9909,3,110130,3 +9910,3,110140,3 +9911,3,110150,3 +9912,3,110160,3 +9913,3,110170,3 +9914,3,110180,3 +9915,3,110190,3 +9916,3,110200,3 +9917,3,110210,3 +9918,3,110220,3 +9919,3,110230,3 +9920,3,110240,3 +9921,3,110250,3 +9922,3,110260,3 +9923,3,110270,3 +9924,3,110620,3 +9925,3,110670,3 +9926,4,140000,3 +9927,4,150010,3 +9928,4,150020,3 +9929,4,150030,3 +9930,4,150040,3 +9932,3,118000020,1 +9933,3,118000030,1 +9934,3,118000040,1 +9935,3,106000060,1 +9936,3,106000070,1 +9937,3,106000080,1 +9938,3,101000070,1 +9939,3,110000060,1 +9940,3,104000060,1 +9941,3,104000070,1 +9942,3,104000080,1 +9943,3,102000050,1 +9944,3,104000170,1 +9945,3,104000260,1 +9946,4,118000050,1 +9947,4,118000060,1 +9948,4,106000100,1 +9949,4,106000110,1 +9950,4,104000090,1 +9951,4,104000100,1 +9952,4,104000110,1 +9953,4,104000270,1 +9954,4,107000090,1 +9955,4,104000180,1 +9956,5,118000070,1 +9957,5,106000120,1 +9958,5,104000120,1 +9959,5,104000190,1 +9960,1,103000000,2 +9961,2,103000001,2 +9962,3,103000002,2 +9963,4,103000003,2 +9964,5,103000004,2 +9965,1,111000000,2 +9966,2,111000001,2 +9967,3,111000002,2 +9968,4,111000003,2 +9969,5,111000004,2 +9970,1,115000000,2 +9971,2,115000001,2 +9972,3,115000002,2 +9973,4,115000003,2 +9974,5,115000004,2 +9975,4,120024,3 +9976,4,120034,3 +9977,4,120044,3 +9978,4,120054,3 +9979,3,120241,3 +9980,3,120251,3 +9981,3,120261,3 +9982,3,120271,3 +9983,3,120300,3 +9984,3,120310,3 +9985,3,120320,3 +9986,3,120330,3 +9987,3,120340,3 +9988,3,120350,3 +9989,3,120360,3 +9990,3,120370,3 +9991,3,120380,3 +9992,3,120390,3 +9993,3,120400,3 +9994,3,120410,3 +9995,3,120420,3 +9996,3,120430,3 +9997,3,120450,3 +9998,3,120460,3 +9999,3,120550,3 +10000,3,120560,3 +10001,3,120570,3 +10002,3,120990,3 +10003,3,121000,3 +10004,3,121010,3 +10005,3,121020,3 +10006,4,140000,3 +10007,4,150010,3 +10008,4,150020,3 +10009,4,150030,3 +10010,4,150040,3 +10012,3,111000010,1 +10013,3,111000020,1 +10014,3,111000030,1 +10015,3,112000020,1 +10016,3,112000030,1 +10017,3,108000060,1 +10018,3,108000070,1 +10019,3,108000080,1 +10020,3,107000050,1 +10021,3,112000010,1 +10022,3,110000070,1 +10023,3,110000080,1 +10024,4,111000040,1 +10025,4,112000040,1 +10026,4,108000100,1 +10027,4,105000090,1 +10028,4,110000100,1 +10029,5,111000060,1 +10030,5,112000060,1 +10031,5,108000110,1 +10032,5,110000110,1 +10033,1,108000000,2 +10034,2,108000001,2 +10035,3,108000002,2 +10036,4,108000003,2 +10037,5,108000004,2 +10038,1,107000000,2 +10039,2,107000001,2 +10040,3,107000002,2 +10041,4,107000003,2 +10042,5,107000004,2 +10043,1,120000000,2 +10044,2,120000001,2 +10045,3,120000002,2 +10046,4,120000003,2 +10047,5,120000004,2 +10048,4,130024,3 +10049,4,130034,3 +10050,4,130044,3 +10051,4,130054,3 +10052,3,130060,3 +10053,3,130070,3 +10054,3,130080,3 +10055,3,130090,3 +10056,3,130100,3 +10057,3,130110,3 +10058,3,130120,3 +10059,3,130130,3 +10060,3,130140,3 +10061,3,130150,3 +10062,3,130160,3 +10063,3,130170,3 +10064,3,130180,3 +10065,3,130190,3 +10066,3,130200,3 +10067,3,130420,3 +10068,3,130510,3 +10069,3,130520,3 +10070,3,130531,3 +10071,3,130540,3 +10072,3,130660,3 +10073,3,130790,3 +10074,3,130800,3 +10075,3,131130,3 +10076,4,140000,3 +10077,4,150010,3 +10078,4,150020,3 +10079,4,150030,3 +10080,4,150040,3 +10082,3,111000010,1 +10083,3,111000020,1 +10084,3,111000030,1 +10085,3,112000020,1 +10086,3,112000030,1 +10087,3,108000060,1 +10088,3,108000070,1 +10089,3,108000080,1 +10090,3,107000050,1 +10091,3,112000010,1 +10092,3,110000070,1 +10093,3,110000080,1 +10094,4,111000040,1 +10095,4,112000040,1 +10096,4,108000100,1 +10097,4,105000090,1 +10098,4,110000100,1 +10099,5,111000060,1 +10100,5,112000060,1 +10101,5,108000110,1 +10102,5,110000110,1 +10103,1,108000000,2 +10104,2,108000001,2 +10105,3,108000002,2 +10106,4,108000003,2 +10107,5,108000004,2 +10108,1,107000000,2 +10109,2,107000001,2 +10110,3,107000002,2 +10111,4,107000003,2 +10112,5,107000004,2 +10113,1,120000000,2 +10114,2,120000001,2 +10115,3,120000002,2 +10116,4,120000003,2 +10117,5,120000004,2 +10118,3,170004,3 +10119,4,170005,3 +10120,3,180004,3 +10121,4,180005,3 +10122,4,140000,3 +10123,4,150010,3 +10124,4,150020,3 +10125,4,150030,3 +10126,4,150040,3 +10128,3,101000050,1 +10129,3,101000060,1 +10130,3,101000080,1 +10131,3,101000040,1 +10132,3,109000060,1 +10133,3,109000070,1 +10134,3,109000080,1 +10135,3,105000060,1 +10136,3,105000070,1 +10137,3,105000080,1 +10138,3,104000050,1 +10139,3,106000050,1 +10140,4,101000090,1 +10141,4,101000100,1 +10142,4,101000110,1 +10143,4,109000100,1 +10144,4,105000100,1 +10145,4,105000110,1 +10146,4,108000090,1 +10147,4,110000090,1 +10148,5,101000120,1 +10149,5,109000110,1 +10150,5,105000120,1 +10151,1,101000000,2 +10152,2,101000001,2 +10153,3,101000002,2 +10154,4,101000008,2 +10155,5,101000004,2 +10156,1,109000000,2 +10157,2,109000001,2 +10158,3,109000002,2 +10159,4,109000003,2 +10160,5,109000004,2 +10161,4,110024,3 +10162,4,110034,3 +10163,4,110044,3 +10164,4,110054,3 +10165,3,110060,3 +10166,3,110070,3 +10167,3,110080,3 +10168,3,110090,3 +10169,3,110100,3 +10170,3,110110,3 +10171,3,110120,3 +10172,3,110130,3 +10173,3,110140,3 +10174,3,110150,3 +10175,3,110160,3 +10176,3,110170,3 +10177,3,110180,3 +10178,3,110190,3 +10179,3,110200,3 +10180,3,110210,3 +10181,3,110220,3 +10182,3,110230,3 +10183,3,110240,3 +10184,3,110250,3 +10185,3,110260,3 +10186,3,110270,3 +10187,3,110620,3 +10188,3,110670,3 +10189,4,140000,3 +10190,4,150010,3 +10191,4,150020,3 +10192,4,150030,3 +10193,4,150040,3 +10195,3,102000060,1 +10196,3,102000070,1 +10197,3,102000080,1 +10198,3,103000050,1 +10199,3,105000050,1 +10200,3,107000060,1 +10201,3,107000070,1 +10202,3,107000080,1 +10203,3,108000050,1 +10204,3,109000050,1 +10205,3,103000060,1 +10206,3,103000070,1 +10207,3,103000080,1 +10208,3,110000050,1 +10209,4,102000100,1 +10210,4,102000110,1 +10211,4,102000350,1 +10212,4,106000090,1 +10213,4,109000090,1 +10214,4,107000100,1 +10215,4,103000090,1 +10216,4,102000090,1 +10217,4,103000100,1 +10218,5,102000120,1 +10219,5,107000110,1 +10220,5,103000120,1 +10221,1,102000000,2 +10222,2,102000001,2 +10223,3,102000002,2 +10224,4,102000003,2 +10225,5,102000004,2 +10226,1,105000000,2 +10227,2,105000001,2 +10228,3,105000002,2 +10229,4,105000003,2 +10230,5,105000004,2 +10231,1,112000000,2 +10232,2,112000001,2 +10233,3,112000002,2 +10234,4,112000003,2 +10235,5,112000004,2 +10236,4,120024,3 +10237,4,120034,3 +10238,4,120044,3 +10239,4,120054,3 +10240,3,120241,3 +10241,3,120251,3 +10242,3,120261,3 +10243,3,120271,3 +10244,3,120300,3 +10245,3,120310,3 +10246,3,120320,3 +10247,3,120330,3 +10248,3,120340,3 +10249,3,120350,3 +10250,3,120360,3 +10251,3,120370,3 +10252,3,120380,3 +10253,3,120390,3 +10254,3,120400,3 +10255,3,120410,3 +10256,3,120420,3 +10257,3,120430,3 +10258,3,120450,3 +10259,3,120460,3 +10260,3,120550,3 +10261,3,120560,3 +10262,3,120570,3 +10263,3,120990,3 +10264,3,121000,3 +10265,3,121010,3 +10266,3,121020,3 +10267,3,121100,3 +10268,4,140000,3 +10269,4,150010,3 +10270,4,150020,3 +10271,4,150030,3 +10272,4,150040,3 +10274,3,118000020,1 +10275,3,118000030,1 +10276,3,118000040,1 +10277,3,106000060,1 +10278,3,106000070,1 +10279,3,106000080,1 +10280,3,101000070,1 +10281,3,110000060,1 +10282,3,104000060,1 +10283,3,104000070,1 +10284,3,104000080,1 +10285,3,102000050,1 +10286,3,104000170,1 +10287,3,104000260,1 +10288,4,118000050,1 +10289,4,118000060,1 +10290,4,106000100,1 +10291,4,106000110,1 +10292,4,104000090,1 +10293,4,104000100,1 +10294,4,104000110,1 +10295,4,104000270,1 +10296,4,107000090,1 +10297,4,104000180,1 +10298,5,118000070,1 +10299,5,106000120,1 +10300,5,104000120,1 +10301,5,104000190,1 +10302,1,103000000,2 +10303,2,103000001,2 +10304,3,103000002,2 +10305,4,103000003,2 +10306,5,103000004,2 +10307,1,111000000,2 +10308,2,111000001,2 +10309,3,111000002,2 +10310,4,111000003,2 +10311,5,111000004,2 +10312,1,115000000,2 +10313,2,115000001,2 +10314,3,115000002,2 +10315,4,115000003,2 +10316,5,115000004,2 +10317,4,130024,3 +10318,4,130034,3 +10319,4,130044,3 +10320,4,130054,3 +10321,3,130060,3 +10322,3,130070,3 +10323,3,130080,3 +10324,3,130090,3 +10325,3,130100,3 +10326,3,130110,3 +10327,3,130120,3 +10328,3,130130,3 +10329,3,130140,3 +10330,3,130150,3 +10331,3,130160,3 +10332,3,130170,3 +10333,3,130180,3 +10334,3,130190,3 +10335,3,130200,3 +10336,3,130420,3 +10337,3,130510,3 +10338,3,130520,3 +10339,3,130531,3 +10340,3,130540,3 +10341,3,130660,3 +10342,3,130790,3 +10343,3,130800,3 +10344,3,131130,3 +10345,4,140000,3 +10346,4,150010,3 +10347,4,150020,3 +10348,4,150030,3 +10349,4,150040,3 +10351,1,101000010,1 +10352,1,102000010,1 +10353,1,103000010,1 +10354,1,104000010,1 +10355,1,105000010,1 +10356,1,106000010,1 +10357,1,107000010,1 +10358,1,108000010,1 +10359,1,109000010,1 +10360,1,110000010,1 +10361,2,101000020,1 +10362,2,101000030,1 +10363,2,102000020,1 +10364,2,102000030,1 +10365,2,102000040,1 +10366,2,103000020,1 +10367,2,103000030,1 +10368,2,103000040,1 +10369,2,104000020,1 +10370,2,104000030,1 +10371,2,104000040,1 +10372,2,105000020,1 +10373,2,105000030,1 +10374,2,105000040,1 +10375,2,106000020,1 +10376,2,106000030,1 +10377,2,106000040,1 +10378,2,107000020,1 +10379,2,107000030,1 +10380,2,107000040,1 +10381,2,108000020,1 +10382,2,108000030,1 +10383,2,108000040,1 +10384,2,109000020,1 +10385,2,109000030,1 +10386,2,109000040,1 +10387,2,110000020,1 +10388,2,110000030,1 +10389,2,110000040,1 +10390,2,118000010,1 +10391,3,101000050,1 +10392,3,101000060,1 +10393,3,101000080,1 +10394,3,101000040,1 +10395,3,109000060,1 +10396,3,109000070,1 +10397,3,109000080,1 +10398,3,105000060,1 +10399,3,105000070,1 +10400,3,105000080,1 +10401,3,104000050,1 +10402,3,106000050,1 +10403,3,102000060,1 +10404,3,102000070,1 +10405,3,102000080,1 +10406,3,103000050,1 +10407,3,105000050,1 +10408,3,107000060,1 +10409,3,107000070,1 +10410,3,107000080,1 +10411,3,108000050,1 +10412,3,109000050,1 +10413,3,103000060,1 +10414,3,103000070,1 +10415,3,103000080,1 +10416,3,110000050,1 +10417,3,106000060,1 +10418,3,106000070,1 +10419,3,106000080,1 +10420,3,101000070,1 +10421,3,110000060,1 +10422,3,104000060,1 +10423,3,104000070,1 +10424,3,104000080,1 +10425,3,102000050,1 +10426,3,104000170,1 +10427,3,104000260,1 +10428,3,111000010,1 +10429,3,111000020,1 +10430,3,111000030,1 +10431,3,112000020,1 +10432,3,112000030,1 +10433,3,108000060,1 +10434,3,108000070,1 +10435,3,108000080,1 +10436,3,107000050,1 +10437,3,112000010,1 +10438,3,110000070,1 +10439,3,110000080,1 +10440,3,118000020,1 +10441,3,118000030,1 +10442,3,118000040,1 +10443,4,101000090,1 +10444,4,101000100,1 +10445,4,101000110,1 +10446,4,109000100,1 +10447,4,105000100,1 +10448,4,105000110,1 +10449,4,108000090,1 +10450,4,110000090,1 +10451,4,102000100,1 +10452,4,102000110,1 +10453,4,106000090,1 +10454,4,109000090,1 +10455,4,107000100,1 +10456,4,103000090,1 +10457,4,102000090,1 +10458,4,103000100,1 +10459,4,106000100,1 +10460,4,106000110,1 +10461,4,104000090,1 +10462,4,104000100,1 +10463,4,104000110,1 +10464,4,107000090,1 +10465,4,104000180,1 +10466,4,111000040,1 +10467,4,112000040,1 +10468,4,108000100,1 +10469,4,105000090,1 +10470,4,110000100,1 +10471,4,118000050,1 +10472,4,118000060,1 +10473,5,101000120,1 +10474,5,109000110,1 +10475,5,105000120,1 +10476,5,102000120,1 +10477,5,107000110,1 +10478,5,103000120,1 +10479,5,106000120,1 +10480,5,104000120,1 +10481,5,104000190,1 +10482,5,111000060,1 +10483,5,112000060,1 +10484,5,108000110,1 +10485,5,110000110,1 +10486,5,118000070,1 +10487,1,201000010,8 +10488,1,292000010,8 +10489,1,299000040,8 +10490,1,299000070,8 +10491,1,299000110,8 +10492,1,299000120,8 +10493,1,299000140,8 +10494,2,202000010,8 +10495,2,290000010,8 +10496,2,299000010,8 +10497,2,299000150,8 +10498,2,299000190,8 +10499,2,299000200,8 +10500,2,299000210,8 +10501,3,298000050,8 +10502,3,298000060,8 +10503,3,299000060,8 +10504,3,299000170,8 +10505,3,290000120,8 +10506,3,291000050,8 +10507,3,292000020,8 +10508,4,299000670,8 +10509,4,299000680,8 +10510,4,204000010,8 +10511,4,209000040,8 +10512,4,202000070,8 +10513,5,297000100,8 +10514,5,291000020,8 +10515,5,297000130,8 +10516,5,297000140,8 +10517,5,203000010,8 +10518,5,206000030,8 +10519,5,203000050,8 +10520,1,170002,3 +10521,1,180002,3 +10522,2,170003,3 +10523,2,180003,3 +10524,3,170004,3 +10525,3,180004,3 +10526,4,140000,3 +10527,5,150010,3 +10528,5,150020,3 +10529,5,150030,3 +10530,5,150040,3 +10532,1,101000010,1 +10533,1,102000010,1 +10534,1,103000010,1 +10535,1,104000010,1 +10536,1,105000010,1 +10537,1,106000010,1 +10538,1,107000010,1 +10539,1,108000010,1 +10540,1,109000010,1 +10541,1,110000010,1 +10542,2,101000020,1 +10543,2,101000030,1 +10544,2,102000020,1 +10545,2,102000030,1 +10546,2,102000040,1 +10547,2,103000020,1 +10548,2,103000030,1 +10549,2,103000040,1 +10550,2,104000020,1 +10551,2,104000030,1 +10552,2,104000040,1 +10553,2,105000020,1 +10554,2,105000030,1 +10555,2,105000040,1 +10556,2,106000020,1 +10557,2,106000030,1 +10558,2,106000040,1 +10559,2,107000020,1 +10560,2,107000030,1 +10561,2,107000040,1 +10562,2,108000020,1 +10563,2,108000030,1 +10564,2,108000040,1 +10565,2,109000020,1 +10566,2,109000030,1 +10567,2,109000040,1 +10568,2,110000020,1 +10569,2,110000030,1 +10570,2,110000040,1 +10571,2,118000010,1 +10572,3,101000050,1 +10573,3,101000060,1 +10574,3,101000080,1 +10575,3,101000040,1 +10576,3,109000060,1 +10577,3,109000070,1 +10578,3,109000080,1 +10579,3,105000060,1 +10580,3,105000070,1 +10581,3,105000080,1 +10582,3,104000050,1 +10583,3,106000050,1 +10584,3,102000060,1 +10585,3,102000070,1 +10586,3,102000080,1 +10587,3,103000050,1 +10588,3,105000050,1 +10589,3,107000060,1 +10590,3,107000070,1 +10591,3,107000080,1 +10592,3,108000050,1 +10593,3,109000050,1 +10594,3,103000060,1 +10595,3,103000070,1 +10596,3,103000080,1 +10597,3,110000050,1 +10598,3,106000060,1 +10599,3,106000070,1 +10600,3,106000080,1 +10601,3,101000070,1 +10602,3,110000060,1 +10603,3,104000060,1 +10604,3,104000070,1 +10605,3,104000080,1 +10606,3,102000050,1 +10607,3,104000170,1 +10608,3,104000260,1 +10609,3,111000010,1 +10610,3,111000020,1 +10611,3,111000030,1 +10612,3,112000020,1 +10613,3,112000030,1 +10614,3,108000060,1 +10615,3,108000070,1 +10616,3,108000080,1 +10617,3,107000050,1 +10618,3,112000010,1 +10619,3,110000070,1 +10620,3,110000080,1 +10621,3,118000020,1 +10622,3,118000030,1 +10623,3,118000040,1 +10624,4,101000090,1 +10625,4,101000100,1 +10626,4,101000110,1 +10627,4,109000100,1 +10628,4,105000100,1 +10629,4,105000110,1 +10630,4,108000090,1 +10631,4,110000090,1 +10632,4,102000100,1 +10633,4,102000110,1 +10634,4,106000090,1 +10635,4,109000090,1 +10636,4,107000100,1 +10637,4,103000090,1 +10638,4,102000090,1 +10639,4,103000100,1 +10640,4,106000100,1 +10641,4,106000110,1 +10642,4,104000090,1 +10643,4,104000100,1 +10644,4,104000110,1 +10645,4,107000090,1 +10646,4,104000180,1 +10647,4,111000040,1 +10648,4,112000040,1 +10649,4,108000100,1 +10650,4,105000090,1 +10651,4,110000100,1 +10652,4,118000050,1 +10653,4,118000060,1 +10654,5,101000120,1 +10655,5,109000110,1 +10656,5,105000120,1 +10657,5,102000120,1 +10658,5,107000110,1 +10659,5,103000120,1 +10660,5,106000120,1 +10661,5,104000120,1 +10662,5,104000190,1 +10663,5,111000060,1 +10664,5,112000060,1 +10665,5,108000110,1 +10666,5,110000110,1 +10667,5,118000070,1 +10668,1,201000010,8 +10669,1,292000010,8 +10670,1,299000040,8 +10671,1,299000070,8 +10672,1,299000110,8 +10673,1,299000120,8 +10674,1,299000140,8 +10675,2,202000010,8 +10676,2,290000010,8 +10677,2,299000010,8 +10678,2,299000150,8 +10679,2,299000190,8 +10680,2,299000200,8 +10681,2,299000210,8 +10682,3,298000050,8 +10683,3,298000060,8 +10684,3,299000060,8 +10685,3,299000170,8 +10686,3,290000120,8 +10687,3,291000050,8 +10688,3,292000020,8 +10689,4,299000670,8 +10690,4,299000680,8 +10691,4,204000010,8 +10692,4,209000040,8 +10693,4,202000070,8 +10694,5,297000100,8 +10695,5,291000020,8 +10696,5,297000130,8 +10697,5,297000140,8 +10698,5,203000010,8 +10699,5,206000030,8 +10700,5,203000050,8 +10701,2,170003,3 +10702,2,180003,3 +10703,3,170004,3 +10704,3,180004,3 +10705,4,140000,3 +10706,5,150010,3 +10707,5,150020,3 +10708,5,150030,3 +10709,5,150040,3 +10711,3,101000050,1 +10712,3,101000060,1 +10713,3,101000080,1 +10714,3,101000040,1 +10715,3,109000060,1 +10716,3,109000070,1 +10717,3,109000080,1 +10718,3,105000060,1 +10719,3,105000070,1 +10720,3,105000080,1 +10721,3,104000050,1 +10722,3,106000050,1 +10723,3,102000060,1 +10724,3,102000070,1 +10725,3,102000080,1 +10726,3,103000050,1 +10727,3,105000050,1 +10728,3,107000060,1 +10729,3,107000070,1 +10730,3,107000080,1 +10731,3,108000050,1 +10732,3,109000050,1 +10733,3,103000060,1 +10734,3,103000070,1 +10735,3,103000080,1 +10736,3,110000050,1 +10737,3,106000060,1 +10738,3,106000070,1 +10739,3,106000080,1 +10740,3,101000070,1 +10741,3,110000060,1 +10742,3,104000060,1 +10743,3,104000070,1 +10744,3,104000080,1 +10745,3,102000050,1 +10746,3,104000170,1 +10747,3,104000260,1 +10748,3,111000010,1 +10749,3,111000020,1 +10750,3,111000030,1 +10751,3,112000020,1 +10752,3,112000030,1 +10753,3,108000060,1 +10754,3,108000070,1 +10755,3,108000080,1 +10756,3,107000050,1 +10757,3,112000010,1 +10758,3,110000070,1 +10759,3,110000080,1 +10760,3,118000020,1 +10761,3,118000030,1 +10762,3,118000040,1 +10763,4,101000090,1 +10764,4,101000100,1 +10765,4,101000110,1 +10766,4,109000100,1 +10767,4,105000100,1 +10768,4,105000110,1 +10769,4,108000090,1 +10770,4,110000090,1 +10771,4,102000100,1 +10772,4,102000110,1 +10773,4,106000090,1 +10774,4,109000090,1 +10775,4,107000100,1 +10776,4,103000090,1 +10777,4,102000090,1 +10778,4,103000100,1 +10779,4,106000100,1 +10780,4,106000110,1 +10781,4,104000090,1 +10782,4,104000100,1 +10783,4,104000110,1 +10784,4,107000090,1 +10785,4,104000180,1 +10786,4,111000040,1 +10787,4,112000040,1 +10788,4,108000100,1 +10789,4,105000090,1 +10790,4,110000100,1 +10791,4,118000050,1 +10792,4,118000060,1 +10793,5,101000120,1 +10794,5,109000110,1 +10795,5,105000120,1 +10796,5,102000120,1 +10797,5,107000110,1 +10798,5,103000120,1 +10799,5,106000120,1 +10800,5,104000120,1 +10801,5,104000190,1 +10802,5,111000060,1 +10803,5,112000060,1 +10804,5,108000110,1 +10805,5,110000110,1 +10806,5,118000070,1 +10807,1,201000010,8 +10808,1,292000010,8 +10809,1,299000040,8 +10810,1,299000070,8 +10811,1,299000110,8 +10812,1,299000120,8 +10813,1,299000140,8 +10814,2,202000010,8 +10815,2,290000010,8 +10816,2,299000010,8 +10817,2,299000150,8 +10818,2,299000190,8 +10819,2,299000200,8 +10820,2,299000210,8 +10821,3,298000050,8 +10822,3,298000060,8 +10823,3,299000060,8 +10824,3,299000170,8 +10825,3,290000120,8 +10826,3,291000050,8 +10827,3,292000020,8 +10828,4,299000670,8 +10829,4,299000680,8 +10830,4,204000010,8 +10831,4,209000040,8 +10832,4,202000070,8 +10833,5,297000100,8 +10834,5,291000020,8 +10835,5,297000130,8 +10836,5,297000140,8 +10837,5,203000010,8 +10838,5,206000030,8 +10839,5,203000050,8 +10840,3,170004,3 +10841,3,180004,3 +10842,4,140000,3 +10843,5,150010,3 +10844,5,150020,3 +10845,5,150030,3 +10846,5,150040,3 +10848,3,101000050,1 +10849,3,101000060,1 +10850,3,101000080,1 +10851,3,101000040,1 +10852,3,109000060,1 +10853,3,109000070,1 +10854,3,109000080,1 +10855,3,105000060,1 +10856,3,105000070,1 +10857,3,105000080,1 +10858,3,104000050,1 +10859,3,106000050,1 +10860,3,102000060,1 +10861,3,102000070,1 +10862,3,102000080,1 +10863,3,103000050,1 +10864,3,105000050,1 +10865,3,107000060,1 +10866,3,107000070,1 +10867,3,107000080,1 +10868,3,108000050,1 +10869,3,109000050,1 +10870,3,103000060,1 +10871,3,103000070,1 +10872,3,103000080,1 +10873,3,110000050,1 +10874,3,106000060,1 +10875,3,106000070,1 +10876,3,106000080,1 +10877,3,101000070,1 +10878,3,110000060,1 +10879,3,104000060,1 +10880,3,104000070,1 +10881,3,104000080,1 +10882,3,102000050,1 +10883,3,104000170,1 +10884,3,104000260,1 +10885,3,111000010,1 +10886,3,111000020,1 +10887,3,111000030,1 +10888,3,112000020,1 +10889,3,112000030,1 +10890,3,108000060,1 +10891,3,108000070,1 +10892,3,108000080,1 +10893,3,107000050,1 +10894,3,112000010,1 +10895,3,110000070,1 +10896,3,110000080,1 +10897,3,118000020,1 +10898,3,118000030,1 +10899,3,118000040,1 +10900,4,101000090,1 +10901,4,101000100,1 +10902,4,101000110,1 +10903,4,109000100,1 +10904,4,105000100,1 +10905,4,105000110,1 +10906,4,108000090,1 +10907,4,110000090,1 +10908,4,102000100,1 +10909,4,102000110,1 +10910,4,106000090,1 +10911,4,109000090,1 +10912,4,107000100,1 +10913,4,103000090,1 +10914,4,102000090,1 +10915,4,103000100,1 +10916,4,106000100,1 +10917,4,106000110,1 +10918,4,104000090,1 +10919,4,104000100,1 +10920,4,104000110,1 +10921,4,107000090,1 +10922,4,104000180,1 +10923,4,111000040,1 +10924,4,112000040,1 +10925,4,108000100,1 +10926,4,105000090,1 +10927,4,110000100,1 +10928,4,118000050,1 +10929,4,118000060,1 +10930,5,101000120,1 +10931,5,109000110,1 +10932,5,105000120,1 +10933,5,102000120,1 +10934,5,107000110,1 +10935,5,103000120,1 +10936,5,106000120,1 +10937,5,104000120,1 +10938,5,104000190,1 +10939,5,111000060,1 +10940,5,112000060,1 +10941,5,108000110,1 +10942,5,110000110,1 +10943,5,118000070,1 +10944,1,201000010,8 +10945,1,292000010,8 +10946,1,299000040,8 +10947,1,299000070,8 +10948,1,299000110,8 +10949,1,299000120,8 +10950,1,299000140,8 +10951,2,202000010,8 +10952,2,290000010,8 +10953,2,299000010,8 +10954,2,299000150,8 +10955,2,299000190,8 +10956,2,299000200,8 +10957,2,299000210,8 +10958,3,298000050,8 +10959,3,298000060,8 +10960,3,299000060,8 +10961,3,299000170,8 +10962,3,290000120,8 +10963,3,291000050,8 +10964,3,292000020,8 +10965,4,299000670,8 +10966,4,299000680,8 +10967,4,204000010,8 +10968,4,209000040,8 +10969,4,202000070,8 +10970,5,297000100,8 +10971,5,291000020,8 +10972,5,297000130,8 +10973,5,297000140,8 +10974,5,203000010,8 +10975,5,206000030,8 +10976,5,203000050,8 +10977,3,170004,3 +10978,3,180004,3 +10979,4,140000,3 +10980,5,150010,3 +10981,5,150020,3 +10982,5,150030,3 +10983,5,150040,3 +10985,3,101000050,1 +10986,3,101000060,1 +10987,3,101000080,1 +10988,3,101000040,1 +10989,3,109000060,1 +10990,3,109000070,1 +10991,3,109000080,1 +10992,3,105000060,1 +10993,3,105000070,1 +10994,3,105000080,1 +10995,3,104000050,1 +10996,3,106000050,1 +10997,3,102000060,1 +10998,3,102000070,1 +10999,3,102000080,1 +11000,3,103000050,1 +11001,3,105000050,1 +11002,3,107000060,1 +11003,3,107000070,1 +11004,3,107000080,1 +11005,3,108000050,1 +11006,3,109000050,1 +11007,3,103000060,1 +11008,3,103000070,1 +11009,3,103000080,1 +11010,3,110000050,1 +11011,3,106000060,1 +11012,3,106000070,1 +11013,3,106000080,1 +11014,3,101000070,1 +11015,3,110000060,1 +11016,3,104000060,1 +11017,3,104000070,1 +11018,3,104000080,1 +11019,3,102000050,1 +11020,3,104000170,1 +11021,3,104000260,1 +11022,3,111000010,1 +11023,3,111000020,1 +11024,3,111000030,1 +11025,3,112000020,1 +11026,3,112000030,1 +11027,3,108000060,1 +11028,3,108000070,1 +11029,3,108000080,1 +11030,3,107000050,1 +11031,3,112000010,1 +11032,3,110000070,1 +11033,3,110000080,1 +11034,3,118000020,1 +11035,3,118000030,1 +11036,3,118000040,1 +11037,4,101000090,1 +11038,4,101000100,1 +11039,4,101000110,1 +11040,4,109000100,1 +11041,4,105000100,1 +11042,4,105000110,1 +11043,4,108000090,1 +11044,4,110000090,1 +11045,4,102000100,1 +11046,4,102000110,1 +11047,4,106000090,1 +11048,4,109000090,1 +11049,4,107000100,1 +11050,4,103000090,1 +11051,4,102000090,1 +11052,4,103000100,1 +11053,4,106000100,1 +11054,4,106000110,1 +11055,4,104000090,1 +11056,4,104000100,1 +11057,4,104000110,1 +11058,4,107000090,1 +11059,4,104000180,1 +11060,4,111000040,1 +11061,4,112000040,1 +11062,4,108000100,1 +11063,4,105000090,1 +11064,4,110000100,1 +11065,4,118000050,1 +11066,4,118000060,1 +11067,5,101000120,1 +11068,5,109000110,1 +11069,5,105000120,1 +11070,5,102000120,1 +11071,5,107000110,1 +11072,5,103000120,1 +11073,5,106000120,1 +11074,5,104000120,1 +11075,5,104000190,1 +11076,5,111000060,1 +11077,5,112000060,1 +11078,5,108000110,1 +11079,5,110000110,1 +11080,5,118000070,1 +11081,1,201000010,8 +11082,1,292000010,8 +11083,1,299000040,8 +11084,1,299000070,8 +11085,1,299000110,8 +11086,1,299000120,8 +11087,1,299000140,8 +11088,2,202000010,8 +11089,2,290000010,8 +11090,2,299000010,8 +11091,2,299000150,8 +11092,2,299000190,8 +11093,2,299000200,8 +11094,2,299000210,8 +11095,3,298000050,8 +11096,3,298000060,8 +11097,3,299000060,8 +11098,3,299000170,8 +11099,3,290000120,8 +11100,3,291000050,8 +11101,3,292000020,8 +11102,4,299000670,8 +11103,4,299000680,8 +11104,4,204000010,8 +11105,4,209000040,8 +11106,4,202000070,8 +11107,5,297000100,8 +11108,5,291000020,8 +11109,5,297000130,8 +11110,5,297000140,8 +11111,5,203000010,8 +11112,5,206000030,8 +11113,5,203000050,8 +11114,3,170004,3 +11115,3,180004,3 +11116,4,140000,3 +11117,5,150010,3 +11118,5,150020,3 +11119,5,150030,3 +11120,5,150040,3 +11121,5,190000,3 +11122,5,200000,3 +11123,5,210000,3 +11125,3,118000020,1 +11126,3,118000030,1 +11127,3,118000040,1 +11128,3,106000060,1 +11129,3,106000070,1 +11130,3,106000080,1 +11131,3,101000070,1 +11132,3,110000060,1 +11133,3,104000060,1 +11134,3,104000070,1 +11135,3,104000080,1 +11136,3,102000050,1 +11137,3,104000170,1 +11138,3,104000260,1 +11139,4,118000050,1 +11140,4,118000060,1 +11141,4,106000100,1 +11142,4,106000110,1 +11143,4,104000090,1 +11144,4,104000100,1 +11145,4,104000110,1 +11146,4,104000270,1 +11147,4,107000090,1 +11148,4,104000180,1 +11149,5,118000070,1 +11150,5,106000120,1 +11151,5,104000120,1 +11152,5,104000190,1 +11153,1,103000000,2 +11154,2,103000001,2 +11155,3,103000002,2 +11156,4,103000003,2 +11157,5,103000004,2 +11158,1,111000000,2 +11159,2,111000001,2 +11160,3,111000002,2 +11161,4,111000003,2 +11162,5,111000004,2 +11163,1,115000000,2 +11164,2,115000001,2 +11165,3,115000002,2 +11166,4,115000003,2 +11167,5,115000004,2 +11168,3,170004,3 +11169,4,170005,3 +11170,3,180004,3 +11171,4,180005,3 +11172,4,140000,3 +11173,4,150010,3 +11174,4,150020,3 +11175,4,150030,3 +11176,4,150040,3 +11178,3,111000010,1 +11179,3,111000020,1 +11180,3,111000030,1 +11181,3,112000020,1 +11182,3,112000030,1 +11183,3,108000060,1 +11184,3,108000070,1 +11185,3,108000080,1 +11186,3,107000050,1 +11187,3,112000010,1 +11188,3,110000070,1 +11189,3,110000080,1 +11190,4,111000040,1 +11191,4,112000040,1 +11192,4,108000100,1 +11193,4,105000090,1 +11194,4,110000100,1 +11195,5,111000060,1 +11196,5,112000060,1 +11197,5,108000110,1 +11198,5,110000110,1 +11199,1,108000000,2 +11200,2,108000001,2 +11201,3,108000002,2 +11202,4,108000003,2 +11203,5,108000004,2 +11204,1,107000000,2 +11205,2,107000001,2 +11206,3,107000002,2 +11207,4,107000003,2 +11208,5,107000004,2 +11209,1,120000000,2 +11210,2,120000001,2 +11211,3,120000002,2 +11212,4,120000003,2 +11213,5,120000004,2 +11214,4,110024,3 +11215,4,110034,3 +11216,4,110044,3 +11217,4,110054,3 +11218,3,110060,3 +11219,3,110070,3 +11220,3,110080,3 +11221,3,110090,3 +11222,3,110100,3 +11223,3,110110,3 +11224,3,110120,3 +11225,3,110130,3 +11226,3,110140,3 +11227,3,110150,3 +11228,3,110160,3 +11229,3,110170,3 +11230,3,110180,3 +11231,3,110190,3 +11232,3,110200,3 +11233,3,110210,3 +11234,3,110220,3 +11235,3,110230,3 +11236,3,110240,3 +11237,3,110250,3 +11238,3,110260,3 +11239,3,110270,3 +11240,3,110620,3 +11241,3,110670,3 +11242,4,140000,3 +11243,4,150010,3 +11244,4,150020,3 +11245,4,150030,3 +11246,4,150040,3 +11248,3,101000050,1 +11249,3,101000060,1 +11250,3,101000080,1 +11251,3,101000040,1 +11252,3,109000060,1 +11253,3,109000070,1 +11254,3,109000080,1 +11255,3,105000060,1 +11256,3,105000070,1 +11257,3,105000080,1 +11258,3,104000050,1 +11259,3,106000050,1 +11260,4,101000090,1 +11261,4,101000100,1 +11262,4,101000110,1 +11263,4,109000100,1 +11264,4,105000100,1 +11265,4,105000110,1 +11266,4,108000090,1 +11267,4,110000090,1 +11268,5,101000120,1 +11269,5,109000110,1 +11270,5,105000120,1 +11271,1,101000000,2 +11272,2,101000001,2 +11273,3,101000002,2 +11274,4,101000008,2 +11275,5,101000004,2 +11276,1,109000000,2 +11277,2,109000001,2 +11278,3,109000002,2 +11279,4,109000003,2 +11280,5,109000004,2 +11281,4,120024,3 +11282,4,120034,3 +11283,4,120044,3 +11284,4,120054,3 +11285,3,120241,3 +11286,3,120251,3 +11287,3,120261,3 +11288,3,120271,3 +11289,3,120300,3 +11290,3,120310,3 +11291,3,120320,3 +11292,3,120330,3 +11293,3,120340,3 +11294,3,120350,3 +11295,3,120360,3 +11296,3,120370,3 +11297,3,120380,3 +11298,3,120390,3 +11299,3,120400,3 +11300,3,120410,3 +11301,3,120420,3 +11302,3,120430,3 +11303,3,120450,3 +11304,3,120460,3 +11305,3,120550,3 +11306,3,120560,3 +11307,3,120570,3 +11308,3,120990,3 +11309,3,121000,3 +11310,3,121010,3 +11311,3,121020,3 +11312,3,121100,3 +11313,4,140000,3 +11314,4,150010,3 +11315,4,150020,3 +11316,4,150030,3 +11317,4,150040,3 +11319,3,102000060,1 +11320,3,102000070,1 +11321,3,102000080,1 +11322,3,103000050,1 +11323,3,105000050,1 +11324,3,107000060,1 +11325,3,107000070,1 +11326,3,107000080,1 +11327,3,108000050,1 +11328,3,109000050,1 +11329,3,103000060,1 +11330,3,103000070,1 +11331,3,103000080,1 +11332,3,110000050,1 +11333,4,102000100,1 +11334,4,102000110,1 +11335,4,102000350,1 +11336,4,106000090,1 +11337,4,109000090,1 +11338,4,107000100,1 +11339,4,103000090,1 +11340,4,102000090,1 +11341,4,103000100,1 +11342,5,102000120,1 +11343,5,107000110,1 +11344,5,103000120,1 +11345,1,102000000,2 +11346,2,102000001,2 +11347,3,102000002,2 +11348,4,102000003,2 +11349,5,102000004,2 +11350,1,105000000,2 +11351,2,105000001,2 +11352,3,105000002,2 +11353,4,105000003,2 +11354,5,105000004,2 +11355,1,112000000,2 +11356,2,112000001,2 +11357,3,112000002,2 +11358,4,112000003,2 +11359,5,112000004,2 +11360,4,130024,3 +11361,4,130034,3 +11362,4,130044,3 +11363,4,130054,3 +11364,3,130060,3 +11365,3,130070,3 +11366,3,130080,3 +11367,3,130090,3 +11368,3,130100,3 +11369,3,130110,3 +11370,3,130120,3 +11371,3,130130,3 +11372,3,130140,3 +11373,3,130150,3 +11374,3,130160,3 +11375,3,130170,3 +11376,3,130180,3 +11377,3,130190,3 +11378,3,130200,3 +11379,3,130420,3 +11380,3,130510,3 +11381,3,130520,3 +11382,3,130531,3 +11383,3,130540,3 +11384,3,130660,3 +11385,3,130790,3 +11386,3,130800,3 +11387,3,131130,3 +11388,4,140000,3 +11389,4,150010,3 +11390,4,150020,3 +11391,4,150030,3 +11392,4,150040,3 +11394,1,101000010,1 +11395,1,102000010,1 +11396,1,103000010,1 +11397,1,104000010,1 +11398,1,105000010,1 +11399,1,106000010,1 +11400,1,107000010,1 +11401,1,108000010,1 +11402,1,109000010,1 +11403,1,110000010,1 +11404,2,101000020,1 +11405,2,101000030,1 +11406,2,102000020,1 +11407,2,102000030,1 +11408,2,102000040,1 +11409,2,103000020,1 +11410,2,103000030,1 +11411,2,103000040,1 +11412,2,104000020,1 +11413,2,104000030,1 +11414,2,104000040,1 +11415,2,105000020,1 +11416,2,105000030,1 +11417,2,105000040,1 +11418,2,106000020,1 +11419,2,106000030,1 +11420,2,106000040,1 +11421,2,107000020,1 +11422,2,107000030,1 +11423,2,107000040,1 +11424,2,108000020,1 +11425,2,108000030,1 +11426,2,108000040,1 +11427,2,109000020,1 +11428,2,109000030,1 +11429,2,109000040,1 +11430,2,110000020,1 +11431,2,110000030,1 +11432,2,110000040,1 +11433,2,118000010,1 +11434,3,101000050,1 +11435,3,101000060,1 +11436,3,101000080,1 +11437,3,101000040,1 +11438,3,109000060,1 +11439,3,109000070,1 +11440,3,109000080,1 +11441,3,105000060,1 +11442,3,105000070,1 +11443,3,105000080,1 +11444,3,104000050,1 +11445,3,106000050,1 +11446,3,102000060,1 +11447,3,102000070,1 +11448,3,102000080,1 +11449,3,103000050,1 +11450,3,105000050,1 +11451,3,107000060,1 +11452,3,107000070,1 +11453,3,107000080,1 +11454,3,108000050,1 +11455,3,109000050,1 +11456,3,103000060,1 +11457,3,103000070,1 +11458,3,103000080,1 +11459,3,110000050,1 +11460,3,106000060,1 +11461,3,106000070,1 +11462,3,106000080,1 +11463,3,101000070,1 +11464,3,110000060,1 +11465,3,104000060,1 +11466,3,104000070,1 +11467,3,104000080,1 +11468,3,102000050,1 +11469,3,104000170,1 +11470,3,104000260,1 +11471,3,111000010,1 +11472,3,111000020,1 +11473,3,111000030,1 +11474,3,112000020,1 +11475,3,112000030,1 +11476,3,108000060,1 +11477,3,108000070,1 +11478,3,108000080,1 +11479,3,107000050,1 +11480,3,112000010,1 +11481,3,110000070,1 +11482,3,110000080,1 +11483,3,118000020,1 +11484,3,118000030,1 +11485,3,118000040,1 +11486,4,101000090,1 +11487,4,101000100,1 +11488,4,101000110,1 +11489,4,109000100,1 +11490,4,105000100,1 +11491,4,105000110,1 +11492,4,108000090,1 +11493,4,110000090,1 +11494,4,102000100,1 +11495,4,102000110,1 +11496,4,106000090,1 +11497,4,109000090,1 +11498,4,107000100,1 +11499,4,103000090,1 +11500,4,102000090,1 +11501,4,103000100,1 +11502,4,106000100,1 +11503,4,106000110,1 +11504,4,104000090,1 +11505,4,104000100,1 +11506,4,104000110,1 +11507,4,107000090,1 +11508,4,104000180,1 +11509,4,111000040,1 +11510,4,112000040,1 +11511,4,108000100,1 +11512,4,105000090,1 +11513,4,110000100,1 +11514,4,118000050,1 +11515,4,118000060,1 +11516,5,101000120,1 +11517,5,109000110,1 +11518,5,105000120,1 +11519,5,102000120,1 +11520,5,107000110,1 +11521,5,103000120,1 +11522,5,106000120,1 +11523,5,104000120,1 +11524,5,104000190,1 +11525,5,111000060,1 +11526,5,112000060,1 +11527,5,108000110,1 +11528,5,110000110,1 +11529,5,118000070,1 +11530,1,201000010,8 +11531,1,292000010,8 +11532,1,299000040,8 +11533,1,299000070,8 +11534,1,299000110,8 +11535,1,299000120,8 +11536,1,299000140,8 +11537,2,202000010,8 +11538,2,290000010,8 +11539,2,299000010,8 +11540,2,299000150,8 +11541,2,299000190,8 +11542,2,299000200,8 +11543,2,299000210,8 +11544,3,298000050,8 +11545,3,298000060,8 +11546,3,299000060,8 +11547,3,299000170,8 +11548,3,290000120,8 +11549,3,291000050,8 +11550,3,292000020,8 +11551,4,299000670,8 +11552,4,299000680,8 +11553,4,204000010,8 +11554,4,209000040,8 +11555,4,202000070,8 +11556,4,209000070,8 +11557,5,297000100,8 +11558,5,291000020,8 +11559,5,297000130,8 +11560,5,297000140,8 +11561,5,203000010,8 +11562,5,206000030,8 +11563,5,203000050,8 +11564,5,202000090,8 +11565,1,170002,3 +11566,1,180002,3 +11567,2,170003,3 +11568,2,180003,3 +11569,3,170004,3 +11570,3,180004,3 +11571,4,140000,3 +11572,5,150010,3 +11573,5,150020,3 +11574,5,150030,3 +11575,5,150040,3 +11577,1,101000010,1 +11578,1,102000010,1 +11579,1,103000010,1 +11580,1,104000010,1 +11581,1,105000010,1 +11582,1,106000010,1 +11583,1,107000010,1 +11584,1,108000010,1 +11585,1,109000010,1 +11586,1,110000010,1 +11587,2,101000020,1 +11588,2,101000030,1 +11589,2,102000020,1 +11590,2,102000030,1 +11591,2,102000040,1 +11592,2,103000020,1 +11593,2,103000030,1 +11594,2,103000040,1 +11595,2,104000020,1 +11596,2,104000030,1 +11597,2,104000040,1 +11598,2,105000020,1 +11599,2,105000030,1 +11600,2,105000040,1 +11601,2,106000020,1 +11602,2,106000030,1 +11603,2,106000040,1 +11604,2,107000020,1 +11605,2,107000030,1 +11606,2,107000040,1 +11607,2,108000020,1 +11608,2,108000030,1 +11609,2,108000040,1 +11610,2,109000020,1 +11611,2,109000030,1 +11612,2,109000040,1 +11613,2,110000020,1 +11614,2,110000030,1 +11615,2,110000040,1 +11616,2,118000010,1 +11617,3,101000050,1 +11618,3,101000060,1 +11619,3,101000080,1 +11620,3,101000040,1 +11621,3,109000060,1 +11622,3,109000070,1 +11623,3,109000080,1 +11624,3,105000060,1 +11625,3,105000070,1 +11626,3,105000080,1 +11627,3,104000050,1 +11628,3,106000050,1 +11629,3,102000060,1 +11630,3,102000070,1 +11631,3,102000080,1 +11632,3,103000050,1 +11633,3,105000050,1 +11634,3,107000060,1 +11635,3,107000070,1 +11636,3,107000080,1 +11637,3,108000050,1 +11638,3,109000050,1 +11639,3,103000060,1 +11640,3,103000070,1 +11641,3,103000080,1 +11642,3,110000050,1 +11643,3,106000060,1 +11644,3,106000070,1 +11645,3,106000080,1 +11646,3,101000070,1 +11647,3,110000060,1 +11648,3,104000060,1 +11649,3,104000070,1 +11650,3,104000080,1 +11651,3,102000050,1 +11652,3,104000170,1 +11653,3,104000260,1 +11654,3,111000010,1 +11655,3,111000020,1 +11656,3,111000030,1 +11657,3,112000020,1 +11658,3,112000030,1 +11659,3,108000060,1 +11660,3,108000070,1 +11661,3,108000080,1 +11662,3,107000050,1 +11663,3,112000010,1 +11664,3,110000070,1 +11665,3,110000080,1 +11666,3,118000020,1 +11667,3,118000030,1 +11668,3,118000040,1 +11669,4,101000090,1 +11670,4,101000100,1 +11671,4,101000110,1 +11672,4,109000100,1 +11673,4,105000100,1 +11674,4,105000110,1 +11675,4,108000090,1 +11676,4,110000090,1 +11677,4,102000100,1 +11678,4,102000110,1 +11679,4,106000090,1 +11680,4,109000090,1 +11681,4,107000100,1 +11682,4,103000090,1 +11683,4,102000090,1 +11684,4,103000100,1 +11685,4,106000100,1 +11686,4,106000110,1 +11687,4,104000090,1 +11688,4,104000100,1 +11689,4,104000110,1 +11690,4,107000090,1 +11691,4,104000180,1 +11692,4,111000040,1 +11693,4,112000040,1 +11694,4,108000100,1 +11695,4,105000090,1 +11696,4,110000100,1 +11697,4,118000050,1 +11698,4,118000060,1 +11699,5,101000120,1 +11700,5,109000110,1 +11701,5,105000120,1 +11702,5,102000120,1 +11703,5,107000110,1 +11704,5,103000120,1 +11705,5,106000120,1 +11706,5,104000120,1 +11707,5,104000190,1 +11708,5,111000060,1 +11709,5,112000060,1 +11710,5,108000110,1 +11711,5,110000110,1 +11712,5,118000070,1 +11713,1,201000010,8 +11714,1,292000010,8 +11715,1,299000040,8 +11716,1,299000070,8 +11717,1,299000110,8 +11718,1,299000120,8 +11719,1,299000140,8 +11720,2,202000010,8 +11721,2,290000010,8 +11722,2,299000010,8 +11723,2,299000150,8 +11724,2,299000190,8 +11725,2,299000200,8 +11726,2,299000210,8 +11727,3,298000050,8 +11728,3,298000060,8 +11729,3,299000060,8 +11730,3,299000170,8 +11731,3,290000120,8 +11732,3,291000050,8 +11733,3,292000020,8 +11734,4,299000670,8 +11735,4,299000680,8 +11736,4,204000010,8 +11737,4,209000040,8 +11738,4,202000070,8 +11739,4,209000070,8 +11740,5,297000100,8 +11741,5,291000020,8 +11742,5,297000130,8 +11743,5,297000140,8 +11744,5,203000010,8 +11745,5,206000030,8 +11746,5,203000050,8 +11747,5,202000090,8 +11748,2,170003,3 +11749,2,180003,3 +11750,3,170004,3 +11751,3,180004,3 +11752,4,140000,3 +11753,5,150010,3 +11754,5,150020,3 +11755,5,150030,3 +11756,5,150040,3 +11758,3,101000050,1 +11759,3,101000060,1 +11760,3,101000080,1 +11761,3,101000040,1 +11762,3,109000060,1 +11763,3,109000070,1 +11764,3,109000080,1 +11765,3,105000060,1 +11766,3,105000070,1 +11767,3,105000080,1 +11768,3,104000050,1 +11769,3,106000050,1 +11770,3,102000060,1 +11771,3,102000070,1 +11772,3,102000080,1 +11773,3,103000050,1 +11774,3,105000050,1 +11775,3,107000060,1 +11776,3,107000070,1 +11777,3,107000080,1 +11778,3,108000050,1 +11779,3,109000050,1 +11780,3,103000060,1 +11781,3,103000070,1 +11782,3,103000080,1 +11783,3,110000050,1 +11784,3,106000060,1 +11785,3,106000070,1 +11786,3,106000080,1 +11787,3,101000070,1 +11788,3,110000060,1 +11789,3,104000060,1 +11790,3,104000070,1 +11791,3,104000080,1 +11792,3,102000050,1 +11793,3,104000170,1 +11794,3,104000260,1 +11795,3,111000010,1 +11796,3,111000020,1 +11797,3,111000030,1 +11798,3,112000020,1 +11799,3,112000030,1 +11800,3,108000060,1 +11801,3,108000070,1 +11802,3,108000080,1 +11803,3,107000050,1 +11804,3,112000010,1 +11805,3,110000070,1 +11806,3,110000080,1 +11807,3,118000020,1 +11808,3,118000030,1 +11809,3,118000040,1 +11810,4,101000090,1 +11811,4,101000100,1 +11812,4,101000110,1 +11813,4,109000100,1 +11814,4,105000100,1 +11815,4,105000110,1 +11816,4,108000090,1 +11817,4,110000090,1 +11818,4,102000100,1 +11819,4,102000110,1 +11820,4,106000090,1 +11821,4,109000090,1 +11822,4,107000100,1 +11823,4,103000090,1 +11824,4,102000090,1 +11825,4,103000100,1 +11826,4,106000100,1 +11827,4,106000110,1 +11828,4,104000090,1 +11829,4,104000100,1 +11830,4,104000110,1 +11831,4,107000090,1 +11832,4,104000180,1 +11833,4,111000040,1 +11834,4,112000040,1 +11835,4,108000100,1 +11836,4,105000090,1 +11837,4,110000100,1 +11838,4,118000050,1 +11839,4,118000060,1 +11840,5,101000120,1 +11841,5,109000110,1 +11842,5,105000120,1 +11843,5,102000120,1 +11844,5,107000110,1 +11845,5,103000120,1 +11846,5,106000120,1 +11847,5,104000120,1 +11848,5,104000190,1 +11849,5,111000060,1 +11850,5,112000060,1 +11851,5,108000110,1 +11852,5,110000110,1 +11853,5,118000070,1 +11854,1,201000010,8 +11855,1,292000010,8 +11856,1,299000040,8 +11857,1,299000070,8 +11858,1,299000110,8 +11859,1,299000120,8 +11860,1,299000140,8 +11861,2,202000010,8 +11862,2,290000010,8 +11863,2,299000010,8 +11864,2,299000150,8 +11865,2,299000190,8 +11866,2,299000200,8 +11867,2,299000210,8 +11868,3,298000050,8 +11869,3,298000060,8 +11870,3,299000060,8 +11871,3,299000170,8 +11872,3,290000120,8 +11873,3,291000050,8 +11874,3,292000020,8 +11875,4,299000670,8 +11876,4,299000680,8 +11877,4,204000010,8 +11878,4,209000040,8 +11879,4,202000070,8 +11880,4,209000070,8 +11881,5,297000100,8 +11882,5,291000020,8 +11883,5,297000130,8 +11884,5,297000140,8 +11885,5,203000010,8 +11886,5,206000030,8 +11887,5,203000050,8 +11888,5,202000090,8 +11889,3,170004,3 +11890,3,180004,3 +11891,4,140000,3 +11892,5,150010,3 +11893,5,150020,3 +11894,5,150030,3 +11895,5,150040,3 +11897,3,101000050,1 +11898,3,101000060,1 +11899,3,101000080,1 +11900,3,101000040,1 +11901,3,109000060,1 +11902,3,109000070,1 +11903,3,109000080,1 +11904,3,105000060,1 +11905,3,105000070,1 +11906,3,105000080,1 +11907,3,104000050,1 +11908,3,106000050,1 +11909,3,102000060,1 +11910,3,102000070,1 +11911,3,102000080,1 +11912,3,103000050,1 +11913,3,105000050,1 +11914,3,107000060,1 +11915,3,107000070,1 +11916,3,107000080,1 +11917,3,108000050,1 +11918,3,109000050,1 +11919,3,103000060,1 +11920,3,103000070,1 +11921,3,103000080,1 +11922,3,110000050,1 +11923,3,106000060,1 +11924,3,106000070,1 +11925,3,106000080,1 +11926,3,101000070,1 +11927,3,110000060,1 +11928,3,104000060,1 +11929,3,104000070,1 +11930,3,104000080,1 +11931,3,102000050,1 +11932,3,104000170,1 +11933,3,104000260,1 +11934,3,111000010,1 +11935,3,111000020,1 +11936,3,111000030,1 +11937,3,112000020,1 +11938,3,112000030,1 +11939,3,108000060,1 +11940,3,108000070,1 +11941,3,108000080,1 +11942,3,107000050,1 +11943,3,112000010,1 +11944,3,110000070,1 +11945,3,110000080,1 +11946,3,118000020,1 +11947,3,118000030,1 +11948,3,118000040,1 +11949,4,101000090,1 +11950,4,101000100,1 +11951,4,101000110,1 +11952,4,109000100,1 +11953,4,105000100,1 +11954,4,105000110,1 +11955,4,108000090,1 +11956,4,110000090,1 +11957,4,102000100,1 +11958,4,102000110,1 +11959,4,106000090,1 +11960,4,109000090,1 +11961,4,107000100,1 +11962,4,103000090,1 +11963,4,102000090,1 +11964,4,103000100,1 +11965,4,106000100,1 +11966,4,106000110,1 +11967,4,104000090,1 +11968,4,104000100,1 +11969,4,104000110,1 +11970,4,107000090,1 +11971,4,104000180,1 +11972,4,111000040,1 +11973,4,112000040,1 +11974,4,108000100,1 +11975,4,105000090,1 +11976,4,110000100,1 +11977,4,118000050,1 +11978,4,118000060,1 +11979,5,101000120,1 +11980,5,109000110,1 +11981,5,105000120,1 +11982,5,102000120,1 +11983,5,107000110,1 +11984,5,103000120,1 +11985,5,106000120,1 +11986,5,104000120,1 +11987,5,104000190,1 +11988,5,111000060,1 +11989,5,112000060,1 +11990,5,108000110,1 +11991,5,110000110,1 +11992,5,118000070,1 +11993,1,201000010,8 +11994,1,292000010,8 +11995,1,299000040,8 +11996,1,299000070,8 +11997,1,299000110,8 +11998,1,299000120,8 +11999,1,299000140,8 +12000,2,202000010,8 +12001,2,290000010,8 +12002,2,299000010,8 +12003,2,299000150,8 +12004,2,299000190,8 +12005,2,299000200,8 +12006,2,299000210,8 +12007,3,298000050,8 +12008,3,298000060,8 +12009,3,299000060,8 +12010,3,299000170,8 +12011,3,290000120,8 +12012,3,291000050,8 +12013,3,292000020,8 +12014,4,299000670,8 +12015,4,299000680,8 +12016,4,204000010,8 +12017,4,209000040,8 +12018,4,202000070,8 +12019,4,209000070,8 +12020,5,297000100,8 +12021,5,291000020,8 +12022,5,297000130,8 +12023,5,297000140,8 +12024,5,203000010,8 +12025,5,206000030,8 +12026,5,203000050,8 +12027,5,202000090,8 +12028,3,170004,3 +12029,3,180004,3 +12030,4,140000,3 +12031,5,150010,3 +12032,5,150020,3 +12033,5,150030,3 +12034,5,150040,3 +12036,3,101000050,1 +12037,3,101000060,1 +12038,3,101000080,1 +12039,3,101000040,1 +12040,3,109000060,1 +12041,3,109000070,1 +12042,3,109000080,1 +12043,3,105000060,1 +12044,3,105000070,1 +12045,3,105000080,1 +12046,3,104000050,1 +12047,3,106000050,1 +12048,3,102000060,1 +12049,3,102000070,1 +12050,3,102000080,1 +12051,3,103000050,1 +12052,3,105000050,1 +12053,3,107000060,1 +12054,3,107000070,1 +12055,3,107000080,1 +12056,3,108000050,1 +12057,3,109000050,1 +12058,3,103000060,1 +12059,3,103000070,1 +12060,3,103000080,1 +12061,3,110000050,1 +12062,3,106000060,1 +12063,3,106000070,1 +12064,3,106000080,1 +12065,3,101000070,1 +12066,3,110000060,1 +12067,3,104000060,1 +12068,3,104000070,1 +12069,3,104000080,1 +12070,3,102000050,1 +12071,3,104000170,1 +12072,3,104000260,1 +12073,3,111000010,1 +12074,3,111000020,1 +12075,3,111000030,1 +12076,3,112000020,1 +12077,3,112000030,1 +12078,3,108000060,1 +12079,3,108000070,1 +12080,3,108000080,1 +12081,3,107000050,1 +12082,3,112000010,1 +12083,3,110000070,1 +12084,3,110000080,1 +12085,3,118000020,1 +12086,3,118000030,1 +12087,3,118000040,1 +12088,4,101000090,1 +12089,4,101000100,1 +12090,4,101000110,1 +12091,4,109000100,1 +12092,4,105000100,1 +12093,4,105000110,1 +12094,4,108000090,1 +12095,4,110000090,1 +12096,4,102000100,1 +12097,4,102000110,1 +12098,4,106000090,1 +12099,4,109000090,1 +12100,4,107000100,1 +12101,4,103000090,1 +12102,4,102000090,1 +12103,4,103000100,1 +12104,4,106000100,1 +12105,4,106000110,1 +12106,4,104000090,1 +12107,4,104000100,1 +12108,4,104000110,1 +12109,4,107000090,1 +12110,4,104000180,1 +12111,4,111000040,1 +12112,4,112000040,1 +12113,4,108000100,1 +12114,4,105000090,1 +12115,4,110000100,1 +12116,4,118000050,1 +12117,4,118000060,1 +12118,5,101000120,1 +12119,5,109000110,1 +12120,5,105000120,1 +12121,5,102000120,1 +12122,5,107000110,1 +12123,5,103000120,1 +12124,5,106000120,1 +12125,5,104000120,1 +12126,5,104000190,1 +12127,5,111000060,1 +12128,5,112000060,1 +12129,5,108000110,1 +12130,5,110000110,1 +12131,5,118000070,1 +12132,1,201000010,8 +12133,1,292000010,8 +12134,1,299000040,8 +12135,1,299000070,8 +12136,1,299000110,8 +12137,1,299000120,8 +12138,1,299000140,8 +12139,2,202000010,8 +12140,2,290000010,8 +12141,2,299000010,8 +12142,2,299000150,8 +12143,2,299000190,8 +12144,2,299000200,8 +12145,2,299000210,8 +12146,3,298000050,8 +12147,3,298000060,8 +12148,3,299000060,8 +12149,3,299000170,8 +12150,3,290000120,8 +12151,3,291000050,8 +12152,3,292000020,8 +12153,4,299000670,8 +12154,4,299000680,8 +12155,4,204000010,8 +12156,4,209000040,8 +12157,4,202000070,8 +12158,4,209000070,8 +12159,5,297000100,8 +12160,5,291000020,8 +12161,5,297000130,8 +12162,5,297000140,8 +12163,5,203000010,8 +12164,5,206000030,8 +12165,5,203000050,8 +12166,5,202000090,8 +12167,3,170004,3 +12168,3,180004,3 +12169,4,140000,3 +12170,5,150010,3 +12171,5,150020,3 +12172,5,150030,3 +12173,5,150040,3 +12174,5,190000,3 +12175,5,200000,3 +12176,5,210000,3 +12178,1,101000010,1 +12179,1,102000010,1 +12180,1,103000010,1 +12181,1,104000010,1 +12182,1,105000010,1 +12183,1,106000010,1 +12184,1,107000010,1 +12185,1,108000010,1 +12186,1,109000010,1 +12187,1,110000010,1 +12188,2,101000020,1 +12189,2,101000030,1 +12190,2,102000020,1 +12191,2,102000030,1 +12192,2,102000040,1 +12193,2,103000020,1 +12194,2,103000030,1 +12195,2,103000040,1 +12196,2,104000020,1 +12197,2,104000030,1 +12198,2,104000040,1 +12199,2,105000020,1 +12200,2,105000030,1 +12201,2,105000040,1 +12202,2,106000020,1 +12203,2,106000030,1 +12204,2,106000040,1 +12205,2,107000020,1 +12206,2,107000030,1 +12207,2,107000040,1 +12208,2,108000020,1 +12209,2,108000030,1 +12210,2,108000040,1 +12211,2,109000020,1 +12212,2,109000030,1 +12213,2,109000040,1 +12214,2,110000020,1 +12215,2,110000030,1 +12216,2,110000040,1 +12217,2,118000010,1 +12218,3,101000050,1 +12219,3,101000060,1 +12220,3,101000080,1 +12221,3,101000040,1 +12222,3,109000060,1 +12223,3,109000070,1 +12224,3,109000080,1 +12225,3,105000060,1 +12226,3,105000070,1 +12227,3,105000080,1 +12228,3,104000050,1 +12229,3,106000050,1 +12230,3,102000060,1 +12231,3,102000070,1 +12232,3,102000080,1 +12233,3,103000050,1 +12234,3,105000050,1 +12235,3,107000060,1 +12236,3,107000070,1 +12237,3,107000080,1 +12238,3,108000050,1 +12239,3,109000050,1 +12240,3,103000060,1 +12241,3,103000070,1 +12242,3,103000080,1 +12243,3,110000050,1 +12244,3,106000060,1 +12245,3,106000070,1 +12246,3,106000080,1 +12247,3,101000070,1 +12248,3,110000060,1 +12249,3,104000060,1 +12250,3,104000070,1 +12251,3,104000080,1 +12252,3,102000050,1 +12253,3,104000170,1 +12254,3,104000260,1 +12255,3,111000010,1 +12256,3,111000020,1 +12257,3,111000030,1 +12258,3,112000020,1 +12259,3,112000030,1 +12260,3,108000060,1 +12261,3,108000070,1 +12262,3,108000080,1 +12263,3,107000050,1 +12264,3,112000010,1 +12265,3,110000070,1 +12266,3,110000080,1 +12267,3,118000020,1 +12268,3,118000030,1 +12269,3,118000040,1 +12270,4,101000090,1 +12271,4,101000100,1 +12272,4,101000110,1 +12273,4,109000100,1 +12274,4,105000100,1 +12275,4,105000110,1 +12276,4,108000090,1 +12277,4,110000090,1 +12278,4,102000100,1 +12279,4,102000110,1 +12280,4,106000090,1 +12281,4,109000090,1 +12282,4,107000100,1 +12283,4,103000090,1 +12284,4,102000090,1 +12285,4,103000100,1 +12286,4,106000100,1 +12287,4,106000110,1 +12288,4,104000090,1 +12289,4,104000100,1 +12290,4,104000110,1 +12291,4,107000090,1 +12292,4,104000180,1 +12293,4,111000040,1 +12294,4,112000040,1 +12295,4,108000100,1 +12296,4,105000090,1 +12297,4,110000100,1 +12298,4,118000050,1 +12299,4,118000060,1 +12300,5,101000120,1 +12301,5,109000110,1 +12302,5,105000120,1 +12303,5,102000120,1 +12304,5,107000110,1 +12305,5,103000120,1 +12306,5,106000120,1 +12307,5,104000120,1 +12308,5,104000190,1 +12309,5,111000060,1 +12310,5,112000060,1 +12311,5,108000110,1 +12312,5,110000110,1 +12313,5,118000070,1 +12314,1,201000010,8 +12315,1,292000010,8 +12316,1,299000040,8 +12317,1,299000070,8 +12318,1,299000110,8 +12319,1,299000120,8 +12320,1,299000140,8 +12321,2,202000010,8 +12322,2,290000010,8 +12323,2,299000010,8 +12324,2,299000150,8 +12325,2,299000190,8 +12326,2,299000200,8 +12327,2,299000210,8 +12328,3,298000050,8 +12329,3,298000060,8 +12330,3,299000060,8 +12331,3,299000170,8 +12332,3,290000120,8 +12333,3,291000050,8 +12334,3,292000020,8 +12335,4,299000670,8 +12336,4,299000680,8 +12337,4,204000010,8 +12338,4,209000040,8 +12339,4,202000070,8 +12340,4,209000070,8 +12341,4,203000110,8 +12342,5,297000100,8 +12343,5,291000020,8 +12344,5,297000130,8 +12345,5,297000140,8 +12346,5,203000010,8 +12347,5,206000030,8 +12348,5,203000050,8 +12349,5,202000090,8 +12350,5,204000080,8 +12351,1,170002,3 +12352,1,180002,3 +12353,2,170003,3 +12354,2,180003,3 +12355,3,170004,3 +12356,3,180004,3 +12357,4,140000,3 +12358,5,150010,3 +12359,5,150020,3 +12360,5,150030,3 +12361,5,150040,3 +12363,1,101000010,1 +12364,1,102000010,1 +12365,1,103000010,1 +12366,1,104000010,1 +12367,1,105000010,1 +12368,1,106000010,1 +12369,1,107000010,1 +12370,1,108000010,1 +12371,1,109000010,1 +12372,1,110000010,1 +12373,2,101000020,1 +12374,2,101000030,1 +12375,2,102000020,1 +12376,2,102000030,1 +12377,2,102000040,1 +12378,2,103000020,1 +12379,2,103000030,1 +12380,2,103000040,1 +12381,2,104000020,1 +12382,2,104000030,1 +12383,2,104000040,1 +12384,2,105000020,1 +12385,2,105000030,1 +12386,2,105000040,1 +12387,2,106000020,1 +12388,2,106000030,1 +12389,2,106000040,1 +12390,2,107000020,1 +12391,2,107000030,1 +12392,2,107000040,1 +12393,2,108000020,1 +12394,2,108000030,1 +12395,2,108000040,1 +12396,2,109000020,1 +12397,2,109000030,1 +12398,2,109000040,1 +12399,2,110000020,1 +12400,2,110000030,1 +12401,2,110000040,1 +12402,2,118000010,1 +12403,3,101000050,1 +12404,3,101000060,1 +12405,3,101000080,1 +12406,3,101000040,1 +12407,3,109000060,1 +12408,3,109000070,1 +12409,3,109000080,1 +12410,3,105000060,1 +12411,3,105000070,1 +12412,3,105000080,1 +12413,3,104000050,1 +12414,3,106000050,1 +12415,3,102000060,1 +12416,3,102000070,1 +12417,3,102000080,1 +12418,3,103000050,1 +12419,3,105000050,1 +12420,3,107000060,1 +12421,3,107000070,1 +12422,3,107000080,1 +12423,3,108000050,1 +12424,3,109000050,1 +12425,3,103000060,1 +12426,3,103000070,1 +12427,3,103000080,1 +12428,3,110000050,1 +12429,3,106000060,1 +12430,3,106000070,1 +12431,3,106000080,1 +12432,3,101000070,1 +12433,3,110000060,1 +12434,3,104000060,1 +12435,3,104000070,1 +12436,3,104000080,1 +12437,3,102000050,1 +12438,3,104000170,1 +12439,3,104000260,1 +12440,3,111000010,1 +12441,3,111000020,1 +12442,3,111000030,1 +12443,3,112000020,1 +12444,3,112000030,1 +12445,3,108000060,1 +12446,3,108000070,1 +12447,3,108000080,1 +12448,3,107000050,1 +12449,3,112000010,1 +12450,3,110000070,1 +12451,3,110000080,1 +12452,3,118000020,1 +12453,3,118000030,1 +12454,3,118000040,1 +12455,4,101000090,1 +12456,4,101000100,1 +12457,4,101000110,1 +12458,4,109000100,1 +12459,4,105000100,1 +12460,4,105000110,1 +12461,4,108000090,1 +12462,4,110000090,1 +12463,4,102000100,1 +12464,4,102000110,1 +12465,4,106000090,1 +12466,4,109000090,1 +12467,4,107000100,1 +12468,4,103000090,1 +12469,4,102000090,1 +12470,4,103000100,1 +12471,4,106000100,1 +12472,4,106000110,1 +12473,4,104000090,1 +12474,4,104000100,1 +12475,4,104000110,1 +12476,4,107000090,1 +12477,4,104000180,1 +12478,4,111000040,1 +12479,4,112000040,1 +12480,4,108000100,1 +12481,4,105000090,1 +12482,4,110000100,1 +12483,4,118000050,1 +12484,4,118000060,1 +12485,5,101000120,1 +12486,5,109000110,1 +12487,5,105000120,1 +12488,5,102000120,1 +12489,5,107000110,1 +12490,5,103000120,1 +12491,5,106000120,1 +12492,5,104000120,1 +12493,5,104000190,1 +12494,5,111000060,1 +12495,5,112000060,1 +12496,5,108000110,1 +12497,5,110000110,1 +12498,5,118000070,1 +12499,1,201000010,8 +12500,1,292000010,8 +12501,1,299000040,8 +12502,1,299000070,8 +12503,1,299000110,8 +12504,1,299000120,8 +12505,1,299000140,8 +12506,2,202000010,8 +12507,2,290000010,8 +12508,2,299000010,8 +12509,2,299000150,8 +12510,2,299000190,8 +12511,2,299000200,8 +12512,2,299000210,8 +12513,3,298000050,8 +12514,3,298000060,8 +12515,3,299000060,8 +12516,3,299000170,8 +12517,3,290000120,8 +12518,3,291000050,8 +12519,3,292000020,8 +12520,4,299000670,8 +12521,4,299000680,8 +12522,4,204000010,8 +12523,4,209000040,8 +12524,4,202000070,8 +12525,4,209000070,8 +12526,4,203000110,8 +12527,5,297000100,8 +12528,5,291000020,8 +12529,5,297000130,8 +12530,5,297000140,8 +12531,5,203000010,8 +12532,5,206000030,8 +12533,5,203000050,8 +12534,5,202000090,8 +12535,5,204000080,8 +12536,2,170003,3 +12537,2,180003,3 +12538,3,170004,3 +12539,3,180004,3 +12540,4,140000,3 +12541,5,150010,3 +12542,5,150020,3 +12543,5,150030,3 +12544,5,150040,3 +12546,3,101000050,1 +12547,3,101000060,1 +12548,3,101000080,1 +12549,3,101000040,1 +12550,3,109000060,1 +12551,3,109000070,1 +12552,3,109000080,1 +12553,3,105000060,1 +12554,3,105000070,1 +12555,3,105000080,1 +12556,3,104000050,1 +12557,3,106000050,1 +12558,3,102000060,1 +12559,3,102000070,1 +12560,3,102000080,1 +12561,3,103000050,1 +12562,3,105000050,1 +12563,3,107000060,1 +12564,3,107000070,1 +12565,3,107000080,1 +12566,3,108000050,1 +12567,3,109000050,1 +12568,3,103000060,1 +12569,3,103000070,1 +12570,3,103000080,1 +12571,3,110000050,1 +12572,3,106000060,1 +12573,3,106000070,1 +12574,3,106000080,1 +12575,3,101000070,1 +12576,3,110000060,1 +12577,3,104000060,1 +12578,3,104000070,1 +12579,3,104000080,1 +12580,3,102000050,1 +12581,3,104000170,1 +12582,3,104000260,1 +12583,3,111000010,1 +12584,3,111000020,1 +12585,3,111000030,1 +12586,3,112000020,1 +12587,3,112000030,1 +12588,3,108000060,1 +12589,3,108000070,1 +12590,3,108000080,1 +12591,3,107000050,1 +12592,3,112000010,1 +12593,3,110000070,1 +12594,3,110000080,1 +12595,3,118000020,1 +12596,3,118000030,1 +12597,3,118000040,1 +12598,4,101000090,1 +12599,4,101000100,1 +12600,4,101000110,1 +12601,4,109000100,1 +12602,4,105000100,1 +12603,4,105000110,1 +12604,4,108000090,1 +12605,4,110000090,1 +12606,4,102000100,1 +12607,4,102000110,1 +12608,4,106000090,1 +12609,4,109000090,1 +12610,4,107000100,1 +12611,4,103000090,1 +12612,4,102000090,1 +12613,4,103000100,1 +12614,4,106000100,1 +12615,4,106000110,1 +12616,4,104000090,1 +12617,4,104000100,1 +12618,4,104000110,1 +12619,4,107000090,1 +12620,4,104000180,1 +12621,4,111000040,1 +12622,4,112000040,1 +12623,4,108000100,1 +12624,4,105000090,1 +12625,4,110000100,1 +12626,4,118000050,1 +12627,4,118000060,1 +12628,5,101000120,1 +12629,5,109000110,1 +12630,5,105000120,1 +12631,5,102000120,1 +12632,5,107000110,1 +12633,5,103000120,1 +12634,5,106000120,1 +12635,5,104000120,1 +12636,5,104000190,1 +12637,5,111000060,1 +12638,5,112000060,1 +12639,5,108000110,1 +12640,5,110000110,1 +12641,5,118000070,1 +12642,1,201000010,8 +12643,1,292000010,8 +12644,1,299000040,8 +12645,1,299000070,8 +12646,1,299000110,8 +12647,1,299000120,8 +12648,1,299000140,8 +12649,2,202000010,8 +12650,2,290000010,8 +12651,2,299000010,8 +12652,2,299000150,8 +12653,2,299000190,8 +12654,2,299000200,8 +12655,2,299000210,8 +12656,3,298000050,8 +12657,3,298000060,8 +12658,3,299000060,8 +12659,3,299000170,8 +12660,3,290000120,8 +12661,3,291000050,8 +12662,3,292000020,8 +12663,4,299000670,8 +12664,4,299000680,8 +12665,4,204000010,8 +12666,4,209000040,8 +12667,4,202000070,8 +12668,4,209000070,8 +12669,4,203000110,8 +12670,5,297000100,8 +12671,5,291000020,8 +12672,5,297000130,8 +12673,5,297000140,8 +12674,5,203000010,8 +12675,5,206000030,8 +12676,5,203000050,8 +12677,5,202000090,8 +12678,5,204000080,8 +12679,3,170004,3 +12680,3,180004,3 +12681,4,140000,3 +12682,5,150010,3 +12683,5,150020,3 +12684,5,150030,3 +12685,5,150040,3 +12687,3,101000050,1 +12688,3,101000060,1 +12689,3,101000080,1 +12690,3,101000040,1 +12691,3,109000060,1 +12692,3,109000070,1 +12693,3,109000080,1 +12694,3,105000060,1 +12695,3,105000070,1 +12696,3,105000080,1 +12697,3,104000050,1 +12698,3,106000050,1 +12699,3,102000060,1 +12700,3,102000070,1 +12701,3,102000080,1 +12702,3,103000050,1 +12703,3,105000050,1 +12704,3,107000060,1 +12705,3,107000070,1 +12706,3,107000080,1 +12707,3,108000050,1 +12708,3,109000050,1 +12709,3,103000060,1 +12710,3,103000070,1 +12711,3,103000080,1 +12712,3,110000050,1 +12713,3,106000060,1 +12714,3,106000070,1 +12715,3,106000080,1 +12716,3,101000070,1 +12717,3,110000060,1 +12718,3,104000060,1 +12719,3,104000070,1 +12720,3,104000080,1 +12721,3,102000050,1 +12722,3,104000170,1 +12723,3,104000260,1 +12724,3,111000010,1 +12725,3,111000020,1 +12726,3,111000030,1 +12727,3,112000020,1 +12728,3,112000030,1 +12729,3,108000060,1 +12730,3,108000070,1 +12731,3,108000080,1 +12732,3,107000050,1 +12733,3,112000010,1 +12734,3,110000070,1 +12735,3,110000080,1 +12736,3,118000020,1 +12737,3,118000030,1 +12738,3,118000040,1 +12739,4,101000090,1 +12740,4,101000100,1 +12741,4,101000110,1 +12742,4,109000100,1 +12743,4,105000100,1 +12744,4,105000110,1 +12745,4,108000090,1 +12746,4,110000090,1 +12747,4,102000100,1 +12748,4,102000110,1 +12749,4,106000090,1 +12750,4,109000090,1 +12751,4,107000100,1 +12752,4,103000090,1 +12753,4,102000090,1 +12754,4,103000100,1 +12755,4,106000100,1 +12756,4,106000110,1 +12757,4,104000090,1 +12758,4,104000100,1 +12759,4,104000110,1 +12760,4,107000090,1 +12761,4,104000180,1 +12762,4,111000040,1 +12763,4,112000040,1 +12764,4,108000100,1 +12765,4,105000090,1 +12766,4,110000100,1 +12767,4,118000050,1 +12768,4,118000060,1 +12769,5,101000120,1 +12770,5,109000110,1 +12771,5,105000120,1 +12772,5,102000120,1 +12773,5,107000110,1 +12774,5,103000120,1 +12775,5,106000120,1 +12776,5,104000120,1 +12777,5,104000190,1 +12778,5,111000060,1 +12779,5,112000060,1 +12780,5,108000110,1 +12781,5,110000110,1 +12782,5,118000070,1 +12783,1,201000010,8 +12784,1,292000010,8 +12785,1,299000040,8 +12786,1,299000070,8 +12787,1,299000110,8 +12788,1,299000120,8 +12789,1,299000140,8 +12790,2,202000010,8 +12791,2,290000010,8 +12792,2,299000010,8 +12793,2,299000150,8 +12794,2,299000190,8 +12795,2,299000200,8 +12796,2,299000210,8 +12797,3,298000050,8 +12798,3,298000060,8 +12799,3,299000060,8 +12800,3,299000170,8 +12801,3,290000120,8 +12802,3,291000050,8 +12803,3,292000020,8 +12804,4,299000670,8 +12805,4,299000680,8 +12806,4,204000010,8 +12807,4,209000040,8 +12808,4,202000070,8 +12809,4,209000070,8 +12810,4,203000110,8 +12811,5,297000100,8 +12812,5,291000020,8 +12813,5,297000130,8 +12814,5,297000140,8 +12815,5,203000010,8 +12816,5,206000030,8 +12817,5,203000050,8 +12818,5,202000090,8 +12819,5,204000080,8 +12820,3,170004,3 +12821,3,180004,3 +12822,4,140000,3 +12823,5,150010,3 +12824,5,150020,3 +12825,5,150030,3 +12826,5,150040,3 +12828,3,101000050,1 +12829,3,101000060,1 +12830,3,101000080,1 +12831,3,101000040,1 +12832,3,109000060,1 +12833,3,109000070,1 +12834,3,109000080,1 +12835,3,105000060,1 +12836,3,105000070,1 +12837,3,105000080,1 +12838,3,104000050,1 +12839,3,106000050,1 +12840,3,102000060,1 +12841,3,102000070,1 +12842,3,102000080,1 +12843,3,103000050,1 +12844,3,105000050,1 +12845,3,107000060,1 +12846,3,107000070,1 +12847,3,107000080,1 +12848,3,108000050,1 +12849,3,109000050,1 +12850,3,103000060,1 +12851,3,103000070,1 +12852,3,103000080,1 +12853,3,110000050,1 +12854,3,106000060,1 +12855,3,106000070,1 +12856,3,106000080,1 +12857,3,101000070,1 +12858,3,110000060,1 +12859,3,104000060,1 +12860,3,104000070,1 +12861,3,104000080,1 +12862,3,102000050,1 +12863,3,104000170,1 +12864,3,104000260,1 +12865,3,111000010,1 +12866,3,111000020,1 +12867,3,111000030,1 +12868,3,112000020,1 +12869,3,112000030,1 +12870,3,108000060,1 +12871,3,108000070,1 +12872,3,108000080,1 +12873,3,107000050,1 +12874,3,112000010,1 +12875,3,110000070,1 +12876,3,110000080,1 +12877,3,118000020,1 +12878,3,118000030,1 +12879,3,118000040,1 +12880,4,101000090,1 +12881,4,101000100,1 +12882,4,101000110,1 +12883,4,109000100,1 +12884,4,105000100,1 +12885,4,105000110,1 +12886,4,108000090,1 +12887,4,110000090,1 +12888,4,102000100,1 +12889,4,102000110,1 +12890,4,106000090,1 +12891,4,109000090,1 +12892,4,107000100,1 +12893,4,103000090,1 +12894,4,102000090,1 +12895,4,103000100,1 +12896,4,106000100,1 +12897,4,106000110,1 +12898,4,104000090,1 +12899,4,104000100,1 +12900,4,104000110,1 +12901,4,107000090,1 +12902,4,104000180,1 +12903,4,111000040,1 +12904,4,112000040,1 +12905,4,108000100,1 +12906,4,105000090,1 +12907,4,110000100,1 +12908,4,118000050,1 +12909,4,118000060,1 +12910,5,101000120,1 +12911,5,109000110,1 +12912,5,105000120,1 +12913,5,102000120,1 +12914,5,107000110,1 +12915,5,103000120,1 +12916,5,106000120,1 +12917,5,104000120,1 +12918,5,104000190,1 +12919,5,111000060,1 +12920,5,112000060,1 +12921,5,108000110,1 +12922,5,110000110,1 +12923,5,118000070,1 +12924,1,201000010,8 +12925,1,292000010,8 +12926,1,299000040,8 +12927,1,299000070,8 +12928,1,299000110,8 +12929,1,299000120,8 +12930,1,299000140,8 +12931,2,202000010,8 +12932,2,290000010,8 +12933,2,299000010,8 +12934,2,299000150,8 +12935,2,299000190,8 +12936,2,299000200,8 +12937,2,299000210,8 +12938,3,298000050,8 +12939,3,298000060,8 +12940,3,299000060,8 +12941,3,299000170,8 +12942,3,290000120,8 +12943,3,291000050,8 +12944,3,292000020,8 +12945,4,299000670,8 +12946,4,299000680,8 +12947,4,204000010,8 +12948,4,209000040,8 +12949,4,202000070,8 +12950,4,209000070,8 +12951,4,203000110,8 +12952,5,297000100,8 +12953,5,291000020,8 +12954,5,297000130,8 +12955,5,297000140,8 +12956,5,203000010,8 +12957,5,206000030,8 +12958,5,203000050,8 +12959,5,202000090,8 +12960,5,204000080,8 +12961,3,170004,3 +12962,3,180004,3 +12963,4,140000,3 +12964,5,150010,3 +12965,5,150020,3 +12966,5,150030,3 +12967,5,150040,3 +12968,5,190000,3 +12969,5,200000,3 +12970,5,210000,3 +12972,3,102000060,1 +12973,3,102000070,1 +12974,3,102000080,1 +12975,3,103000050,1 +12976,3,105000050,1 +12977,3,107000060,1 +12978,3,107000070,1 +12979,3,107000080,1 +12980,3,108000050,1 +12981,3,109000050,1 +12982,3,103000060,1 +12983,3,103000070,1 +12984,3,103000080,1 +12985,3,110000050,1 +12986,4,102000100,1 +12987,4,102000110,1 +12988,4,102000350,1 +12989,4,106000090,1 +12990,4,109000090,1 +12991,4,107000100,1 +12992,4,103000090,1 +12993,4,102000090,1 +12994,4,103000100,1 +12995,5,102000120,1 +12996,5,107000110,1 +12997,5,103000120,1 +12998,1,102000000,2 +12999,2,102000001,2 +13000,3,102000002,2 +13001,4,102000003,2 +13002,5,102000004,2 +13003,1,105000000,2 +13004,2,105000001,2 +13005,3,105000002,2 +13006,4,105000003,2 +13007,5,105000004,2 +13008,1,112000000,2 +13009,2,112000001,2 +13010,3,112000002,2 +13011,4,112000003,2 +13012,5,112000004,2 +13013,3,170004,3 +13014,4,170005,3 +13015,3,180004,3 +13016,4,180005,3 +13017,4,140000,3 +13018,4,150010,3 +13019,4,150020,3 +13020,4,150030,3 +13021,4,150040,3 +13023,3,118000020,1 +13024,3,118000030,1 +13025,3,118000040,1 +13026,3,106000060,1 +13027,3,106000070,1 +13028,3,106000080,1 +13029,3,101000070,1 +13030,3,110000060,1 +13031,3,104000060,1 +13032,3,104000070,1 +13033,3,104000080,1 +13034,3,102000050,1 +13035,3,104000170,1 +13036,3,104000260,1 +13037,4,118000050,1 +13038,4,118000060,1 +13039,4,106000100,1 +13040,4,106000110,1 +13041,4,104000090,1 +13042,4,104000100,1 +13043,4,104000110,1 +13044,4,104000270,1 +13045,4,107000090,1 +13046,4,104000180,1 +13047,5,118000070,1 +13048,5,106000120,1 +13049,5,104000120,1 +13050,5,104000190,1 +13051,1,103000000,2 +13052,2,103000001,2 +13053,3,103000002,2 +13054,4,103000003,2 +13055,5,103000004,2 +13056,1,111000000,2 +13057,2,111000001,2 +13058,3,111000002,2 +13059,4,111000003,2 +13060,5,111000004,2 +13061,1,115000000,2 +13062,2,115000001,2 +13063,3,115000002,2 +13064,4,115000003,2 +13065,5,115000004,2 +13066,4,110024,3 +13067,4,110034,3 +13068,4,110044,3 +13069,4,110054,3 +13070,3,110060,3 +13071,3,110070,3 +13072,3,110080,3 +13073,3,110090,3 +13074,3,110100,3 +13075,3,110110,3 +13076,3,110120,3 +13077,3,110130,3 +13078,3,110140,3 +13079,3,110150,3 +13080,3,110160,3 +13081,3,110170,3 +13082,3,110180,3 +13083,3,110190,3 +13084,3,110200,3 +13085,3,110210,3 +13086,3,110220,3 +13087,3,110230,3 +13088,3,110240,3 +13089,3,110250,3 +13090,3,110260,3 +13091,3,110270,3 +13092,3,110620,3 +13093,3,110670,3 +13094,4,140000,3 +13095,4,150010,3 +13096,4,150020,3 +13097,4,150030,3 +13098,4,150040,3 +13100,3,111000010,1 +13101,3,111000020,1 +13102,3,111000030,1 +13103,3,112000020,1 +13104,3,112000030,1 +13105,3,108000060,1 +13106,3,108000070,1 +13107,3,108000080,1 +13108,3,107000050,1 +13109,3,112000010,1 +13110,3,110000070,1 +13111,3,110000080,1 +13112,4,111000040,1 +13113,4,112000040,1 +13114,4,108000100,1 +13115,4,105000090,1 +13116,4,110000100,1 +13117,5,111000060,1 +13118,5,112000060,1 +13119,5,108000110,1 +13120,5,110000110,1 +13121,1,108000000,2 +13122,2,108000001,2 +13123,3,108000002,2 +13124,4,108000003,2 +13125,5,108000004,2 +13126,1,107000000,2 +13127,2,107000001,2 +13128,3,107000002,2 +13129,4,107000003,2 +13130,5,107000004,2 +13131,1,120000000,2 +13132,2,120000001,2 +13133,3,120000002,2 +13134,4,120000003,2 +13135,5,120000004,2 +13136,4,120024,3 +13137,4,120034,3 +13138,4,120044,3 +13139,4,120054,3 +13140,3,120241,3 +13141,3,120251,3 +13142,3,120261,3 +13143,3,120271,3 +13144,3,120300,3 +13145,3,120310,3 +13146,3,120320,3 +13147,3,120330,3 +13148,3,120340,3 +13149,3,120350,3 +13150,3,120360,3 +13151,3,120370,3 +13152,3,120380,3 +13153,3,120390,3 +13154,3,120400,3 +13155,3,120410,3 +13156,3,120420,3 +13157,3,120430,3 +13158,3,120450,3 +13159,3,120460,3 +13160,3,120550,3 +13161,3,120560,3 +13162,3,120570,3 +13163,3,120990,3 +13164,3,121000,3 +13165,3,121010,3 +13166,3,121020,3 +13167,3,121100,3 +13168,4,140000,3 +13169,4,150010,3 +13170,4,150020,3 +13171,4,150030,3 +13172,4,150040,3 +13174,3,101000050,1 +13175,3,101000060,1 +13176,3,101000080,1 +13177,3,101000040,1 +13178,3,109000060,1 +13179,3,109000070,1 +13180,3,109000080,1 +13181,3,105000060,1 +13182,3,105000070,1 +13183,3,105000080,1 +13184,3,104000050,1 +13185,3,106000050,1 +13186,4,101000090,1 +13187,4,101000100,1 +13188,4,101000110,1 +13189,4,109000100,1 +13190,4,105000100,1 +13191,4,105000110,1 +13192,4,108000090,1 +13193,4,110000090,1 +13194,5,101000120,1 +13195,5,109000110,1 +13196,5,105000120,1 +13197,1,101000000,2 +13198,2,101000001,2 +13199,3,101000002,2 +13200,4,101000008,2 +13201,5,101000004,2 +13202,1,109000000,2 +13203,2,109000001,2 +13204,3,109000002,2 +13205,4,109000003,2 +13206,5,109000004,2 +13207,4,130024,3 +13208,4,130034,3 +13209,4,130044,3 +13210,4,130054,3 +13211,3,130060,3 +13212,3,130070,3 +13213,3,130080,3 +13214,3,130090,3 +13215,3,130100,3 +13216,3,130110,3 +13217,3,130120,3 +13218,3,130130,3 +13219,3,130140,3 +13220,3,130150,3 +13221,3,130160,3 +13222,3,130170,3 +13223,3,130180,3 +13224,3,130190,3 +13225,3,130200,3 +13226,3,130420,3 +13227,3,130510,3 +13228,3,130520,3 +13229,3,130531,3 +13230,3,130540,3 +13231,3,130660,3 +13232,3,130700,3 +13233,3,130790,3 +13234,3,130800,3 +13235,3,131130,3 +13236,4,140000,3 +13237,4,150010,3 +13238,4,150020,3 +13239,4,150030,3 +13240,4,150040,3 +13242,3,101000050,1 +13243,3,101000060,1 +13244,3,101000080,1 +13245,3,101000040,1 +13246,3,109000060,1 +13247,3,109000070,1 +13248,3,109000080,1 +13249,3,105000060,1 +13250,3,105000070,1 +13251,3,105000080,1 +13252,3,104000050,1 +13253,3,106000050,1 +13254,4,101000090,1 +13255,4,101000100,1 +13256,4,101000110,1 +13257,4,109000100,1 +13258,4,105000100,1 +13259,4,105000110,1 +13260,4,108000090,1 +13261,4,110000090,1 +13262,5,101000120,1 +13263,5,109000110,1 +13264,5,105000120,1 +13265,1,101000000,2 +13266,2,101000001,2 +13267,3,101000002,2 +13268,4,101000008,2 +13269,5,101000004,2 +13270,1,109000000,2 +13271,2,109000001,2 +13272,3,109000002,2 +13273,4,109000003,2 +13274,5,109000004,2 +13275,3,170004,3 +13276,4,170005,3 +13277,3,180004,3 +13278,4,180005,3 +13279,4,140000,3 +13280,4,150010,3 +13281,4,150020,3 +13282,4,150030,3 +13283,4,150040,3 +13285,3,102000060,1 +13286,3,102000070,1 +13287,3,102000080,1 +13288,3,103000050,1 +13289,3,105000050,1 +13290,3,107000060,1 +13291,3,107000070,1 +13292,3,107000080,1 +13293,3,108000050,1 +13294,3,109000050,1 +13295,3,103000060,1 +13296,3,103000070,1 +13297,3,103000080,1 +13298,3,110000050,1 +13299,4,102000100,1 +13300,4,102000110,1 +13301,4,102000350,1 +13302,4,106000090,1 +13303,4,109000090,1 +13304,4,107000100,1 +13305,4,103000090,1 +13306,4,102000090,1 +13307,4,103000100,1 +13308,5,102000120,1 +13309,5,107000110,1 +13310,5,103000120,1 +13311,1,102000000,2 +13312,2,102000001,2 +13313,3,102000002,2 +13314,4,102000003,2 +13315,5,102000004,2 +13316,1,105000000,2 +13317,2,105000001,2 +13318,3,105000002,2 +13319,4,105000003,2 +13320,5,105000004,2 +13321,1,112000000,2 +13322,2,112000001,2 +13323,3,112000002,2 +13324,4,112000003,2 +13325,5,112000004,2 +13326,4,110024,3 +13327,4,110034,3 +13328,4,110044,3 +13329,4,110054,3 +13330,3,110060,3 +13331,3,110070,3 +13332,3,110080,3 +13333,3,110090,3 +13334,3,110100,3 +13335,3,110110,3 +13336,3,110120,3 +13337,3,110130,3 +13338,3,110140,3 +13339,3,110150,3 +13340,3,110160,3 +13341,3,110170,3 +13342,3,110180,3 +13343,3,110190,3 +13344,3,110200,3 +13345,3,110210,3 +13346,3,110220,3 +13347,3,110230,3 +13348,3,110240,3 +13349,3,110250,3 +13350,3,110260,3 +13351,3,110270,3 +13352,3,110620,3 +13353,3,110670,3 +13354,4,140000,3 +13355,4,150010,3 +13356,4,150020,3 +13357,4,150030,3 +13358,4,150040,3 +13360,3,118000020,1 +13361,3,118000030,1 +13362,3,118000040,1 +13363,3,106000060,1 +13364,3,106000070,1 +13365,3,106000080,1 +13366,3,101000070,1 +13367,3,110000060,1 +13368,3,104000060,1 +13369,3,104000070,1 +13370,3,104000080,1 +13371,3,102000050,1 +13372,3,104000170,1 +13373,3,104000260,1 +13374,4,118000050,1 +13375,4,118000060,1 +13376,4,106000100,1 +13377,4,106000110,1 +13378,4,104000090,1 +13379,4,104000100,1 +13380,4,104000110,1 +13381,4,104000270,1 +13382,4,107000090,1 +13383,4,104000180,1 +13384,5,118000070,1 +13385,5,106000120,1 +13386,5,104000120,1 +13387,5,104000190,1 +13388,1,103000000,2 +13389,2,103000001,2 +13390,3,103000002,2 +13391,4,103000003,2 +13392,5,103000004,2 +13393,1,111000000,2 +13394,2,111000001,2 +13395,3,111000002,2 +13396,4,111000003,2 +13397,5,111000004,2 +13398,1,115000000,2 +13399,2,115000001,2 +13400,3,115000002,2 +13401,4,115000003,2 +13402,5,115000004,2 +13403,4,120024,3 +13404,4,120034,3 +13405,4,120044,3 +13406,4,120054,3 +13407,3,120241,3 +13408,3,120251,3 +13409,3,120261,3 +13410,3,120271,3 +13411,3,120300,3 +13412,3,120310,3 +13413,3,120320,3 +13414,3,120330,3 +13415,3,120340,3 +13416,3,120350,3 +13417,3,120360,3 +13418,3,120370,3 +13419,3,120380,3 +13420,3,120390,3 +13421,3,120400,3 +13422,3,120410,3 +13423,3,120420,3 +13424,3,120430,3 +13425,3,120450,3 +13426,3,120460,3 +13427,3,120550,3 +13428,3,120560,3 +13429,3,120570,3 +13430,3,120990,3 +13431,3,121000,3 +13432,3,121010,3 +13433,3,121020,3 +13434,3,121100,3 +13435,4,140000,3 +13436,4,150010,3 +13437,4,150020,3 +13438,4,150030,3 +13439,4,150040,3 +13441,3,111000010,1 +13442,3,111000020,1 +13443,3,111000030,1 +13444,3,112000020,1 +13445,3,112000030,1 +13446,3,108000060,1 +13447,3,108000070,1 +13448,3,108000080,1 +13449,3,107000050,1 +13450,3,112000010,1 +13451,3,110000070,1 +13452,3,110000080,1 +13453,4,111000040,1 +13454,4,112000040,1 +13455,4,108000100,1 +13456,4,105000090,1 +13457,4,110000100,1 +13458,5,111000060,1 +13459,5,112000060,1 +13460,5,108000110,1 +13461,5,110000110,1 +13462,1,108000000,2 +13463,2,108000001,2 +13464,3,108000002,2 +13465,4,108000003,2 +13466,5,108000004,2 +13467,1,107000000,2 +13468,2,107000001,2 +13469,3,107000002,2 +13470,4,107000003,2 +13471,5,107000004,2 +13472,1,120000000,2 +13473,2,120000001,2 +13474,3,120000002,2 +13475,4,120000003,2 +13476,5,120000004,2 +13477,4,130024,3 +13478,4,130034,3 +13479,4,130044,3 +13480,4,130054,3 +13481,3,130060,3 +13482,3,130070,3 +13483,3,130080,3 +13484,3,130090,3 +13485,3,130100,3 +13486,3,130110,3 +13487,3,130120,3 +13488,3,130130,3 +13489,3,130140,3 +13490,3,130150,3 +13491,3,130160,3 +13492,3,130170,3 +13493,3,130180,3 +13494,3,130190,3 +13495,3,130200,3 +13496,3,130420,3 +13497,3,130510,3 +13498,3,130520,3 +13499,3,130531,3 +13500,3,130540,3 +13501,3,130660,3 +13502,3,130700,3 +13503,3,130790,3 +13504,3,130800,3 +13505,3,131130,3 +13506,4,140000,3 +13507,4,150010,3 +13508,4,150020,3 +13509,4,150030,3 +13510,4,150040,3 +13512,1,101000010,1 +13513,1,102000010,1 +13514,1,103000010,1 +13515,1,104000010,1 +13516,1,105000010,1 +13517,1,106000010,1 +13518,1,107000010,1 +13519,1,108000010,1 +13520,1,109000010,1 +13521,1,110000010,1 +13522,2,101000020,1 +13523,2,101000030,1 +13524,2,102000020,1 +13525,2,102000030,1 +13526,2,102000040,1 +13527,2,103000020,1 +13528,2,103000030,1 +13529,2,103000040,1 +13530,2,104000020,1 +13531,2,104000030,1 +13532,2,104000040,1 +13533,2,105000020,1 +13534,2,105000030,1 +13535,2,105000040,1 +13536,2,106000020,1 +13537,2,106000030,1 +13538,2,106000040,1 +13539,2,107000020,1 +13540,2,107000030,1 +13541,2,107000040,1 +13542,2,108000020,1 +13543,2,108000030,1 +13544,2,108000040,1 +13545,2,109000020,1 +13546,2,109000030,1 +13547,2,109000040,1 +13548,2,110000020,1 +13549,2,110000030,1 +13550,2,110000040,1 +13551,2,118000010,1 +13552,3,101000050,1 +13553,3,101000060,1 +13554,3,101000080,1 +13555,3,101000040,1 +13556,3,109000060,1 +13557,3,109000070,1 +13558,3,109000080,1 +13559,3,105000060,1 +13560,3,105000070,1 +13561,3,105000080,1 +13562,3,104000050,1 +13563,3,106000050,1 +13564,3,102000060,1 +13565,3,102000070,1 +13566,3,102000080,1 +13567,3,103000050,1 +13568,3,105000050,1 +13569,3,107000060,1 +13570,3,107000070,1 +13571,3,107000080,1 +13572,3,108000050,1 +13573,3,109000050,1 +13574,3,103000060,1 +13575,3,103000070,1 +13576,3,103000080,1 +13577,3,110000050,1 +13578,3,106000060,1 +13579,3,106000070,1 +13580,3,106000080,1 +13581,3,101000070,1 +13582,3,110000060,1 +13583,3,104000060,1 +13584,3,104000070,1 +13585,3,104000080,1 +13586,3,102000050,1 +13587,3,104000170,1 +13588,3,104000260,1 +13589,3,111000010,1 +13590,3,111000020,1 +13591,3,111000030,1 +13592,3,112000020,1 +13593,3,112000030,1 +13594,3,108000060,1 +13595,3,108000070,1 +13596,3,108000080,1 +13597,3,107000050,1 +13598,3,112000010,1 +13599,3,110000070,1 +13600,3,110000080,1 +13601,3,118000020,1 +13602,3,118000030,1 +13603,3,118000040,1 +13604,4,101000090,1 +13605,4,101000100,1 +13606,4,101000110,1 +13607,4,109000100,1 +13608,4,105000100,1 +13609,4,105000110,1 +13610,4,108000090,1 +13611,4,110000090,1 +13612,4,102000100,1 +13613,4,102000110,1 +13614,4,106000090,1 +13615,4,109000090,1 +13616,4,107000100,1 +13617,4,103000090,1 +13618,4,102000090,1 +13619,4,103000100,1 +13620,4,106000100,1 +13621,4,106000110,1 +13622,4,104000090,1 +13623,4,104000100,1 +13624,4,104000110,1 +13625,4,107000090,1 +13626,4,104000180,1 +13627,4,111000040,1 +13628,4,112000040,1 +13629,4,108000100,1 +13630,4,105000090,1 +13631,4,110000100,1 +13632,4,118000050,1 +13633,4,118000060,1 +13634,5,101000120,1 +13635,5,109000110,1 +13636,5,105000120,1 +13637,5,102000120,1 +13638,5,107000110,1 +13639,5,103000120,1 +13640,5,106000120,1 +13641,5,104000120,1 +13642,5,104000190,1 +13643,5,111000060,1 +13644,5,112000060,1 +13645,5,108000110,1 +13646,5,110000110,1 +13647,5,118000070,1 +13648,1,201000010,8 +13649,1,292000010,8 +13650,1,299000040,8 +13651,1,299000070,8 +13652,1,299000110,8 +13653,1,299000120,8 +13654,1,299000140,8 +13655,2,202000010,8 +13656,2,290000010,8 +13657,2,299000010,8 +13658,2,299000150,8 +13659,2,299000190,8 +13660,2,299000200,8 +13661,2,299000210,8 +13662,3,298000050,8 +13663,3,298000060,8 +13664,3,299000060,8 +13665,3,299000170,8 +13666,3,290000120,8 +13667,3,291000050,8 +13668,3,292000020,8 +13669,4,299000670,8 +13670,4,299000680,8 +13671,4,204000010,8 +13672,4,209000040,8 +13673,4,202000070,8 +13674,4,209000070,8 +13675,4,203000110,8 +13676,4,290000110,8 +13677,5,297000100,8 +13678,5,291000020,8 +13679,5,297000130,8 +13680,5,297000140,8 +13681,5,203000010,8 +13682,5,206000030,8 +13683,5,203000050,8 +13684,5,202000090,8 +13685,5,204000080,8 +13686,5,202000150,8 +13687,1,170002,3 +13688,1,180002,3 +13689,2,170003,3 +13690,2,180003,3 +13691,3,170004,3 +13692,3,180004,3 +13693,4,140000,3 +13694,5,150010,3 +13695,5,150020,3 +13696,5,150030,3 +13697,5,150040,3 +13699,1,101000010,1 +13700,1,102000010,1 +13701,1,103000010,1 +13702,1,104000010,1 +13703,1,105000010,1 +13704,1,106000010,1 +13705,1,107000010,1 +13706,1,108000010,1 +13707,1,109000010,1 +13708,1,110000010,1 +13709,2,101000020,1 +13710,2,101000030,1 +13711,2,102000020,1 +13712,2,102000030,1 +13713,2,102000040,1 +13714,2,103000020,1 +13715,2,103000030,1 +13716,2,103000040,1 +13717,2,104000020,1 +13718,2,104000030,1 +13719,2,104000040,1 +13720,2,105000020,1 +13721,2,105000030,1 +13722,2,105000040,1 +13723,2,106000020,1 +13724,2,106000030,1 +13725,2,106000040,1 +13726,2,107000020,1 +13727,2,107000030,1 +13728,2,107000040,1 +13729,2,108000020,1 +13730,2,108000030,1 +13731,2,108000040,1 +13732,2,109000020,1 +13733,2,109000030,1 +13734,2,109000040,1 +13735,2,110000020,1 +13736,2,110000030,1 +13737,2,110000040,1 +13738,2,118000010,1 +13739,3,101000050,1 +13740,3,101000060,1 +13741,3,101000080,1 +13742,3,101000040,1 +13743,3,109000060,1 +13744,3,109000070,1 +13745,3,109000080,1 +13746,3,105000060,1 +13747,3,105000070,1 +13748,3,105000080,1 +13749,3,104000050,1 +13750,3,106000050,1 +13751,3,102000060,1 +13752,3,102000070,1 +13753,3,102000080,1 +13754,3,103000050,1 +13755,3,105000050,1 +13756,3,107000060,1 +13757,3,107000070,1 +13758,3,107000080,1 +13759,3,108000050,1 +13760,3,109000050,1 +13761,3,103000060,1 +13762,3,103000070,1 +13763,3,103000080,1 +13764,3,110000050,1 +13765,3,106000060,1 +13766,3,106000070,1 +13767,3,106000080,1 +13768,3,101000070,1 +13769,3,110000060,1 +13770,3,104000060,1 +13771,3,104000070,1 +13772,3,104000080,1 +13773,3,102000050,1 +13774,3,104000170,1 +13775,3,104000260,1 +13776,3,111000010,1 +13777,3,111000020,1 +13778,3,111000030,1 +13779,3,112000020,1 +13780,3,112000030,1 +13781,3,108000060,1 +13782,3,108000070,1 +13783,3,108000080,1 +13784,3,107000050,1 +13785,3,112000010,1 +13786,3,110000070,1 +13787,3,110000080,1 +13788,3,118000020,1 +13789,3,118000030,1 +13790,3,118000040,1 +13791,4,101000090,1 +13792,4,101000100,1 +13793,4,101000110,1 +13794,4,109000100,1 +13795,4,105000100,1 +13796,4,105000110,1 +13797,4,108000090,1 +13798,4,110000090,1 +13799,4,102000100,1 +13800,4,102000110,1 +13801,4,106000090,1 +13802,4,109000090,1 +13803,4,107000100,1 +13804,4,103000090,1 +13805,4,102000090,1 +13806,4,103000100,1 +13807,4,106000100,1 +13808,4,106000110,1 +13809,4,104000090,1 +13810,4,104000100,1 +13811,4,104000110,1 +13812,4,107000090,1 +13813,4,104000180,1 +13814,4,111000040,1 +13815,4,112000040,1 +13816,4,108000100,1 +13817,4,105000090,1 +13818,4,110000100,1 +13819,4,118000050,1 +13820,4,118000060,1 +13821,5,101000120,1 +13822,5,109000110,1 +13823,5,105000120,1 +13824,5,102000120,1 +13825,5,107000110,1 +13826,5,103000120,1 +13827,5,106000120,1 +13828,5,104000120,1 +13829,5,104000190,1 +13830,5,111000060,1 +13831,5,112000060,1 +13832,5,108000110,1 +13833,5,110000110,1 +13834,5,118000070,1 +13835,1,201000010,8 +13836,1,292000010,8 +13837,1,299000040,8 +13838,1,299000070,8 +13839,1,299000110,8 +13840,1,299000120,8 +13841,1,299000140,8 +13842,2,202000010,8 +13843,2,290000010,8 +13844,2,299000010,8 +13845,2,299000150,8 +13846,2,299000190,8 +13847,2,299000200,8 +13848,2,299000210,8 +13849,3,298000050,8 +13850,3,298000060,8 +13851,3,299000060,8 +13852,3,299000170,8 +13853,3,290000120,8 +13854,3,291000050,8 +13855,3,292000020,8 +13856,4,299000670,8 +13857,4,299000680,8 +13858,4,204000010,8 +13859,4,209000040,8 +13860,4,202000070,8 +13861,4,209000070,8 +13862,4,203000110,8 +13863,4,290000110,8 +13864,5,297000100,8 +13865,5,291000020,8 +13866,5,297000130,8 +13867,5,297000140,8 +13868,5,203000010,8 +13869,5,206000030,8 +13870,5,203000050,8 +13871,5,202000090,8 +13872,5,204000080,8 +13873,5,202000150,8 +13874,2,170003,3 +13875,2,180003,3 +13876,3,170004,3 +13877,3,180004,3 +13878,4,140000,3 +13879,5,150010,3 +13880,5,150020,3 +13881,5,150030,3 +13882,5,150040,3 +13884,3,101000050,1 +13885,3,101000060,1 +13886,3,101000080,1 +13887,3,101000040,1 +13888,3,109000060,1 +13889,3,109000070,1 +13890,3,109000080,1 +13891,3,105000060,1 +13892,3,105000070,1 +13893,3,105000080,1 +13894,3,104000050,1 +13895,3,106000050,1 +13896,3,102000060,1 +13897,3,102000070,1 +13898,3,102000080,1 +13899,3,103000050,1 +13900,3,105000050,1 +13901,3,107000060,1 +13902,3,107000070,1 +13903,3,107000080,1 +13904,3,108000050,1 +13905,3,109000050,1 +13906,3,103000060,1 +13907,3,103000070,1 +13908,3,103000080,1 +13909,3,110000050,1 +13910,3,106000060,1 +13911,3,106000070,1 +13912,3,106000080,1 +13913,3,101000070,1 +13914,3,110000060,1 +13915,3,104000060,1 +13916,3,104000070,1 +13917,3,104000080,1 +13918,3,102000050,1 +13919,3,104000170,1 +13920,3,104000260,1 +13921,3,111000010,1 +13922,3,111000020,1 +13923,3,111000030,1 +13924,3,112000020,1 +13925,3,112000030,1 +13926,3,108000060,1 +13927,3,108000070,1 +13928,3,108000080,1 +13929,3,107000050,1 +13930,3,112000010,1 +13931,3,110000070,1 +13932,3,110000080,1 +13933,3,118000020,1 +13934,3,118000030,1 +13935,3,118000040,1 +13936,4,101000090,1 +13937,4,101000100,1 +13938,4,101000110,1 +13939,4,109000100,1 +13940,4,105000100,1 +13941,4,105000110,1 +13942,4,108000090,1 +13943,4,110000090,1 +13944,4,102000100,1 +13945,4,102000110,1 +13946,4,106000090,1 +13947,4,109000090,1 +13948,4,107000100,1 +13949,4,103000090,1 +13950,4,102000090,1 +13951,4,103000100,1 +13952,4,106000100,1 +13953,4,106000110,1 +13954,4,104000090,1 +13955,4,104000100,1 +13956,4,104000110,1 +13957,4,107000090,1 +13958,4,104000180,1 +13959,4,111000040,1 +13960,4,112000040,1 +13961,4,108000100,1 +13962,4,105000090,1 +13963,4,110000100,1 +13964,4,118000050,1 +13965,4,118000060,1 +13966,5,101000120,1 +13967,5,109000110,1 +13968,5,105000120,1 +13969,5,102000120,1 +13970,5,107000110,1 +13971,5,103000120,1 +13972,5,106000120,1 +13973,5,104000120,1 +13974,5,104000190,1 +13975,5,111000060,1 +13976,5,112000060,1 +13977,5,108000110,1 +13978,5,110000110,1 +13979,5,118000070,1 +13980,1,201000010,8 +13981,1,292000010,8 +13982,1,299000040,8 +13983,1,299000070,8 +13984,1,299000110,8 +13985,1,299000120,8 +13986,1,299000140,8 +13987,2,202000010,8 +13988,2,290000010,8 +13989,2,299000010,8 +13990,2,299000150,8 +13991,2,299000190,8 +13992,2,299000200,8 +13993,2,299000210,8 +13994,3,298000050,8 +13995,3,298000060,8 +13996,3,299000060,8 +13997,3,299000170,8 +13998,3,290000120,8 +13999,3,291000050,8 +14000,3,292000020,8 +14001,4,299000670,8 +14002,4,299000680,8 +14003,4,204000010,8 +14004,4,209000040,8 +14005,4,202000070,8 +14006,4,209000070,8 +14007,4,203000110,8 +14008,4,290000110,8 +14009,5,297000100,8 +14010,5,291000020,8 +14011,5,297000130,8 +14012,5,297000140,8 +14013,5,203000010,8 +14014,5,206000030,8 +14015,5,203000050,8 +14016,5,202000090,8 +14017,5,204000080,8 +14018,5,202000150,8 +14019,3,170004,3 +14020,3,180004,3 +14021,4,140000,3 +14022,5,150010,3 +14023,5,150020,3 +14024,5,150030,3 +14025,5,150040,3 +14027,3,101000050,1 +14028,3,101000060,1 +14029,3,101000080,1 +14030,3,101000040,1 +14031,3,109000060,1 +14032,3,109000070,1 +14033,3,109000080,1 +14034,3,105000060,1 +14035,3,105000070,1 +14036,3,105000080,1 +14037,3,104000050,1 +14038,3,106000050,1 +14039,3,102000060,1 +14040,3,102000070,1 +14041,3,102000080,1 +14042,3,103000050,1 +14043,3,105000050,1 +14044,3,107000060,1 +14045,3,107000070,1 +14046,3,107000080,1 +14047,3,108000050,1 +14048,3,109000050,1 +14049,3,103000060,1 +14050,3,103000070,1 +14051,3,103000080,1 +14052,3,110000050,1 +14053,3,106000060,1 +14054,3,106000070,1 +14055,3,106000080,1 +14056,3,101000070,1 +14057,3,110000060,1 +14058,3,104000060,1 +14059,3,104000070,1 +14060,3,104000080,1 +14061,3,102000050,1 +14062,3,104000170,1 +14063,3,104000260,1 +14064,3,111000010,1 +14065,3,111000020,1 +14066,3,111000030,1 +14067,3,112000020,1 +14068,3,112000030,1 +14069,3,108000060,1 +14070,3,108000070,1 +14071,3,108000080,1 +14072,3,107000050,1 +14073,3,112000010,1 +14074,3,110000070,1 +14075,3,110000080,1 +14076,3,118000020,1 +14077,3,118000030,1 +14078,3,118000040,1 +14079,4,101000090,1 +14080,4,101000100,1 +14081,4,101000110,1 +14082,4,109000100,1 +14083,4,105000100,1 +14084,4,105000110,1 +14085,4,108000090,1 +14086,4,110000090,1 +14087,4,102000100,1 +14088,4,102000110,1 +14089,4,106000090,1 +14090,4,109000090,1 +14091,4,107000100,1 +14092,4,103000090,1 +14093,4,102000090,1 +14094,4,103000100,1 +14095,4,106000100,1 +14096,4,106000110,1 +14097,4,104000090,1 +14098,4,104000100,1 +14099,4,104000110,1 +14100,4,107000090,1 +14101,4,104000180,1 +14102,4,111000040,1 +14103,4,112000040,1 +14104,4,108000100,1 +14105,4,105000090,1 +14106,4,110000100,1 +14107,4,118000050,1 +14108,4,118000060,1 +14109,5,101000120,1 +14110,5,109000110,1 +14111,5,105000120,1 +14112,5,102000120,1 +14113,5,107000110,1 +14114,5,103000120,1 +14115,5,106000120,1 +14116,5,104000120,1 +14117,5,104000190,1 +14118,5,111000060,1 +14119,5,112000060,1 +14120,5,108000110,1 +14121,5,110000110,1 +14122,5,118000070,1 +14123,1,201000010,8 +14124,1,292000010,8 +14125,1,299000040,8 +14126,1,299000070,8 +14127,1,299000110,8 +14128,1,299000120,8 +14129,1,299000140,8 +14130,2,202000010,8 +14131,2,290000010,8 +14132,2,299000010,8 +14133,2,299000150,8 +14134,2,299000190,8 +14135,2,299000200,8 +14136,2,299000210,8 +14137,3,298000050,8 +14138,3,298000060,8 +14139,3,299000060,8 +14140,3,299000170,8 +14141,3,290000120,8 +14142,3,291000050,8 +14143,3,292000020,8 +14144,4,299000670,8 +14145,4,299000680,8 +14146,4,204000010,8 +14147,4,209000040,8 +14148,4,202000070,8 +14149,4,209000070,8 +14150,4,203000110,8 +14151,4,290000110,8 +14152,5,297000100,8 +14153,5,291000020,8 +14154,5,297000130,8 +14155,5,297000140,8 +14156,5,203000010,8 +14157,5,206000030,8 +14158,5,203000050,8 +14159,5,202000090,8 +14160,5,204000080,8 +14161,5,202000150,8 +14162,3,170004,3 +14163,3,180004,3 +14164,4,140000,3 +14165,5,150010,3 +14166,5,150020,3 +14167,5,150030,3 +14168,5,150040,3 +14170,3,101000050,1 +14171,3,101000060,1 +14172,3,101000080,1 +14173,3,101000040,1 +14174,3,109000060,1 +14175,3,109000070,1 +14176,3,109000080,1 +14177,3,105000060,1 +14178,3,105000070,1 +14179,3,105000080,1 +14180,3,104000050,1 +14181,3,106000050,1 +14182,3,102000060,1 +14183,3,102000070,1 +14184,3,102000080,1 +14185,3,103000050,1 +14186,3,105000050,1 +14187,3,107000060,1 +14188,3,107000070,1 +14189,3,107000080,1 +14190,3,108000050,1 +14191,3,109000050,1 +14192,3,103000060,1 +14193,3,103000070,1 +14194,3,103000080,1 +14195,3,110000050,1 +14196,3,106000060,1 +14197,3,106000070,1 +14198,3,106000080,1 +14199,3,101000070,1 +14200,3,110000060,1 +14201,3,104000060,1 +14202,3,104000070,1 +14203,3,104000080,1 +14204,3,102000050,1 +14205,3,104000170,1 +14206,3,104000260,1 +14207,3,111000010,1 +14208,3,111000020,1 +14209,3,111000030,1 +14210,3,112000020,1 +14211,3,112000030,1 +14212,3,108000060,1 +14213,3,108000070,1 +14214,3,108000080,1 +14215,3,107000050,1 +14216,3,112000010,1 +14217,3,110000070,1 +14218,3,110000080,1 +14219,3,118000020,1 +14220,3,118000030,1 +14221,3,118000040,1 +14222,4,101000090,1 +14223,4,101000100,1 +14224,4,101000110,1 +14225,4,109000100,1 +14226,4,105000100,1 +14227,4,105000110,1 +14228,4,108000090,1 +14229,4,110000090,1 +14230,4,102000100,1 +14231,4,102000110,1 +14232,4,106000090,1 +14233,4,109000090,1 +14234,4,107000100,1 +14235,4,103000090,1 +14236,4,102000090,1 +14237,4,103000100,1 +14238,4,106000100,1 +14239,4,106000110,1 +14240,4,104000090,1 +14241,4,104000100,1 +14242,4,104000110,1 +14243,4,107000090,1 +14244,4,104000180,1 +14245,4,111000040,1 +14246,4,112000040,1 +14247,4,108000100,1 +14248,4,105000090,1 +14249,4,110000100,1 +14250,4,118000050,1 +14251,4,118000060,1 +14252,5,101000120,1 +14253,5,109000110,1 +14254,5,105000120,1 +14255,5,102000120,1 +14256,5,107000110,1 +14257,5,103000120,1 +14258,5,106000120,1 +14259,5,104000120,1 +14260,5,104000190,1 +14261,5,111000060,1 +14262,5,112000060,1 +14263,5,108000110,1 +14264,5,110000110,1 +14265,5,118000070,1 +14266,1,201000010,8 +14267,1,292000010,8 +14268,1,299000040,8 +14269,1,299000070,8 +14270,1,299000110,8 +14271,1,299000120,8 +14272,1,299000140,8 +14273,2,202000010,8 +14274,2,290000010,8 +14275,2,299000010,8 +14276,2,299000150,8 +14277,2,299000190,8 +14278,2,299000200,8 +14279,2,299000210,8 +14280,3,298000050,8 +14281,3,298000060,8 +14282,3,299000060,8 +14283,3,299000170,8 +14284,3,290000120,8 +14285,3,291000050,8 +14286,3,292000020,8 +14287,4,299000670,8 +14288,4,299000680,8 +14289,4,204000010,8 +14290,4,209000040,8 +14291,4,202000070,8 +14292,4,209000070,8 +14293,4,203000110,8 +14294,4,290000110,8 +14295,5,297000100,8 +14296,5,291000020,8 +14297,5,297000130,8 +14298,5,297000140,8 +14299,5,203000010,8 +14300,5,206000030,8 +14301,5,203000050,8 +14302,5,202000090,8 +14303,5,204000080,8 +14304,5,202000150,8 +14305,3,170004,3 +14306,3,180004,3 +14307,4,140000,3 +14308,5,150010,3 +14309,5,150020,3 +14310,5,150030,3 +14311,5,150040,3 +14312,5,190000,3 +14313,5,200000,3 +14314,5,210000,3 +14316,1,101000010,1 +14317,1,102000010,1 +14318,1,103000010,1 +14319,1,104000010,1 +14320,1,105000010,1 +14321,1,106000010,1 +14322,1,107000010,1 +14323,1,108000010,1 +14324,1,109000010,1 +14325,1,110000010,1 +14326,2,101000020,1 +14327,2,101000030,1 +14328,2,102000020,1 +14329,2,102000030,1 +14330,2,102000040,1 +14331,2,103000020,1 +14332,2,103000030,1 +14333,2,103000040,1 +14334,2,104000020,1 +14335,2,104000030,1 +14336,2,104000040,1 +14337,2,105000020,1 +14338,2,105000030,1 +14339,2,105000040,1 +14340,2,106000020,1 +14341,2,106000030,1 +14342,2,106000040,1 +14343,2,107000020,1 +14344,2,107000030,1 +14345,2,107000040,1 +14346,2,108000020,1 +14347,2,108000030,1 +14348,2,108000040,1 +14349,2,109000020,1 +14350,2,109000030,1 +14351,2,109000040,1 +14352,2,110000020,1 +14353,2,110000030,1 +14354,2,110000040,1 +14355,2,118000010,1 +14356,3,101000050,1 +14357,3,101000060,1 +14358,3,101000080,1 +14359,3,101000040,1 +14360,3,109000060,1 +14361,3,109000070,1 +14362,3,109000080,1 +14363,3,105000060,1 +14364,3,105000070,1 +14365,3,105000080,1 +14366,3,104000050,1 +14367,3,106000050,1 +14368,3,102000060,1 +14369,3,102000070,1 +14370,3,102000080,1 +14371,3,103000050,1 +14372,3,105000050,1 +14373,3,107000060,1 +14374,3,107000070,1 +14375,3,107000080,1 +14376,3,108000050,1 +14377,3,109000050,1 +14378,3,103000060,1 +14379,3,103000070,1 +14380,3,103000080,1 +14381,3,110000050,1 +14382,3,106000060,1 +14383,3,106000070,1 +14384,3,106000080,1 +14385,3,101000070,1 +14386,3,110000060,1 +14387,3,104000060,1 +14388,3,104000070,1 +14389,3,104000080,1 +14390,3,102000050,1 +14391,3,104000170,1 +14392,3,104000260,1 +14393,3,111000010,1 +14394,3,111000020,1 +14395,3,111000030,1 +14396,3,112000020,1 +14397,3,112000030,1 +14398,3,108000060,1 +14399,3,108000070,1 +14400,3,108000080,1 +14401,3,107000050,1 +14402,3,112000010,1 +14403,3,110000070,1 +14404,3,110000080,1 +14405,3,118000020,1 +14406,3,118000030,1 +14407,3,118000040,1 +14408,4,101000090,1 +14409,4,101000100,1 +14410,4,101000110,1 +14411,4,109000100,1 +14412,4,105000100,1 +14413,4,105000110,1 +14414,4,108000090,1 +14415,4,110000090,1 +14416,4,102000100,1 +14417,4,102000110,1 +14418,4,106000090,1 +14419,4,109000090,1 +14420,4,107000100,1 +14421,4,103000090,1 +14422,4,102000090,1 +14423,4,103000100,1 +14424,4,106000100,1 +14425,4,106000110,1 +14426,4,104000090,1 +14427,4,104000100,1 +14428,4,104000110,1 +14429,4,107000090,1 +14430,4,104000180,1 +14431,4,111000040,1 +14432,4,112000040,1 +14433,4,108000100,1 +14434,4,105000090,1 +14435,4,110000100,1 +14436,4,118000050,1 +14437,4,118000060,1 +14438,5,101000120,1 +14439,5,109000110,1 +14440,5,105000120,1 +14441,5,102000120,1 +14442,5,107000110,1 +14443,5,103000120,1 +14444,5,106000120,1 +14445,5,104000120,1 +14446,5,104000190,1 +14447,5,111000060,1 +14448,5,112000060,1 +14449,5,108000110,1 +14450,5,110000110,1 +14451,5,118000070,1 +14452,1,201000010,8 +14453,1,292000010,8 +14454,1,299000040,8 +14455,1,299000070,8 +14456,1,299000110,8 +14457,1,299000120,8 +14458,1,299000140,8 +14459,2,202000010,8 +14460,2,290000010,8 +14461,2,299000010,8 +14462,2,299000150,8 +14463,2,299000190,8 +14464,2,299000200,8 +14465,2,299000210,8 +14466,3,298000050,8 +14467,3,298000060,8 +14468,3,299000060,8 +14469,3,299000170,8 +14470,3,290000120,8 +14471,3,291000050,8 +14472,3,292000020,8 +14473,4,299000670,8 +14474,4,299000680,8 +14475,4,204000010,8 +14476,4,209000040,8 +14477,4,202000070,8 +14478,4,209000070,8 +14479,4,203000110,8 +14480,4,290000110,8 +14481,4,206000110,8 +14482,5,297000100,8 +14483,5,291000020,8 +14484,5,297000130,8 +14485,5,297000140,8 +14486,5,203000010,8 +14487,5,206000030,8 +14488,5,203000050,8 +14489,5,202000090,8 +14490,5,204000080,8 +14491,5,202000150,8 +14492,5,204000100,8 +14493,1,170002,3 +14494,1,180002,3 +14495,2,170003,3 +14496,2,180003,3 +14497,3,170004,3 +14498,3,180004,3 +14499,4,140000,3 +14500,5,150010,3 +14501,5,150020,3 +14502,5,150030,3 +14503,5,150040,3 +14505,1,101000010,1 +14506,1,102000010,1 +14507,1,103000010,1 +14508,1,104000010,1 +14509,1,105000010,1 +14510,1,106000010,1 +14511,1,107000010,1 +14512,1,108000010,1 +14513,1,109000010,1 +14514,1,110000010,1 +14515,2,101000020,1 +14516,2,101000030,1 +14517,2,102000020,1 +14518,2,102000030,1 +14519,2,102000040,1 +14520,2,103000020,1 +14521,2,103000030,1 +14522,2,103000040,1 +14523,2,104000020,1 +14524,2,104000030,1 +14525,2,104000040,1 +14526,2,105000020,1 +14527,2,105000030,1 +14528,2,105000040,1 +14529,2,106000020,1 +14530,2,106000030,1 +14531,2,106000040,1 +14532,2,107000020,1 +14533,2,107000030,1 +14534,2,107000040,1 +14535,2,108000020,1 +14536,2,108000030,1 +14537,2,108000040,1 +14538,2,109000020,1 +14539,2,109000030,1 +14540,2,109000040,1 +14541,2,110000020,1 +14542,2,110000030,1 +14543,2,110000040,1 +14544,2,118000010,1 +14545,3,101000050,1 +14546,3,101000060,1 +14547,3,101000080,1 +14548,3,101000040,1 +14549,3,109000060,1 +14550,3,109000070,1 +14551,3,109000080,1 +14552,3,105000060,1 +14553,3,105000070,1 +14554,3,105000080,1 +14555,3,104000050,1 +14556,3,106000050,1 +14557,3,102000060,1 +14558,3,102000070,1 +14559,3,102000080,1 +14560,3,103000050,1 +14561,3,105000050,1 +14562,3,107000060,1 +14563,3,107000070,1 +14564,3,107000080,1 +14565,3,108000050,1 +14566,3,109000050,1 +14567,3,103000060,1 +14568,3,103000070,1 +14569,3,103000080,1 +14570,3,110000050,1 +14571,3,106000060,1 +14572,3,106000070,1 +14573,3,106000080,1 +14574,3,101000070,1 +14575,3,110000060,1 +14576,3,104000060,1 +14577,3,104000070,1 +14578,3,104000080,1 +14579,3,102000050,1 +14580,3,104000170,1 +14581,3,104000260,1 +14582,3,111000010,1 +14583,3,111000020,1 +14584,3,111000030,1 +14585,3,112000020,1 +14586,3,112000030,1 +14587,3,108000060,1 +14588,3,108000070,1 +14589,3,108000080,1 +14590,3,107000050,1 +14591,3,112000010,1 +14592,3,110000070,1 +14593,3,110000080,1 +14594,3,118000020,1 +14595,3,118000030,1 +14596,3,118000040,1 +14597,4,101000090,1 +14598,4,101000100,1 +14599,4,101000110,1 +14600,4,109000100,1 +14601,4,105000100,1 +14602,4,105000110,1 +14603,4,108000090,1 +14604,4,110000090,1 +14605,4,102000100,1 +14606,4,102000110,1 +14607,4,106000090,1 +14608,4,109000090,1 +14609,4,107000100,1 +14610,4,103000090,1 +14611,4,102000090,1 +14612,4,103000100,1 +14613,4,106000100,1 +14614,4,106000110,1 +14615,4,104000090,1 +14616,4,104000100,1 +14617,4,104000110,1 +14618,4,107000090,1 +14619,4,104000180,1 +14620,4,111000040,1 +14621,4,112000040,1 +14622,4,108000100,1 +14623,4,105000090,1 +14624,4,110000100,1 +14625,4,118000050,1 +14626,4,118000060,1 +14627,5,101000120,1 +14628,5,109000110,1 +14629,5,105000120,1 +14630,5,102000120,1 +14631,5,107000110,1 +14632,5,103000120,1 +14633,5,106000120,1 +14634,5,104000120,1 +14635,5,104000190,1 +14636,5,111000060,1 +14637,5,112000060,1 +14638,5,108000110,1 +14639,5,110000110,1 +14640,5,118000070,1 +14641,1,201000010,8 +14642,1,292000010,8 +14643,1,299000040,8 +14644,1,299000070,8 +14645,1,299000110,8 +14646,1,299000120,8 +14647,1,299000140,8 +14648,2,202000010,8 +14649,2,290000010,8 +14650,2,299000010,8 +14651,2,299000150,8 +14652,2,299000190,8 +14653,2,299000200,8 +14654,2,299000210,8 +14655,3,298000050,8 +14656,3,298000060,8 +14657,3,299000060,8 +14658,3,299000170,8 +14659,3,290000120,8 +14660,3,291000050,8 +14661,3,292000020,8 +14662,4,299000670,8 +14663,4,299000680,8 +14664,4,204000010,8 +14665,4,209000040,8 +14666,4,202000070,8 +14667,4,209000070,8 +14668,4,203000110,8 +14669,4,290000110,8 +14670,4,206000110,8 +14671,5,297000100,8 +14672,5,291000020,8 +14673,5,297000130,8 +14674,5,297000140,8 +14675,5,203000010,8 +14676,5,206000030,8 +14677,5,203000050,8 +14678,5,202000090,8 +14679,5,204000080,8 +14680,5,202000150,8 +14681,5,204000100,8 +14682,2,170003,3 +14683,2,180003,3 +14684,3,170004,3 +14685,3,180004,3 +14686,4,140000,3 +14687,5,150010,3 +14688,5,150020,3 +14689,5,150030,3 +14690,5,150040,3 +14692,3,101000050,1 +14693,3,101000060,1 +14694,3,101000080,1 +14695,3,101000040,1 +14696,3,109000060,1 +14697,3,109000070,1 +14698,3,109000080,1 +14699,3,105000060,1 +14700,3,105000070,1 +14701,3,105000080,1 +14702,3,104000050,1 +14703,3,106000050,1 +14704,3,102000060,1 +14705,3,102000070,1 +14706,3,102000080,1 +14707,3,103000050,1 +14708,3,105000050,1 +14709,3,107000060,1 +14710,3,107000070,1 +14711,3,107000080,1 +14712,3,108000050,1 +14713,3,109000050,1 +14714,3,103000060,1 +14715,3,103000070,1 +14716,3,103000080,1 +14717,3,110000050,1 +14718,3,106000060,1 +14719,3,106000070,1 +14720,3,106000080,1 +14721,3,101000070,1 +14722,3,110000060,1 +14723,3,104000060,1 +14724,3,104000070,1 +14725,3,104000080,1 +14726,3,102000050,1 +14727,3,104000170,1 +14728,3,104000260,1 +14729,3,111000010,1 +14730,3,111000020,1 +14731,3,111000030,1 +14732,3,112000020,1 +14733,3,112000030,1 +14734,3,108000060,1 +14735,3,108000070,1 +14736,3,108000080,1 +14737,3,107000050,1 +14738,3,112000010,1 +14739,3,110000070,1 +14740,3,110000080,1 +14741,3,118000020,1 +14742,3,118000030,1 +14743,3,118000040,1 +14744,4,101000090,1 +14745,4,101000100,1 +14746,4,101000110,1 +14747,4,109000100,1 +14748,4,105000100,1 +14749,4,105000110,1 +14750,4,108000090,1 +14751,4,110000090,1 +14752,4,102000100,1 +14753,4,102000110,1 +14754,4,106000090,1 +14755,4,109000090,1 +14756,4,107000100,1 +14757,4,103000090,1 +14758,4,102000090,1 +14759,4,103000100,1 +14760,4,106000100,1 +14761,4,106000110,1 +14762,4,104000090,1 +14763,4,104000100,1 +14764,4,104000110,1 +14765,4,107000090,1 +14766,4,104000180,1 +14767,4,111000040,1 +14768,4,112000040,1 +14769,4,108000100,1 +14770,4,105000090,1 +14771,4,110000100,1 +14772,4,118000050,1 +14773,4,118000060,1 +14774,5,101000120,1 +14775,5,109000110,1 +14776,5,105000120,1 +14777,5,102000120,1 +14778,5,107000110,1 +14779,5,103000120,1 +14780,5,106000120,1 +14781,5,104000120,1 +14782,5,104000190,1 +14783,5,111000060,1 +14784,5,112000060,1 +14785,5,108000110,1 +14786,5,110000110,1 +14787,5,118000070,1 +14788,1,201000010,8 +14789,1,292000010,8 +14790,1,299000040,8 +14791,1,299000070,8 +14792,1,299000110,8 +14793,1,299000120,8 +14794,1,299000140,8 +14795,2,202000010,8 +14796,2,290000010,8 +14797,2,299000010,8 +14798,2,299000150,8 +14799,2,299000190,8 +14800,2,299000200,8 +14801,2,299000210,8 +14802,3,298000050,8 +14803,3,298000060,8 +14804,3,299000060,8 +14805,3,299000170,8 +14806,3,290000120,8 +14807,3,291000050,8 +14808,3,292000020,8 +14809,4,299000670,8 +14810,4,299000680,8 +14811,4,204000010,8 +14812,4,209000040,8 +14813,4,202000070,8 +14814,4,209000070,8 +14815,4,203000110,8 +14816,4,290000110,8 +14817,4,206000110,8 +14818,5,297000100,8 +14819,5,291000020,8 +14820,5,297000130,8 +14821,5,297000140,8 +14822,5,203000010,8 +14823,5,206000030,8 +14824,5,203000050,8 +14825,5,202000090,8 +14826,5,204000080,8 +14827,5,202000150,8 +14828,5,204000100,8 +14829,3,170004,3 +14830,3,180004,3 +14831,4,140000,3 +14832,5,150010,3 +14833,5,150020,3 +14834,5,150030,3 +14835,5,150040,3 +14837,3,101000050,1 +14838,3,101000060,1 +14839,3,101000080,1 +14840,3,101000040,1 +14841,3,109000060,1 +14842,3,109000070,1 +14843,3,109000080,1 +14844,3,105000060,1 +14845,3,105000070,1 +14846,3,105000080,1 +14847,3,104000050,1 +14848,3,106000050,1 +14849,3,102000060,1 +14850,3,102000070,1 +14851,3,102000080,1 +14852,3,103000050,1 +14853,3,105000050,1 +14854,3,107000060,1 +14855,3,107000070,1 +14856,3,107000080,1 +14857,3,108000050,1 +14858,3,109000050,1 +14859,3,103000060,1 +14860,3,103000070,1 +14861,3,103000080,1 +14862,3,110000050,1 +14863,3,106000060,1 +14864,3,106000070,1 +14865,3,106000080,1 +14866,3,101000070,1 +14867,3,110000060,1 +14868,3,104000060,1 +14869,3,104000070,1 +14870,3,104000080,1 +14871,3,102000050,1 +14872,3,104000170,1 +14873,3,104000260,1 +14874,3,111000010,1 +14875,3,111000020,1 +14876,3,111000030,1 +14877,3,112000020,1 +14878,3,112000030,1 +14879,3,108000060,1 +14880,3,108000070,1 +14881,3,108000080,1 +14882,3,107000050,1 +14883,3,112000010,1 +14884,3,110000070,1 +14885,3,110000080,1 +14886,3,118000020,1 +14887,3,118000030,1 +14888,3,118000040,1 +14889,4,101000090,1 +14890,4,101000100,1 +14891,4,101000110,1 +14892,4,109000100,1 +14893,4,105000100,1 +14894,4,105000110,1 +14895,4,108000090,1 +14896,4,110000090,1 +14897,4,102000100,1 +14898,4,102000110,1 +14899,4,106000090,1 +14900,4,109000090,1 +14901,4,107000100,1 +14902,4,103000090,1 +14903,4,102000090,1 +14904,4,103000100,1 +14905,4,106000100,1 +14906,4,106000110,1 +14907,4,104000090,1 +14908,4,104000100,1 +14909,4,104000110,1 +14910,4,107000090,1 +14911,4,104000180,1 +14912,4,111000040,1 +14913,4,112000040,1 +14914,4,108000100,1 +14915,4,105000090,1 +14916,4,110000100,1 +14917,4,118000050,1 +14918,4,118000060,1 +14919,5,101000120,1 +14920,5,109000110,1 +14921,5,105000120,1 +14922,5,102000120,1 +14923,5,107000110,1 +14924,5,103000120,1 +14925,5,106000120,1 +14926,5,104000120,1 +14927,5,104000190,1 +14928,5,111000060,1 +14929,5,112000060,1 +14930,5,108000110,1 +14931,5,110000110,1 +14932,5,118000070,1 +14933,1,201000010,8 +14934,1,292000010,8 +14935,1,299000040,8 +14936,1,299000070,8 +14937,1,299000110,8 +14938,1,299000120,8 +14939,1,299000140,8 +14940,2,202000010,8 +14941,2,290000010,8 +14942,2,299000010,8 +14943,2,299000150,8 +14944,2,299000190,8 +14945,2,299000200,8 +14946,2,299000210,8 +14947,3,298000050,8 +14948,3,298000060,8 +14949,3,299000060,8 +14950,3,299000170,8 +14951,3,290000120,8 +14952,3,291000050,8 +14953,3,292000020,8 +14954,4,299000670,8 +14955,4,299000680,8 +14956,4,204000010,8 +14957,4,209000040,8 +14958,4,202000070,8 +14959,4,209000070,8 +14960,4,203000110,8 +14961,4,290000110,8 +14962,4,206000110,8 +14963,5,297000100,8 +14964,5,291000020,8 +14965,5,297000130,8 +14966,5,297000140,8 +14967,5,203000010,8 +14968,5,206000030,8 +14969,5,203000050,8 +14970,5,202000090,8 +14971,5,204000080,8 +14972,5,202000150,8 +14973,5,204000100,8 +14974,3,170004,3 +14975,3,180004,3 +14976,4,140000,3 +14977,5,150010,3 +14978,5,150020,3 +14979,5,150030,3 +14980,5,150040,3 +14982,3,101000050,1 +14983,3,101000060,1 +14984,3,101000080,1 +14985,3,101000040,1 +14986,3,109000060,1 +14987,3,109000070,1 +14988,3,109000080,1 +14989,3,105000060,1 +14990,3,105000070,1 +14991,3,105000080,1 +14992,3,104000050,1 +14993,3,106000050,1 +14994,3,102000060,1 +14995,3,102000070,1 +14996,3,102000080,1 +14997,3,103000050,1 +14998,3,105000050,1 +14999,3,107000060,1 +15000,3,107000070,1 +15001,3,107000080,1 +15002,3,108000050,1 +15003,3,109000050,1 +15004,3,103000060,1 +15005,3,103000070,1 +15006,3,103000080,1 +15007,3,110000050,1 +15008,3,106000060,1 +15009,3,106000070,1 +15010,3,106000080,1 +15011,3,101000070,1 +15012,3,110000060,1 +15013,3,104000060,1 +15014,3,104000070,1 +15015,3,104000080,1 +15016,3,102000050,1 +15017,3,104000170,1 +15018,3,104000260,1 +15019,3,111000010,1 +15020,3,111000020,1 +15021,3,111000030,1 +15022,3,112000020,1 +15023,3,112000030,1 +15024,3,108000060,1 +15025,3,108000070,1 +15026,3,108000080,1 +15027,3,107000050,1 +15028,3,112000010,1 +15029,3,110000070,1 +15030,3,110000080,1 +15031,3,118000020,1 +15032,3,118000030,1 +15033,3,118000040,1 +15034,4,101000090,1 +15035,4,101000100,1 +15036,4,101000110,1 +15037,4,109000100,1 +15038,4,105000100,1 +15039,4,105000110,1 +15040,4,108000090,1 +15041,4,110000090,1 +15042,4,102000100,1 +15043,4,102000110,1 +15044,4,106000090,1 +15045,4,109000090,1 +15046,4,107000100,1 +15047,4,103000090,1 +15048,4,102000090,1 +15049,4,103000100,1 +15050,4,106000100,1 +15051,4,106000110,1 +15052,4,104000090,1 +15053,4,104000100,1 +15054,4,104000110,1 +15055,4,107000090,1 +15056,4,104000180,1 +15057,4,111000040,1 +15058,4,112000040,1 +15059,4,108000100,1 +15060,4,105000090,1 +15061,4,110000100,1 +15062,4,118000050,1 +15063,4,118000060,1 +15064,5,101000120,1 +15065,5,109000110,1 +15066,5,105000120,1 +15067,5,102000120,1 +15068,5,107000110,1 +15069,5,103000120,1 +15070,5,106000120,1 +15071,5,104000120,1 +15072,5,104000190,1 +15073,5,111000060,1 +15074,5,112000060,1 +15075,5,108000110,1 +15076,5,110000110,1 +15077,5,118000070,1 +15078,1,201000010,8 +15079,1,292000010,8 +15080,1,299000040,8 +15081,1,299000070,8 +15082,1,299000110,8 +15083,1,299000120,8 +15084,1,299000140,8 +15085,2,202000010,8 +15086,2,290000010,8 +15087,2,299000010,8 +15088,2,299000150,8 +15089,2,299000190,8 +15090,2,299000200,8 +15091,2,299000210,8 +15092,3,298000050,8 +15093,3,298000060,8 +15094,3,299000060,8 +15095,3,299000170,8 +15096,3,290000120,8 +15097,3,291000050,8 +15098,3,292000020,8 +15099,4,299000670,8 +15100,4,299000680,8 +15101,4,204000010,8 +15102,4,209000040,8 +15103,4,202000070,8 +15104,4,209000070,8 +15105,4,203000110,8 +15106,4,290000110,8 +15107,4,206000110,8 +15108,5,297000100,8 +15109,5,291000020,8 +15110,5,297000130,8 +15111,5,297000140,8 +15112,5,203000010,8 +15113,5,206000030,8 +15114,5,203000050,8 +15115,5,202000090,8 +15116,5,204000080,8 +15117,5,202000150,8 +15118,5,204000100,8 +15119,3,170004,3 +15120,3,180004,3 +15121,4,140000,3 +15122,5,150010,3 +15123,5,150020,3 +15124,5,150030,3 +15125,5,150040,3 +15126,5,190000,3 +15127,5,200000,3 +15128,5,210000,3 +15130,3,111000010,1 +15131,3,111000020,1 +15132,3,111000030,1 +15133,3,112000020,1 +15134,3,112000030,1 +15135,3,108000060,1 +15136,3,108000070,1 +15137,3,108000080,1 +15138,3,107000050,1 +15139,3,112000010,1 +15140,3,110000070,1 +15141,3,110000080,1 +15142,4,111000040,1 +15143,4,112000040,1 +15144,4,108000100,1 +15145,4,105000090,1 +15146,4,110000100,1 +15147,5,111000060,1 +15148,5,112000060,1 +15149,5,108000110,1 +15150,5,110000110,1 +15151,1,108000000,2 +15152,2,108000001,2 +15153,3,108000002,2 +15154,4,108000003,2 +15155,5,108000004,2 +15156,1,107000000,2 +15157,2,107000001,2 +15158,3,107000002,2 +15159,4,107000003,2 +15160,5,107000004,2 +15161,1,120000000,2 +15162,2,120000001,2 +15163,3,120000002,2 +15164,4,120000003,2 +15165,5,120000004,2 +15166,3,170004,3 +15167,4,170005,3 +15168,3,180004,3 +15169,4,180005,3 +15170,4,140000,3 +15171,4,150010,3 +15172,4,150020,3 +15173,4,150030,3 +15174,4,150040,3 +15176,3,101000050,1 +15177,3,101000060,1 +15178,3,101000080,1 +15179,3,101000040,1 +15180,3,109000060,1 +15181,3,109000070,1 +15182,3,109000080,1 +15183,3,105000060,1 +15184,3,105000070,1 +15185,3,105000080,1 +15186,3,104000050,1 +15187,3,106000050,1 +15188,4,101000090,1 +15189,4,101000100,1 +15190,4,101000110,1 +15191,4,109000100,1 +15192,4,105000100,1 +15193,4,105000110,1 +15194,4,108000090,1 +15195,4,110000090,1 +15196,5,101000120,1 +15197,5,109000110,1 +15198,5,105000120,1 +15199,1,101000000,2 +15200,2,101000001,2 +15201,3,101000002,2 +15202,4,101000008,2 +15203,5,101000004,2 +15204,1,109000000,2 +15205,2,109000001,2 +15206,3,109000002,2 +15207,4,109000003,2 +15208,5,109000004,2 +15209,4,110024,3 +15210,4,110034,3 +15211,4,110044,3 +15212,4,110054,3 +15213,3,110060,3 +15214,3,110070,3 +15215,3,110080,3 +15216,3,110090,3 +15217,3,110100,3 +15218,3,110110,3 +15219,3,110120,3 +15220,3,110130,3 +15221,3,110140,3 +15222,3,110150,3 +15223,3,110160,3 +15224,3,110170,3 +15225,3,110180,3 +15226,3,110190,3 +15227,3,110200,3 +15228,3,110210,3 +15229,3,110220,3 +15230,3,110230,3 +15231,3,110240,3 +15232,3,110250,3 +15233,3,110260,3 +15234,3,110270,3 +15235,3,110620,3 +15236,3,110670,3 +15237,4,140000,3 +15238,4,150010,3 +15239,4,150020,3 +15240,4,150030,3 +15241,4,150040,3 +15243,3,102000060,1 +15244,3,102000070,1 +15245,3,102000080,1 +15246,3,103000050,1 +15247,3,105000050,1 +15248,3,107000060,1 +15249,3,107000070,1 +15250,3,107000080,1 +15251,3,108000050,1 +15252,3,109000050,1 +15253,3,103000060,1 +15254,3,103000070,1 +15255,3,103000080,1 +15256,3,110000050,1 +15257,4,102000100,1 +15258,4,102000110,1 +15259,4,102000350,1 +15260,4,106000090,1 +15261,4,109000090,1 +15262,4,107000100,1 +15263,4,103000090,1 +15264,4,102000090,1 +15265,4,103000100,1 +15266,5,102000120,1 +15267,5,107000110,1 +15268,5,103000120,1 +15269,1,102000000,2 +15270,2,102000001,2 +15271,3,102000002,2 +15272,4,102000003,2 +15273,5,102000004,2 +15274,1,105000000,2 +15275,2,105000001,2 +15276,3,105000002,2 +15277,4,105000003,2 +15278,5,105000004,2 +15279,1,112000000,2 +15280,2,112000001,2 +15281,3,112000002,2 +15282,4,112000003,2 +15283,5,112000004,2 +15284,4,120024,3 +15285,4,120034,3 +15286,4,120044,3 +15287,4,120054,3 +15288,3,120241,3 +15289,3,120251,3 +15290,3,120261,3 +15291,3,120271,3 +15292,3,120300,3 +15293,3,120310,3 +15294,3,120320,3 +15295,3,120330,3 +15296,3,120340,3 +15297,3,120350,3 +15298,3,120360,3 +15299,3,120370,3 +15300,3,120380,3 +15301,3,120390,3 +15302,3,120400,3 +15303,3,120410,3 +15304,3,120420,3 +15305,3,120430,3 +15306,3,120450,3 +15307,3,120460,3 +15308,3,120550,3 +15309,3,120560,3 +15310,3,120570,3 +15311,3,120990,3 +15312,3,121000,3 +15313,3,121010,3 +15314,3,121020,3 +15315,3,121100,3 +15316,4,140000,3 +15317,4,150010,3 +15318,4,150020,3 +15319,4,150030,3 +15320,4,150040,3 +15322,3,118000020,1 +15323,3,118000030,1 +15324,3,118000040,1 +15325,3,106000060,1 +15326,3,106000070,1 +15327,3,106000080,1 +15328,3,101000070,1 +15329,3,110000060,1 +15330,3,104000060,1 +15331,3,104000070,1 +15332,3,104000080,1 +15333,3,102000050,1 +15334,3,104000170,1 +15335,3,104000260,1 +15336,4,118000050,1 +15337,4,118000060,1 +15338,4,106000100,1 +15339,4,106000110,1 +15340,4,104000090,1 +15341,4,104000100,1 +15342,4,104000110,1 +15343,4,104000270,1 +15344,4,107000090,1 +15345,4,104000180,1 +15346,5,118000070,1 +15347,5,106000120,1 +15348,5,104000120,1 +15349,5,104000190,1 +15350,1,103000000,2 +15351,2,103000001,2 +15352,3,103000002,2 +15353,4,103000003,2 +15354,5,103000004,2 +15355,1,111000000,2 +15356,2,111000001,2 +15357,3,111000002,2 +15358,4,111000003,2 +15359,5,111000004,2 +15360,1,115000000,2 +15361,2,115000001,2 +15362,3,115000002,2 +15363,4,115000003,2 +15364,5,115000004,2 +15365,4,130024,3 +15366,4,130034,3 +15367,4,130044,3 +15368,4,130054,3 +15369,3,130060,3 +15370,3,130070,3 +15371,3,130080,3 +15372,3,130090,3 +15373,3,130100,3 +15374,3,130110,3 +15375,3,130120,3 +15376,3,130130,3 +15377,3,130140,3 +15378,3,130150,3 +15379,3,130160,3 +15380,3,130170,3 +15381,3,130180,3 +15382,3,130190,3 +15383,3,130200,3 +15384,3,130420,3 +15385,3,130510,3 +15386,3,130520,3 +15387,3,130531,3 +15388,3,130540,3 +15389,3,130660,3 +15390,3,130700,3 +15391,3,130790,3 +15392,3,130800,3 +15393,3,131130,3 +15394,4,140000,3 +15395,4,150010,3 +15396,4,150020,3 +15397,4,150030,3 +15398,4,150040,3 +15400,1,101000010,1 +15401,1,102000010,1 +15402,1,103000010,1 +15403,1,104000010,1 +15404,1,105000010,1 +15405,1,106000010,1 +15406,1,107000010,1 +15407,1,108000010,1 +15408,1,109000010,1 +15409,1,110000010,1 +15410,2,101000020,1 +15411,2,101000030,1 +15412,2,102000020,1 +15413,2,102000030,1 +15414,2,102000040,1 +15415,2,103000020,1 +15416,2,103000030,1 +15417,2,103000040,1 +15418,2,104000020,1 +15419,2,104000030,1 +15420,2,104000040,1 +15421,2,105000020,1 +15422,2,105000030,1 +15423,2,105000040,1 +15424,2,106000020,1 +15425,2,106000030,1 +15426,2,106000040,1 +15427,2,107000020,1 +15428,2,107000030,1 +15429,2,107000040,1 +15430,2,108000020,1 +15431,2,108000030,1 +15432,2,108000040,1 +15433,2,109000020,1 +15434,2,109000030,1 +15435,2,109000040,1 +15436,2,110000020,1 +15437,2,110000030,1 +15438,2,110000040,1 +15439,2,118000010,1 +15440,3,101000050,1 +15441,3,101000060,1 +15442,3,101000080,1 +15443,3,101000040,1 +15444,3,109000060,1 +15445,3,109000070,1 +15446,3,109000080,1 +15447,3,105000060,1 +15448,3,105000070,1 +15449,3,105000080,1 +15450,3,104000050,1 +15451,3,106000050,1 +15452,3,102000060,1 +15453,3,102000070,1 +15454,3,102000080,1 +15455,3,103000050,1 +15456,3,105000050,1 +15457,3,107000060,1 +15458,3,107000070,1 +15459,3,107000080,1 +15460,3,108000050,1 +15461,3,109000050,1 +15462,3,103000060,1 +15463,3,103000070,1 +15464,3,103000080,1 +15465,3,110000050,1 +15466,3,106000060,1 +15467,3,106000070,1 +15468,3,106000080,1 +15469,3,101000070,1 +15470,3,110000060,1 +15471,3,104000060,1 +15472,3,104000070,1 +15473,3,104000080,1 +15474,3,102000050,1 +15475,3,104000170,1 +15476,3,104000260,1 +15477,3,111000010,1 +15478,3,111000020,1 +15479,3,111000030,1 +15480,3,112000020,1 +15481,3,112000030,1 +15482,3,108000060,1 +15483,3,108000070,1 +15484,3,108000080,1 +15485,3,107000050,1 +15486,3,112000010,1 +15487,3,110000070,1 +15488,3,110000080,1 +15489,3,118000020,1 +15490,3,118000030,1 +15491,3,118000040,1 +15492,4,101000090,1 +15493,4,101000100,1 +15494,4,101000110,1 +15495,4,109000100,1 +15496,4,105000100,1 +15497,4,105000110,1 +15498,4,108000090,1 +15499,4,110000090,1 +15500,4,102000100,1 +15501,4,102000110,1 +15502,4,106000090,1 +15503,4,109000090,1 +15504,4,107000100,1 +15505,4,103000090,1 +15506,4,102000090,1 +15507,4,103000100,1 +15508,4,106000100,1 +15509,4,106000110,1 +15510,4,104000090,1 +15511,4,104000100,1 +15512,4,104000110,1 +15513,4,107000090,1 +15514,4,104000180,1 +15515,4,111000040,1 +15516,4,112000040,1 +15517,4,108000100,1 +15518,4,105000090,1 +15519,4,110000100,1 +15520,4,118000050,1 +15521,4,118000060,1 +15522,5,101000120,1 +15523,5,109000110,1 +15524,5,105000120,1 +15525,5,102000120,1 +15526,5,107000110,1 +15527,5,103000120,1 +15528,5,106000120,1 +15529,5,104000120,1 +15530,5,104000190,1 +15531,5,111000060,1 +15532,5,112000060,1 +15533,5,108000110,1 +15534,5,110000110,1 +15535,5,118000070,1 +15536,1,201000010,8 +15537,1,292000010,8 +15538,1,299000040,8 +15539,1,299000070,8 +15540,1,299000110,8 +15541,1,299000120,8 +15542,1,299000140,8 +15543,2,202000010,8 +15544,2,290000010,8 +15545,2,299000010,8 +15546,2,299000150,8 +15547,2,299000190,8 +15548,2,299000200,8 +15549,2,299000210,8 +15550,3,298000050,8 +15551,3,298000060,8 +15552,3,299000060,8 +15553,3,299000170,8 +15554,3,290000120,8 +15555,3,291000050,8 +15556,3,292000020,8 +15557,4,299000670,8 +15558,4,299000680,8 +15559,4,204000010,8 +15560,4,209000040,8 +15561,4,202000070,8 +15562,4,209000070,8 +15563,4,203000110,8 +15564,4,290000110,8 +15565,4,206000110,8 +15566,4,209000160,8 +15567,5,297000100,8 +15568,5,291000020,8 +15569,5,297000130,8 +15570,5,297000140,8 +15571,5,203000010,8 +15572,5,206000030,8 +15573,5,203000050,8 +15574,5,202000090,8 +15575,5,204000080,8 +15576,5,202000150,8 +15577,5,204000100,8 +15578,5,206000170,8 +15579,1,170002,3 +15580,1,180002,3 +15581,2,170003,3 +15582,2,180003,3 +15583,3,170004,3 +15584,3,180004,3 +15585,4,140000,3 +15586,5,150010,3 +15587,5,150020,3 +15588,5,150030,3 +15589,5,150040,3 +15591,1,101000010,1 +15592,1,102000010,1 +15593,1,103000010,1 +15594,1,104000010,1 +15595,1,105000010,1 +15596,1,106000010,1 +15597,1,107000010,1 +15598,1,108000010,1 +15599,1,109000010,1 +15600,1,110000010,1 +15601,2,101000020,1 +15602,2,101000030,1 +15603,2,102000020,1 +15604,2,102000030,1 +15605,2,102000040,1 +15606,2,103000020,1 +15607,2,103000030,1 +15608,2,103000040,1 +15609,2,104000020,1 +15610,2,104000030,1 +15611,2,104000040,1 +15612,2,105000020,1 +15613,2,105000030,1 +15614,2,105000040,1 +15615,2,106000020,1 +15616,2,106000030,1 +15617,2,106000040,1 +15618,2,107000020,1 +15619,2,107000030,1 +15620,2,107000040,1 +15621,2,108000020,1 +15622,2,108000030,1 +15623,2,108000040,1 +15624,2,109000020,1 +15625,2,109000030,1 +15626,2,109000040,1 +15627,2,110000020,1 +15628,2,110000030,1 +15629,2,110000040,1 +15630,2,118000010,1 +15631,3,101000050,1 +15632,3,101000060,1 +15633,3,101000080,1 +15634,3,101000040,1 +15635,3,109000060,1 +15636,3,109000070,1 +15637,3,109000080,1 +15638,3,105000060,1 +15639,3,105000070,1 +15640,3,105000080,1 +15641,3,104000050,1 +15642,3,106000050,1 +15643,3,102000060,1 +15644,3,102000070,1 +15645,3,102000080,1 +15646,3,103000050,1 +15647,3,105000050,1 +15648,3,107000060,1 +15649,3,107000070,1 +15650,3,107000080,1 +15651,3,108000050,1 +15652,3,109000050,1 +15653,3,103000060,1 +15654,3,103000070,1 +15655,3,103000080,1 +15656,3,110000050,1 +15657,3,106000060,1 +15658,3,106000070,1 +15659,3,106000080,1 +15660,3,101000070,1 +15661,3,110000060,1 +15662,3,104000060,1 +15663,3,104000070,1 +15664,3,104000080,1 +15665,3,102000050,1 +15666,3,104000170,1 +15667,3,104000260,1 +15668,3,111000010,1 +15669,3,111000020,1 +15670,3,111000030,1 +15671,3,112000020,1 +15672,3,112000030,1 +15673,3,108000060,1 +15674,3,108000070,1 +15675,3,108000080,1 +15676,3,107000050,1 +15677,3,112000010,1 +15678,3,110000070,1 +15679,3,110000080,1 +15680,3,118000020,1 +15681,3,118000030,1 +15682,3,118000040,1 +15683,4,101000090,1 +15684,4,101000100,1 +15685,4,101000110,1 +15686,4,109000100,1 +15687,4,105000100,1 +15688,4,105000110,1 +15689,4,108000090,1 +15690,4,110000090,1 +15691,4,102000100,1 +15692,4,102000110,1 +15693,4,106000090,1 +15694,4,109000090,1 +15695,4,107000100,1 +15696,4,103000090,1 +15697,4,102000090,1 +15698,4,103000100,1 +15699,4,106000100,1 +15700,4,106000110,1 +15701,4,104000090,1 +15702,4,104000100,1 +15703,4,104000110,1 +15704,4,107000090,1 +15705,4,104000180,1 +15706,4,111000040,1 +15707,4,112000040,1 +15708,4,108000100,1 +15709,4,105000090,1 +15710,4,110000100,1 +15711,4,118000050,1 +15712,4,118000060,1 +15713,5,101000120,1 +15714,5,109000110,1 +15715,5,105000120,1 +15716,5,102000120,1 +15717,5,107000110,1 +15718,5,103000120,1 +15719,5,106000120,1 +15720,5,104000120,1 +15721,5,104000190,1 +15722,5,111000060,1 +15723,5,112000060,1 +15724,5,108000110,1 +15725,5,110000110,1 +15726,5,118000070,1 +15727,1,201000010,8 +15728,1,292000010,8 +15729,1,299000040,8 +15730,1,299000070,8 +15731,1,299000110,8 +15732,1,299000120,8 +15733,1,299000140,8 +15734,2,202000010,8 +15735,2,290000010,8 +15736,2,299000010,8 +15737,2,299000150,8 +15738,2,299000190,8 +15739,2,299000200,8 +15740,2,299000210,8 +15741,3,298000050,8 +15742,3,298000060,8 +15743,3,299000060,8 +15744,3,299000170,8 +15745,3,290000120,8 +15746,3,291000050,8 +15747,3,292000020,8 +15748,4,299000670,8 +15749,4,299000680,8 +15750,4,204000010,8 +15751,4,209000040,8 +15752,4,202000070,8 +15753,4,209000070,8 +15754,4,203000110,8 +15755,4,290000110,8 +15756,4,206000110,8 +15757,4,209000160,8 +15758,5,297000100,8 +15759,5,291000020,8 +15760,5,297000130,8 +15761,5,297000140,8 +15762,5,203000010,8 +15763,5,206000030,8 +15764,5,203000050,8 +15765,5,202000090,8 +15766,5,204000080,8 +15767,5,202000150,8 +15768,5,204000100,8 +15769,5,206000170,8 +15770,2,170003,3 +15771,2,180003,3 +15772,3,170004,3 +15773,3,180004,3 +15774,4,140000,3 +15775,5,150010,3 +15776,5,150020,3 +15777,5,150030,3 +15778,5,150040,3 +15780,3,101000050,1 +15781,3,101000060,1 +15782,3,101000080,1 +15783,3,101000040,1 +15784,3,109000060,1 +15785,3,109000070,1 +15786,3,109000080,1 +15787,3,105000060,1 +15788,3,105000070,1 +15789,3,105000080,1 +15790,3,104000050,1 +15791,3,106000050,1 +15792,3,102000060,1 +15793,3,102000070,1 +15794,3,102000080,1 +15795,3,103000050,1 +15796,3,105000050,1 +15797,3,107000060,1 +15798,3,107000070,1 +15799,3,107000080,1 +15800,3,108000050,1 +15801,3,109000050,1 +15802,3,103000060,1 +15803,3,103000070,1 +15804,3,103000080,1 +15805,3,110000050,1 +15806,3,106000060,1 +15807,3,106000070,1 +15808,3,106000080,1 +15809,3,101000070,1 +15810,3,110000060,1 +15811,3,104000060,1 +15812,3,104000070,1 +15813,3,104000080,1 +15814,3,102000050,1 +15815,3,104000170,1 +15816,3,104000260,1 +15817,3,111000010,1 +15818,3,111000020,1 +15819,3,111000030,1 +15820,3,112000020,1 +15821,3,112000030,1 +15822,3,108000060,1 +15823,3,108000070,1 +15824,3,108000080,1 +15825,3,107000050,1 +15826,3,112000010,1 +15827,3,110000070,1 +15828,3,110000080,1 +15829,3,118000020,1 +15830,3,118000030,1 +15831,3,118000040,1 +15832,4,101000090,1 +15833,4,101000100,1 +15834,4,101000110,1 +15835,4,109000100,1 +15836,4,105000100,1 +15837,4,105000110,1 +15838,4,108000090,1 +15839,4,110000090,1 +15840,4,102000100,1 +15841,4,102000110,1 +15842,4,106000090,1 +15843,4,109000090,1 +15844,4,107000100,1 +15845,4,103000090,1 +15846,4,102000090,1 +15847,4,103000100,1 +15848,4,106000100,1 +15849,4,106000110,1 +15850,4,104000090,1 +15851,4,104000100,1 +15852,4,104000110,1 +15853,4,107000090,1 +15854,4,104000180,1 +15855,4,111000040,1 +15856,4,112000040,1 +15857,4,108000100,1 +15858,4,105000090,1 +15859,4,110000100,1 +15860,4,118000050,1 +15861,4,118000060,1 +15862,5,101000120,1 +15863,5,109000110,1 +15864,5,105000120,1 +15865,5,102000120,1 +15866,5,107000110,1 +15867,5,103000120,1 +15868,5,106000120,1 +15869,5,104000120,1 +15870,5,104000190,1 +15871,5,111000060,1 +15872,5,112000060,1 +15873,5,108000110,1 +15874,5,110000110,1 +15875,5,118000070,1 +15876,1,201000010,8 +15877,1,292000010,8 +15878,1,299000040,8 +15879,1,299000070,8 +15880,1,299000110,8 +15881,1,299000120,8 +15882,1,299000140,8 +15883,2,202000010,8 +15884,2,290000010,8 +15885,2,299000010,8 +15886,2,299000150,8 +15887,2,299000190,8 +15888,2,299000200,8 +15889,2,299000210,8 +15890,3,298000050,8 +15891,3,298000060,8 +15892,3,299000060,8 +15893,3,299000170,8 +15894,3,290000120,8 +15895,3,291000050,8 +15896,3,292000020,8 +15897,4,299000670,8 +15898,4,299000680,8 +15899,4,204000010,8 +15900,4,209000040,8 +15901,4,202000070,8 +15902,4,209000070,8 +15903,4,203000110,8 +15904,4,290000110,8 +15905,4,206000110,8 +15906,4,209000160,8 +15907,5,297000100,8 +15908,5,291000020,8 +15909,5,297000130,8 +15910,5,297000140,8 +15911,5,203000010,8 +15912,5,206000030,8 +15913,5,203000050,8 +15914,5,202000090,8 +15915,5,204000080,8 +15916,5,202000150,8 +15917,5,204000100,8 +15918,5,206000170,8 +15919,3,170004,3 +15920,3,180004,3 +15921,4,140000,3 +15922,5,150010,3 +15923,5,150020,3 +15924,5,150030,3 +15925,5,150040,3 +15927,3,101000050,1 +15928,3,101000060,1 +15929,3,101000080,1 +15930,3,101000040,1 +15931,3,109000060,1 +15932,3,109000070,1 +15933,3,109000080,1 +15934,3,105000060,1 +15935,3,105000070,1 +15936,3,105000080,1 +15937,3,104000050,1 +15938,3,106000050,1 +15939,3,102000060,1 +15940,3,102000070,1 +15941,3,102000080,1 +15942,3,103000050,1 +15943,3,105000050,1 +15944,3,107000060,1 +15945,3,107000070,1 +15946,3,107000080,1 +15947,3,108000050,1 +15948,3,109000050,1 +15949,3,103000060,1 +15950,3,103000070,1 +15951,3,103000080,1 +15952,3,110000050,1 +15953,3,106000060,1 +15954,3,106000070,1 +15955,3,106000080,1 +15956,3,101000070,1 +15957,3,110000060,1 +15958,3,104000060,1 +15959,3,104000070,1 +15960,3,104000080,1 +15961,3,102000050,1 +15962,3,104000170,1 +15963,3,104000260,1 +15964,3,111000010,1 +15965,3,111000020,1 +15966,3,111000030,1 +15967,3,112000020,1 +15968,3,112000030,1 +15969,3,108000060,1 +15970,3,108000070,1 +15971,3,108000080,1 +15972,3,107000050,1 +15973,3,112000010,1 +15974,3,110000070,1 +15975,3,110000080,1 +15976,3,118000020,1 +15977,3,118000030,1 +15978,3,118000040,1 +15979,4,101000090,1 +15980,4,101000100,1 +15981,4,101000110,1 +15982,4,109000100,1 +15983,4,105000100,1 +15984,4,105000110,1 +15985,4,108000090,1 +15986,4,110000090,1 +15987,4,102000100,1 +15988,4,102000110,1 +15989,4,106000090,1 +15990,4,109000090,1 +15991,4,107000100,1 +15992,4,103000090,1 +15993,4,102000090,1 +15994,4,103000100,1 +15995,4,106000100,1 +15996,4,106000110,1 +15997,4,104000090,1 +15998,4,104000100,1 +15999,4,104000110,1 +16000,4,107000090,1 +16001,4,104000180,1 +16002,4,111000040,1 +16003,4,112000040,1 +16004,4,108000100,1 +16005,4,105000090,1 +16006,4,110000100,1 +16007,4,118000050,1 +16008,4,118000060,1 +16009,5,101000120,1 +16010,5,109000110,1 +16011,5,105000120,1 +16012,5,102000120,1 +16013,5,107000110,1 +16014,5,103000120,1 +16015,5,106000120,1 +16016,5,104000120,1 +16017,5,104000190,1 +16018,5,111000060,1 +16019,5,112000060,1 +16020,5,108000110,1 +16021,5,110000110,1 +16022,5,118000070,1 +16023,1,201000010,8 +16024,1,292000010,8 +16025,1,299000040,8 +16026,1,299000070,8 +16027,1,299000110,8 +16028,1,299000120,8 +16029,1,299000140,8 +16030,2,202000010,8 +16031,2,290000010,8 +16032,2,299000010,8 +16033,2,299000150,8 +16034,2,299000190,8 +16035,2,299000200,8 +16036,2,299000210,8 +16037,3,298000050,8 +16038,3,298000060,8 +16039,3,299000060,8 +16040,3,299000170,8 +16041,3,290000120,8 +16042,3,291000050,8 +16043,3,292000020,8 +16044,4,299000670,8 +16045,4,299000680,8 +16046,4,204000010,8 +16047,4,209000040,8 +16048,4,202000070,8 +16049,4,209000070,8 +16050,4,203000110,8 +16051,4,290000110,8 +16052,4,206000110,8 +16053,4,209000160,8 +16054,5,297000100,8 +16055,5,291000020,8 +16056,5,297000130,8 +16057,5,297000140,8 +16058,5,203000010,8 +16059,5,206000030,8 +16060,5,203000050,8 +16061,5,202000090,8 +16062,5,204000080,8 +16063,5,202000150,8 +16064,5,204000100,8 +16065,5,206000170,8 +16066,3,170004,3 +16067,3,180004,3 +16068,4,140000,3 +16069,5,150010,3 +16070,5,150020,3 +16071,5,150030,3 +16072,5,150040,3 +16074,3,101000050,1 +16075,3,101000060,1 +16076,3,101000080,1 +16077,3,101000040,1 +16078,3,109000060,1 +16079,3,109000070,1 +16080,3,109000080,1 +16081,3,105000060,1 +16082,3,105000070,1 +16083,3,105000080,1 +16084,3,104000050,1 +16085,3,106000050,1 +16086,3,102000060,1 +16087,3,102000070,1 +16088,3,102000080,1 +16089,3,103000050,1 +16090,3,105000050,1 +16091,3,107000060,1 +16092,3,107000070,1 +16093,3,107000080,1 +16094,3,108000050,1 +16095,3,109000050,1 +16096,3,103000060,1 +16097,3,103000070,1 +16098,3,103000080,1 +16099,3,110000050,1 +16100,3,106000060,1 +16101,3,106000070,1 +16102,3,106000080,1 +16103,3,101000070,1 +16104,3,110000060,1 +16105,3,104000060,1 +16106,3,104000070,1 +16107,3,104000080,1 +16108,3,102000050,1 +16109,3,104000170,1 +16110,3,104000260,1 +16111,3,111000010,1 +16112,3,111000020,1 +16113,3,111000030,1 +16114,3,112000020,1 +16115,3,112000030,1 +16116,3,108000060,1 +16117,3,108000070,1 +16118,3,108000080,1 +16119,3,107000050,1 +16120,3,112000010,1 +16121,3,110000070,1 +16122,3,110000080,1 +16123,3,118000020,1 +16124,3,118000030,1 +16125,3,118000040,1 +16126,4,101000090,1 +16127,4,101000100,1 +16128,4,101000110,1 +16129,4,109000100,1 +16130,4,105000100,1 +16131,4,105000110,1 +16132,4,108000090,1 +16133,4,110000090,1 +16134,4,102000100,1 +16135,4,102000110,1 +16136,4,106000090,1 +16137,4,109000090,1 +16138,4,107000100,1 +16139,4,103000090,1 +16140,4,102000090,1 +16141,4,103000100,1 +16142,4,106000100,1 +16143,4,106000110,1 +16144,4,104000090,1 +16145,4,104000100,1 +16146,4,104000110,1 +16147,4,107000090,1 +16148,4,104000180,1 +16149,4,111000040,1 +16150,4,112000040,1 +16151,4,108000100,1 +16152,4,105000090,1 +16153,4,110000100,1 +16154,4,118000050,1 +16155,4,118000060,1 +16156,5,101000120,1 +16157,5,109000110,1 +16158,5,105000120,1 +16159,5,102000120,1 +16160,5,107000110,1 +16161,5,103000120,1 +16162,5,106000120,1 +16163,5,104000120,1 +16164,5,104000190,1 +16165,5,111000060,1 +16166,5,112000060,1 +16167,5,108000110,1 +16168,5,110000110,1 +16169,5,118000070,1 +16170,1,201000010,8 +16171,1,292000010,8 +16172,1,299000040,8 +16173,1,299000070,8 +16174,1,299000110,8 +16175,1,299000120,8 +16176,1,299000140,8 +16177,2,202000010,8 +16178,2,290000010,8 +16179,2,299000010,8 +16180,2,299000150,8 +16181,2,299000190,8 +16182,2,299000200,8 +16183,2,299000210,8 +16184,3,298000050,8 +16185,3,298000060,8 +16186,3,299000060,8 +16187,3,299000170,8 +16188,3,290000120,8 +16189,3,291000050,8 +16190,3,292000020,8 +16191,4,299000670,8 +16192,4,299000680,8 +16193,4,204000010,8 +16194,4,209000040,8 +16195,4,202000070,8 +16196,4,209000070,8 +16197,4,203000110,8 +16198,4,290000110,8 +16199,4,206000110,8 +16200,4,209000160,8 +16201,5,297000100,8 +16202,5,291000020,8 +16203,5,297000130,8 +16204,5,297000140,8 +16205,5,203000010,8 +16206,5,206000030,8 +16207,5,203000050,8 +16208,5,202000090,8 +16209,5,204000080,8 +16210,5,202000150,8 +16211,5,204000100,8 +16212,5,206000170,8 +16213,3,170004,3 +16214,3,180004,3 +16215,4,140000,3 +16216,5,150010,3 +16217,5,150020,3 +16218,5,150030,3 +16219,5,150040,3 +16220,5,190000,3 +16221,5,200000,3 +16222,5,210000,3 +16224,1,101000010,1 +16225,1,102000010,1 +16226,1,103000010,1 +16227,1,104000010,1 +16228,1,105000010,1 +16229,1,106000010,1 +16230,1,107000010,1 +16231,1,108000010,1 +16232,1,109000010,1 +16233,1,110000010,1 +16234,2,101000020,1 +16235,2,101000030,1 +16236,2,102000020,1 +16237,2,102000030,1 +16238,2,102000040,1 +16239,2,103000020,1 +16240,2,103000030,1 +16241,2,103000040,1 +16242,2,104000020,1 +16243,2,104000030,1 +16244,2,104000040,1 +16245,2,105000020,1 +16246,2,105000030,1 +16247,2,105000040,1 +16248,2,106000020,1 +16249,2,106000030,1 +16250,2,106000040,1 +16251,2,107000020,1 +16252,2,107000030,1 +16253,2,107000040,1 +16254,2,108000020,1 +16255,2,108000030,1 +16256,2,108000040,1 +16257,2,109000020,1 +16258,2,109000030,1 +16259,2,109000040,1 +16260,2,110000020,1 +16261,2,110000030,1 +16262,2,110000040,1 +16263,2,118000010,1 +16264,3,101000050,1 +16265,3,101000060,1 +16266,3,101000080,1 +16267,3,101000040,1 +16268,3,109000060,1 +16269,3,109000070,1 +16270,3,109000080,1 +16271,3,105000060,1 +16272,3,105000070,1 +16273,3,105000080,1 +16274,3,104000050,1 +16275,3,106000050,1 +16276,3,102000060,1 +16277,3,102000070,1 +16278,3,102000080,1 +16279,3,103000050,1 +16280,3,105000050,1 +16281,3,107000060,1 +16282,3,107000070,1 +16283,3,107000080,1 +16284,3,108000050,1 +16285,3,109000050,1 +16286,3,103000060,1 +16287,3,103000070,1 +16288,3,103000080,1 +16289,3,110000050,1 +16290,3,106000060,1 +16291,3,106000070,1 +16292,3,106000080,1 +16293,3,101000070,1 +16294,3,110000060,1 +16295,3,104000060,1 +16296,3,104000070,1 +16297,3,104000080,1 +16298,3,102000050,1 +16299,3,104000170,1 +16300,3,104000260,1 +16301,3,111000010,1 +16302,3,111000020,1 +16303,3,111000030,1 +16304,3,112000020,1 +16305,3,112000030,1 +16306,3,108000060,1 +16307,3,108000070,1 +16308,3,108000080,1 +16309,3,107000050,1 +16310,3,112000010,1 +16311,3,110000070,1 +16312,3,110000080,1 +16313,3,118000020,1 +16314,3,118000030,1 +16315,3,118000040,1 +16316,4,101000090,1 +16317,4,101000100,1 +16318,4,101000110,1 +16319,4,109000100,1 +16320,4,105000100,1 +16321,4,105000110,1 +16322,4,108000090,1 +16323,4,110000090,1 +16324,4,102000100,1 +16325,4,102000110,1 +16326,4,106000090,1 +16327,4,109000090,1 +16328,4,107000100,1 +16329,4,103000090,1 +16330,4,102000090,1 +16331,4,103000100,1 +16332,4,106000100,1 +16333,4,106000110,1 +16334,4,104000090,1 +16335,4,104000100,1 +16336,4,104000110,1 +16337,4,107000090,1 +16338,4,104000180,1 +16339,4,111000040,1 +16340,4,112000040,1 +16341,4,108000100,1 +16342,4,105000090,1 +16343,4,110000100,1 +16344,4,118000050,1 +16345,4,118000060,1 +16346,5,101000120,1 +16347,5,109000110,1 +16348,5,105000120,1 +16349,5,102000120,1 +16350,5,107000110,1 +16351,5,103000120,1 +16352,5,106000120,1 +16353,5,104000120,1 +16354,5,104000190,1 +16355,5,111000060,1 +16356,5,112000060,1 +16357,5,108000110,1 +16358,5,110000110,1 +16359,5,118000070,1 +16360,1,201000010,8 +16361,1,292000010,8 +16362,1,299000040,8 +16363,1,299000070,8 +16364,1,299000110,8 +16365,1,299000120,8 +16366,1,299000140,8 +16367,2,202000010,8 +16368,2,290000010,8 +16369,2,299000010,8 +16370,2,299000150,8 +16371,2,299000190,8 +16372,2,299000200,8 +16373,2,299000210,8 +16374,3,298000050,8 +16375,3,298000060,8 +16376,3,299000060,8 +16377,3,299000170,8 +16378,3,290000120,8 +16379,3,291000050,8 +16380,3,292000020,8 +16381,4,299000670,8 +16382,4,299000680,8 +16383,4,204000010,8 +16384,4,209000040,8 +16385,4,202000070,8 +16386,4,209000070,8 +16387,4,203000110,8 +16388,4,290000110,8 +16389,4,206000110,8 +16390,4,209000160,8 +16391,4,209000190,8 +16392,5,297000100,8 +16393,5,291000020,8 +16394,5,297000130,8 +16395,5,297000140,8 +16396,5,203000010,8 +16397,5,206000030,8 +16398,5,203000050,8 +16399,5,202000090,8 +16400,5,204000080,8 +16401,5,202000150,8 +16402,5,204000100,8 +16403,5,206000170,8 +16404,5,204000200,8 +16405,1,170002,3 +16406,1,180002,3 +16407,2,170003,3 +16408,2,180003,3 +16409,3,170004,3 +16410,3,180004,3 +16411,4,140000,3 +16412,5,150010,3 +16413,5,150020,3 +16414,5,150030,3 +16415,5,150040,3 +16417,1,101000010,1 +16418,1,102000010,1 +16419,1,103000010,1 +16420,1,104000010,1 +16421,1,105000010,1 +16422,1,106000010,1 +16423,1,107000010,1 +16424,1,108000010,1 +16425,1,109000010,1 +16426,1,110000010,1 +16427,2,101000020,1 +16428,2,101000030,1 +16429,2,102000020,1 +16430,2,102000030,1 +16431,2,102000040,1 +16432,2,103000020,1 +16433,2,103000030,1 +16434,2,103000040,1 +16435,2,104000020,1 +16436,2,104000030,1 +16437,2,104000040,1 +16438,2,105000020,1 +16439,2,105000030,1 +16440,2,105000040,1 +16441,2,106000020,1 +16442,2,106000030,1 +16443,2,106000040,1 +16444,2,107000020,1 +16445,2,107000030,1 +16446,2,107000040,1 +16447,2,108000020,1 +16448,2,108000030,1 +16449,2,108000040,1 +16450,2,109000020,1 +16451,2,109000030,1 +16452,2,109000040,1 +16453,2,110000020,1 +16454,2,110000030,1 +16455,2,110000040,1 +16456,2,118000010,1 +16457,3,101000050,1 +16458,3,101000060,1 +16459,3,101000080,1 +16460,3,101000040,1 +16461,3,109000060,1 +16462,3,109000070,1 +16463,3,109000080,1 +16464,3,105000060,1 +16465,3,105000070,1 +16466,3,105000080,1 +16467,3,104000050,1 +16468,3,106000050,1 +16469,3,102000060,1 +16470,3,102000070,1 +16471,3,102000080,1 +16472,3,103000050,1 +16473,3,105000050,1 +16474,3,107000060,1 +16475,3,107000070,1 +16476,3,107000080,1 +16477,3,108000050,1 +16478,3,109000050,1 +16479,3,103000060,1 +16480,3,103000070,1 +16481,3,103000080,1 +16482,3,110000050,1 +16483,3,106000060,1 +16484,3,106000070,1 +16485,3,106000080,1 +16486,3,101000070,1 +16487,3,110000060,1 +16488,3,104000060,1 +16489,3,104000070,1 +16490,3,104000080,1 +16491,3,102000050,1 +16492,3,104000170,1 +16493,3,104000260,1 +16494,3,111000010,1 +16495,3,111000020,1 +16496,3,111000030,1 +16497,3,112000020,1 +16498,3,112000030,1 +16499,3,108000060,1 +16500,3,108000070,1 +16501,3,108000080,1 +16502,3,107000050,1 +16503,3,112000010,1 +16504,3,110000070,1 +16505,3,110000080,1 +16506,3,118000020,1 +16507,3,118000030,1 +16508,3,118000040,1 +16509,4,101000090,1 +16510,4,101000100,1 +16511,4,101000110,1 +16512,4,109000100,1 +16513,4,105000100,1 +16514,4,105000110,1 +16515,4,108000090,1 +16516,4,110000090,1 +16517,4,102000100,1 +16518,4,102000110,1 +16519,4,106000090,1 +16520,4,109000090,1 +16521,4,107000100,1 +16522,4,103000090,1 +16523,4,102000090,1 +16524,4,103000100,1 +16525,4,106000100,1 +16526,4,106000110,1 +16527,4,104000090,1 +16528,4,104000100,1 +16529,4,104000110,1 +16530,4,107000090,1 +16531,4,104000180,1 +16532,4,111000040,1 +16533,4,112000040,1 +16534,4,108000100,1 +16535,4,105000090,1 +16536,4,110000100,1 +16537,4,118000050,1 +16538,4,118000060,1 +16539,5,101000120,1 +16540,5,109000110,1 +16541,5,105000120,1 +16542,5,102000120,1 +16543,5,107000110,1 +16544,5,103000120,1 +16545,5,106000120,1 +16546,5,104000120,1 +16547,5,104000190,1 +16548,5,111000060,1 +16549,5,112000060,1 +16550,5,108000110,1 +16551,5,110000110,1 +16552,5,118000070,1 +16553,1,201000010,8 +16554,1,292000010,8 +16555,1,299000040,8 +16556,1,299000070,8 +16557,1,299000110,8 +16558,1,299000120,8 +16559,1,299000140,8 +16560,2,202000010,8 +16561,2,290000010,8 +16562,2,299000010,8 +16563,2,299000150,8 +16564,2,299000190,8 +16565,2,299000200,8 +16566,2,299000210,8 +16567,3,298000050,8 +16568,3,298000060,8 +16569,3,299000060,8 +16570,3,299000170,8 +16571,3,290000120,8 +16572,3,291000050,8 +16573,3,292000020,8 +16574,4,299000670,8 +16575,4,299000680,8 +16576,4,204000010,8 +16577,4,209000040,8 +16578,4,202000070,8 +16579,4,209000070,8 +16580,4,203000110,8 +16581,4,290000110,8 +16582,4,206000110,8 +16583,4,209000160,8 +16584,4,209000190,8 +16585,5,297000100,8 +16586,5,291000020,8 +16587,5,297000130,8 +16588,5,297000140,8 +16589,5,203000010,8 +16590,5,206000030,8 +16591,5,203000050,8 +16592,5,202000090,8 +16593,5,204000080,8 +16594,5,202000150,8 +16595,5,204000100,8 +16596,5,206000170,8 +16597,5,204000200,8 +16598,2,170003,3 +16599,2,180003,3 +16600,3,170004,3 +16601,3,180004,3 +16602,4,140000,3 +16603,5,150010,3 +16604,5,150020,3 +16605,5,150030,3 +16606,5,150040,3 +16608,3,101000050,1 +16609,3,101000060,1 +16610,3,101000080,1 +16611,3,101000040,1 +16612,3,109000060,1 +16613,3,109000070,1 +16614,3,109000080,1 +16615,3,105000060,1 +16616,3,105000070,1 +16617,3,105000080,1 +16618,3,104000050,1 +16619,3,106000050,1 +16620,3,102000060,1 +16621,3,102000070,1 +16622,3,102000080,1 +16623,3,103000050,1 +16624,3,105000050,1 +16625,3,107000060,1 +16626,3,107000070,1 +16627,3,107000080,1 +16628,3,108000050,1 +16629,3,109000050,1 +16630,3,103000060,1 +16631,3,103000070,1 +16632,3,103000080,1 +16633,3,110000050,1 +16634,3,106000060,1 +16635,3,106000070,1 +16636,3,106000080,1 +16637,3,101000070,1 +16638,3,110000060,1 +16639,3,104000060,1 +16640,3,104000070,1 +16641,3,104000080,1 +16642,3,102000050,1 +16643,3,104000170,1 +16644,3,104000260,1 +16645,3,111000010,1 +16646,3,111000020,1 +16647,3,111000030,1 +16648,3,112000020,1 +16649,3,112000030,1 +16650,3,108000060,1 +16651,3,108000070,1 +16652,3,108000080,1 +16653,3,107000050,1 +16654,3,112000010,1 +16655,3,110000070,1 +16656,3,110000080,1 +16657,3,118000020,1 +16658,3,118000030,1 +16659,3,118000040,1 +16660,4,101000090,1 +16661,4,101000100,1 +16662,4,101000110,1 +16663,4,109000100,1 +16664,4,105000100,1 +16665,4,105000110,1 +16666,4,108000090,1 +16667,4,110000090,1 +16668,4,102000100,1 +16669,4,102000110,1 +16670,4,106000090,1 +16671,4,109000090,1 +16672,4,107000100,1 +16673,4,103000090,1 +16674,4,102000090,1 +16675,4,103000100,1 +16676,4,106000100,1 +16677,4,106000110,1 +16678,4,104000090,1 +16679,4,104000100,1 +16680,4,104000110,1 +16681,4,107000090,1 +16682,4,104000180,1 +16683,4,111000040,1 +16684,4,112000040,1 +16685,4,108000100,1 +16686,4,105000090,1 +16687,4,110000100,1 +16688,4,118000050,1 +16689,4,118000060,1 +16690,5,101000120,1 +16691,5,109000110,1 +16692,5,105000120,1 +16693,5,102000120,1 +16694,5,107000110,1 +16695,5,103000120,1 +16696,5,106000120,1 +16697,5,104000120,1 +16698,5,104000190,1 +16699,5,111000060,1 +16700,5,112000060,1 +16701,5,108000110,1 +16702,5,110000110,1 +16703,5,118000070,1 +16704,1,201000010,8 +16705,1,292000010,8 +16706,1,299000040,8 +16707,1,299000070,8 +16708,1,299000110,8 +16709,1,299000120,8 +16710,1,299000140,8 +16711,2,202000010,8 +16712,2,290000010,8 +16713,2,299000010,8 +16714,2,299000150,8 +16715,2,299000190,8 +16716,2,299000200,8 +16717,2,299000210,8 +16718,3,298000050,8 +16719,3,298000060,8 +16720,3,299000060,8 +16721,3,299000170,8 +16722,3,290000120,8 +16723,3,291000050,8 +16724,3,292000020,8 +16725,4,299000670,8 +16726,4,299000680,8 +16727,4,204000010,8 +16728,4,209000040,8 +16729,4,202000070,8 +16730,4,209000070,8 +16731,4,203000110,8 +16732,4,290000110,8 +16733,4,206000110,8 +16734,4,209000160,8 +16735,4,209000190,8 +16736,5,297000100,8 +16737,5,291000020,8 +16738,5,297000130,8 +16739,5,297000140,8 +16740,5,203000010,8 +16741,5,206000030,8 +16742,5,203000050,8 +16743,5,202000090,8 +16744,5,204000080,8 +16745,5,202000150,8 +16746,5,204000100,8 +16747,5,206000170,8 +16748,5,204000200,8 +16749,3,170004,3 +16750,3,180004,3 +16751,4,140000,3 +16752,5,150010,3 +16753,5,150020,3 +16754,5,150030,3 +16755,5,150040,3 +16757,3,101000050,1 +16758,3,101000060,1 +16759,3,101000080,1 +16760,3,101000040,1 +16761,3,109000060,1 +16762,3,109000070,1 +16763,3,109000080,1 +16764,3,105000060,1 +16765,3,105000070,1 +16766,3,105000080,1 +16767,3,104000050,1 +16768,3,106000050,1 +16769,3,102000060,1 +16770,3,102000070,1 +16771,3,102000080,1 +16772,3,103000050,1 +16773,3,105000050,1 +16774,3,107000060,1 +16775,3,107000070,1 +16776,3,107000080,1 +16777,3,108000050,1 +16778,3,109000050,1 +16779,3,103000060,1 +16780,3,103000070,1 +16781,3,103000080,1 +16782,3,110000050,1 +16783,3,106000060,1 +16784,3,106000070,1 +16785,3,106000080,1 +16786,3,101000070,1 +16787,3,110000060,1 +16788,3,104000060,1 +16789,3,104000070,1 +16790,3,104000080,1 +16791,3,102000050,1 +16792,3,104000170,1 +16793,3,104000260,1 +16794,3,111000010,1 +16795,3,111000020,1 +16796,3,111000030,1 +16797,3,112000020,1 +16798,3,112000030,1 +16799,3,108000060,1 +16800,3,108000070,1 +16801,3,108000080,1 +16802,3,107000050,1 +16803,3,112000010,1 +16804,3,110000070,1 +16805,3,110000080,1 +16806,3,118000020,1 +16807,3,118000030,1 +16808,3,118000040,1 +16809,4,101000090,1 +16810,4,101000100,1 +16811,4,101000110,1 +16812,4,109000100,1 +16813,4,105000100,1 +16814,4,105000110,1 +16815,4,108000090,1 +16816,4,110000090,1 +16817,4,102000100,1 +16818,4,102000110,1 +16819,4,106000090,1 +16820,4,109000090,1 +16821,4,107000100,1 +16822,4,103000090,1 +16823,4,102000090,1 +16824,4,103000100,1 +16825,4,106000100,1 +16826,4,106000110,1 +16827,4,104000090,1 +16828,4,104000100,1 +16829,4,104000110,1 +16830,4,107000090,1 +16831,4,104000180,1 +16832,4,111000040,1 +16833,4,112000040,1 +16834,4,108000100,1 +16835,4,105000090,1 +16836,4,110000100,1 +16837,4,118000050,1 +16838,4,118000060,1 +16839,5,101000120,1 +16840,5,109000110,1 +16841,5,105000120,1 +16842,5,102000120,1 +16843,5,107000110,1 +16844,5,103000120,1 +16845,5,106000120,1 +16846,5,104000120,1 +16847,5,104000190,1 +16848,5,111000060,1 +16849,5,112000060,1 +16850,5,108000110,1 +16851,5,110000110,1 +16852,5,118000070,1 +16853,1,201000010,8 +16854,1,292000010,8 +16855,1,299000040,8 +16856,1,299000070,8 +16857,1,299000110,8 +16858,1,299000120,8 +16859,1,299000140,8 +16860,2,202000010,8 +16861,2,290000010,8 +16862,2,299000010,8 +16863,2,299000150,8 +16864,2,299000190,8 +16865,2,299000200,8 +16866,2,299000210,8 +16867,3,298000050,8 +16868,3,298000060,8 +16869,3,299000060,8 +16870,3,299000170,8 +16871,3,290000120,8 +16872,3,291000050,8 +16873,3,292000020,8 +16874,4,299000670,8 +16875,4,299000680,8 +16876,4,204000010,8 +16877,4,209000040,8 +16878,4,202000070,8 +16879,4,209000070,8 +16880,4,203000110,8 +16881,4,290000110,8 +16882,4,206000110,8 +16883,4,209000160,8 +16884,4,209000190,8 +16885,5,297000100,8 +16886,5,291000020,8 +16887,5,297000130,8 +16888,5,297000140,8 +16889,5,203000010,8 +16890,5,206000030,8 +16891,5,203000050,8 +16892,5,202000090,8 +16893,5,204000080,8 +16894,5,202000150,8 +16895,5,204000100,8 +16896,5,206000170,8 +16897,5,204000200,8 +16898,3,170004,3 +16899,3,180004,3 +16900,4,140000,3 +16901,5,150010,3 +16902,5,150020,3 +16903,5,150030,3 +16904,5,150040,3 +16906,3,101000050,1 +16907,3,101000060,1 +16908,3,101000080,1 +16909,3,101000040,1 +16910,3,109000060,1 +16911,3,109000070,1 +16912,3,109000080,1 +16913,3,105000060,1 +16914,3,105000070,1 +16915,3,105000080,1 +16916,3,104000050,1 +16917,3,106000050,1 +16918,3,102000060,1 +16919,3,102000070,1 +16920,3,102000080,1 +16921,3,103000050,1 +16922,3,105000050,1 +16923,3,107000060,1 +16924,3,107000070,1 +16925,3,107000080,1 +16926,3,108000050,1 +16927,3,109000050,1 +16928,3,103000060,1 +16929,3,103000070,1 +16930,3,103000080,1 +16931,3,110000050,1 +16932,3,106000060,1 +16933,3,106000070,1 +16934,3,106000080,1 +16935,3,101000070,1 +16936,3,110000060,1 +16937,3,104000060,1 +16938,3,104000070,1 +16939,3,104000080,1 +16940,3,102000050,1 +16941,3,104000170,1 +16942,3,104000260,1 +16943,3,111000010,1 +16944,3,111000020,1 +16945,3,111000030,1 +16946,3,112000020,1 +16947,3,112000030,1 +16948,3,108000060,1 +16949,3,108000070,1 +16950,3,108000080,1 +16951,3,107000050,1 +16952,3,112000010,1 +16953,3,110000070,1 +16954,3,110000080,1 +16955,3,118000020,1 +16956,3,118000030,1 +16957,3,118000040,1 +16958,4,101000090,1 +16959,4,101000100,1 +16960,4,101000110,1 +16961,4,109000100,1 +16962,4,105000100,1 +16963,4,105000110,1 +16964,4,108000090,1 +16965,4,110000090,1 +16966,4,102000100,1 +16967,4,102000110,1 +16968,4,106000090,1 +16969,4,109000090,1 +16970,4,107000100,1 +16971,4,103000090,1 +16972,4,102000090,1 +16973,4,103000100,1 +16974,4,106000100,1 +16975,4,106000110,1 +16976,4,104000090,1 +16977,4,104000100,1 +16978,4,104000110,1 +16979,4,107000090,1 +16980,4,104000180,1 +16981,4,111000040,1 +16982,4,112000040,1 +16983,4,108000100,1 +16984,4,105000090,1 +16985,4,110000100,1 +16986,4,118000050,1 +16987,4,118000060,1 +16988,5,101000120,1 +16989,5,109000110,1 +16990,5,105000120,1 +16991,5,102000120,1 +16992,5,107000110,1 +16993,5,103000120,1 +16994,5,106000120,1 +16995,5,104000120,1 +16996,5,104000190,1 +16997,5,111000060,1 +16998,5,112000060,1 +16999,5,108000110,1 +17000,5,110000110,1 +17001,5,118000070,1 +17002,1,201000010,8 +17003,1,292000010,8 +17004,1,299000040,8 +17005,1,299000070,8 +17006,1,299000110,8 +17007,1,299000120,8 +17008,1,299000140,8 +17009,2,202000010,8 +17010,2,290000010,8 +17011,2,299000010,8 +17012,2,299000150,8 +17013,2,299000190,8 +17014,2,299000200,8 +17015,2,299000210,8 +17016,3,298000050,8 +17017,3,298000060,8 +17018,3,299000060,8 +17019,3,299000170,8 +17020,3,290000120,8 +17021,3,291000050,8 +17022,3,292000020,8 +17023,4,299000670,8 +17024,4,299000680,8 +17025,4,204000010,8 +17026,4,209000040,8 +17027,4,202000070,8 +17028,4,209000070,8 +17029,4,203000110,8 +17030,4,290000110,8 +17031,4,206000110,8 +17032,4,209000160,8 +17033,4,209000190,8 +17034,5,297000100,8 +17035,5,291000020,8 +17036,5,297000130,8 +17037,5,297000140,8 +17038,5,203000010,8 +17039,5,206000030,8 +17040,5,203000050,8 +17041,5,202000090,8 +17042,5,204000080,8 +17043,5,202000150,8 +17044,5,204000100,8 +17045,5,206000170,8 +17046,5,204000200,8 +17047,3,170004,3 +17048,3,180004,3 +17049,4,140000,3 +17050,5,150010,3 +17051,5,150020,3 +17052,5,150030,3 +17053,5,150040,3 +17054,5,190000,3 +17055,5,200000,3 +17057,3,118000020,1 +17058,3,118000030,1 +17059,3,118000040,1 +17060,3,106000060,1 +17061,3,106000070,1 +17062,3,106000080,1 +17063,3,101000070,1 +17064,3,110000060,1 +17065,3,104000060,1 +17066,3,104000070,1 +17067,3,104000080,1 +17068,3,102000050,1 +17069,3,104000170,1 +17070,3,104000260,1 +17071,4,118000050,1 +17072,4,118000060,1 +17073,4,106000100,1 +17074,4,106000110,1 +17075,4,104000090,1 +17076,4,104000100,1 +17077,4,104000110,1 +17078,4,104000270,1 +17079,4,107000090,1 +17080,4,104000180,1 +17081,5,118000070,1 +17082,5,106000120,1 +17083,5,104000120,1 +17084,5,104000190,1 +17085,1,103000000,2 +17086,2,103000001,2 +17087,3,103000002,2 +17088,4,103000003,2 +17089,5,103000004,2 +17090,1,111000000,2 +17091,2,111000001,2 +17092,3,111000002,2 +17093,4,111000003,2 +17094,5,111000004,2 +17095,1,115000000,2 +17096,2,115000001,2 +17097,3,115000002,2 +17098,4,115000003,2 +17099,5,115000004,2 +17100,3,170004,3 +17101,4,170005,3 +17102,3,180004,3 +17103,4,180005,3 +17104,4,140000,3 +17105,4,150010,3 +17106,4,150020,3 +17107,4,150030,3 +17108,4,150040,3 +17110,3,111000010,1 +17111,3,111000020,1 +17112,3,111000030,1 +17113,3,112000020,1 +17114,3,112000030,1 +17115,3,108000060,1 +17116,3,108000070,1 +17117,3,108000080,1 +17118,3,107000050,1 +17119,3,112000010,1 +17120,3,110000070,1 +17121,3,110000080,1 +17122,4,111000040,1 +17123,4,112000040,1 +17124,4,108000100,1 +17125,4,105000090,1 +17126,4,110000100,1 +17127,5,111000060,1 +17128,5,112000060,1 +17129,5,108000110,1 +17130,5,110000110,1 +17131,1,108000000,2 +17132,2,108000001,2 +17133,3,108000002,2 +17134,4,108000003,2 +17135,5,108000004,2 +17136,1,107000000,2 +17137,2,107000001,2 +17138,3,107000002,2 +17139,4,107000003,2 +17140,5,107000004,2 +17141,1,120000000,2 +17142,2,120000001,2 +17143,3,120000002,2 +17144,4,120000003,2 +17145,5,120000004,2 +17146,4,110024,3 +17147,4,110034,3 +17148,4,110044,3 +17149,4,110054,3 +17150,3,110060,3 +17151,3,110070,3 +17152,3,110080,3 +17153,3,110090,3 +17154,3,110100,3 +17155,3,110110,3 +17156,3,110120,3 +17157,3,110130,3 +17158,3,110140,3 +17159,3,110150,3 +17160,3,110160,3 +17161,3,110170,3 +17162,3,110180,3 +17163,3,110190,3 +17164,3,110200,3 +17165,3,110210,3 +17166,3,110220,3 +17167,3,110230,3 +17168,3,110240,3 +17169,3,110250,3 +17170,3,110260,3 +17171,3,110270,3 +17172,3,110620,3 +17173,3,110670,3 +17174,4,140000,3 +17175,4,150010,3 +17176,4,150020,3 +17177,4,150030,3 +17178,4,150040,3 +17180,3,101000050,1 +17181,3,101000060,1 +17182,3,101000080,1 +17183,3,101000040,1 +17184,3,109000060,1 +17185,3,109000070,1 +17186,3,109000080,1 +17187,3,105000060,1 +17188,3,105000070,1 +17189,3,105000080,1 +17190,3,104000050,1 +17191,3,106000050,1 +17192,4,101000090,1 +17193,4,101000100,1 +17194,4,101000110,1 +17195,4,109000100,1 +17196,4,105000100,1 +17197,4,105000110,1 +17198,4,108000090,1 +17199,4,110000090,1 +17200,5,101000120,1 +17201,5,109000110,1 +17202,5,105000120,1 +17203,1,101000000,2 +17204,2,101000001,2 +17205,3,101000002,2 +17206,4,101000008,2 +17207,5,101000004,2 +17208,1,109000000,2 +17209,2,109000001,2 +17210,3,109000002,2 +17211,4,109000003,2 +17212,5,109000004,2 +17213,4,120024,3 +17214,4,120034,3 +17215,4,120044,3 +17216,4,120054,3 +17217,3,120241,3 +17218,3,120251,3 +17219,3,120261,3 +17220,3,120271,3 +17221,3,120300,3 +17222,3,120310,3 +17223,3,120320,3 +17224,3,120330,3 +17225,3,120340,3 +17226,3,120350,3 +17227,3,120360,3 +17228,3,120370,3 +17229,3,120380,3 +17230,3,120390,3 +17231,3,120400,3 +17232,3,120410,3 +17233,3,120420,3 +17234,3,120430,3 +17235,3,120450,3 +17236,3,120460,3 +17237,3,120550,3 +17238,3,120560,3 +17239,3,120570,3 +17240,3,120990,3 +17241,3,121000,3 +17242,3,121010,3 +17243,3,121020,3 +17244,3,121100,3 +17245,4,140000,3 +17246,4,150010,3 +17247,4,150020,3 +17248,4,150030,3 +17249,4,150040,3 +17251,3,102000060,1 +17252,3,102000070,1 +17253,3,102000080,1 +17254,3,103000050,1 +17255,3,105000050,1 +17256,3,107000060,1 +17257,3,107000070,1 +17258,3,107000080,1 +17259,3,108000050,1 +17260,3,109000050,1 +17261,3,103000060,1 +17262,3,103000070,1 +17263,3,103000080,1 +17264,3,110000050,1 +17265,4,102000100,1 +17266,4,102000110,1 +17267,4,102000350,1 +17268,4,106000090,1 +17269,4,109000090,1 +17270,4,107000100,1 +17271,4,103000090,1 +17272,4,102000090,1 +17273,4,103000100,1 +17274,5,102000120,1 +17275,5,107000110,1 +17276,5,103000120,1 +17277,1,102000000,2 +17278,2,102000001,2 +17279,3,102000002,2 +17280,4,102000003,2 +17281,5,102000004,2 +17282,1,105000000,2 +17283,2,105000001,2 +17284,3,105000002,2 +17285,4,105000003,2 +17286,5,105000004,2 +17287,1,112000000,2 +17288,2,112000001,2 +17289,3,112000002,2 +17290,4,112000003,2 +17291,5,112000004,2 +17292,4,130024,3 +17293,4,130034,3 +17294,4,130044,3 +17295,4,130054,3 +17296,3,130060,3 +17297,3,130070,3 +17298,3,130080,3 +17299,3,130090,3 +17300,3,130100,3 +17301,3,130110,3 +17302,3,130120,3 +17303,3,130130,3 +17304,3,130140,3 +17305,3,130150,3 +17306,3,130160,3 +17307,3,130170,3 +17308,3,130180,3 +17309,3,130190,3 +17310,3,130200,3 +17311,3,130420,3 +17312,3,130510,3 +17313,3,130520,3 +17314,3,130531,3 +17315,3,130540,3 +17316,3,130660,3 +17317,3,130700,3 +17318,3,130790,3 +17319,3,130800,3 +17320,3,131130,3 +17321,4,140000,3 +17322,4,150010,3 +17323,4,150020,3 +17324,4,150030,3 +17325,4,150040,3 +17327,4,101000250,1 +17328,4,102000260,1 +17329,4,103000220,1 +17330,4,104000250,1 +17331,4,105000210,1 +17332,4,106000210,1 +17333,4,107000140,1 +17334,4,108000150,1 +17335,4,109000230,1 +17336,4,110000170,1 +17337,4,111000140,1 +17338,4,112000110,1 +17339,4,118000140,1 +17340,5,101000300,1 +17341,5,102000360,1 +17342,5,103000310,1 +17343,5,104000370,1 +17344,5,109000290,1 +17345,5,101000310,1 +17346,5,102000410,1 +17347,1,201000010,8 +17348,1,292000010,8 +17349,1,299000040,8 +17350,1,299000070,8 +17351,1,299000110,8 +17352,1,299000120,8 +17353,1,299000140,8 +17354,2,202000010,8 +17355,2,290000010,8 +17356,2,299000010,8 +17357,2,299000150,8 +17358,2,299000190,8 +17359,2,299000200,8 +17360,2,299000210,8 +17361,3,290000120,8 +17362,3,291000050,8 +17363,3,292000020,8 +17364,3,298000050,8 +17365,3,298000060,8 +17366,3,299000060,8 +17367,3,299000170,8 +17368,4,201000170,8 +17369,4,202000070,8 +17370,4,202000370,8 +17371,4,202000400,8 +17372,4,202000440,8 +17373,4,203000110,8 +17374,4,203000270,8 +17375,4,203000350,8 +17376,4,204000010,8 +17377,4,204000370,8 +17378,4,205000220,8 +17379,4,206000110,8 +17380,4,206000240,8 +17381,4,206000270,8 +17382,4,209000040,8 +17383,4,209000070,8 +17384,4,209000160,8 +17385,4,209000190,8 +17386,4,209000240,8 +17387,4,215000150,8 +17388,4,290000110,8 +17389,4,290000130,8 +17390,4,298000120,8 +17391,4,299000670,8 +17392,4,299000680,8 +17393,4,201000090,8 +17394,4,202000160,8 +17395,4,203000150,8 +17396,4,204000110,8 +17397,4,205000110,8 +17398,4,206000120,8 +17399,4,211000060,8 +17400,4,212000050,8 +17401,4,299000440,8 +17402,4,299000450,8 +17403,4,299000460,8 +17404,4,299000470,8 +17405,4,299000480,8 +17406,4,299000550,8 +17407,4,299000560,8 +17408,4,299000570,8 +17409,4,299000580,8 +17410,4,299000590,8 +17411,4,299000600,8 +17412,4,299000610,8 +17413,4,299000620,8 +17414,4,299000630,8 +17415,4,299000640,8 +17416,4,299000650,8 +17417,4,299000230,8 +17418,4,299000240,8 +17419,4,299000250,8 +17420,4,299000260,8 +17421,4,299000270,8 +17422,4,299000280,8 +17423,4,299000290,8 +17424,4,299000300,8 +17425,4,299000310,8 +17426,4,299000320,8 +17427,4,299000330,8 +17428,4,299000340,8 +17429,4,299000350,8 +17430,4,299000360,8 +17431,4,299000370,8 +17432,4,299000380,8 +17433,4,299000390,8 +17434,4,299000400,8 +17435,4,215000050,8 +17436,4,216000060,8 +17437,4,217000020,8 +17438,4,218000030,8 +17439,4,299000500,8 +17440,4,299000510,8 +17441,4,299000520,8 +17442,4,299000530,8 +17443,4,299000540,8 +17444,4,299000660,8 +17445,5,202000090,8 +17446,5,202000150,8 +17447,5,203000010,8 +17448,5,203000050,8 +17449,5,203000260,8 +17450,5,204000080,8 +17451,5,204000100,8 +17452,5,204000200,8 +17453,5,204000270,8 +17454,5,206000030,8 +17455,5,206000170,8 +17456,5,209000320,8 +17457,5,220000080,8 +17458,5,290000160,8 +17459,5,291000020,8 +17460,5,297000100,8 +17461,5,297000130,8 +17462,5,297000140,8 +17463,5,298000110,8 +17464,5,202000290,8 +17465,5,203000240,8 +17466,5,204000210,8 +17467,5,205000150,8 +17468,5,206000200,8 +17469,5,211000090,8 +17470,5,212000060,8 +17471,5,299000800,8 +17472,5,299000810,8 +17473,5,299000820,8 +17474,5,299000840,8 +17475,5,299000850,8 +17476,5,299000860,8 +17477,5,299000870,8 +17478,5,299000880,8 +17479,5,299000890,8 +17480,5,201000120,8 +17481,5,202000240,8 +17482,5,211000080,8 +17483,5,299000730,8 +17484,5,299000740,8 +17485,5,299000750,8 +17486,5,299000760,8 +17487,5,299000770,8 +17488,5,201000130,8 +17489,5,299000830,8 +17490,5,202000280,8 +17491,5,204000220,8 +17492,5,201000150,8 +17493,5,202000350,8 +17494,5,205000200,8 +17495,5,299001050,8 +17496,5,299001060,8 +17497,5,201000200,8 +17498,5,202000430,8 +17499,5,211000150,8 +17500,5,299001170,8 +17501,5,299001180,8 +17502,5,202000270,8 +17503,5,206000190,8 +17504,5,220000060,8 +17505,5,201000140,8 +17506,5,203000230,8 +17507,5,205000160,8 +17508,5,204000230,8 +17509,5,209000200,8 +17510,5,299000900,8 +17511,5,202000340,8 +17512,5,203000280,8 +17513,5,205000190,8 +17514,5,208000040,8 +17515,5,211000110,8 +17516,5,220000070,8 +17517,5,202000420,8 +17518,5,203000340,8 +17519,5,204000360,8 +17520,5,211000140,8 +17521,5,212000090,8 +17522,5,299000920,8 +17523,5,299000930,8 +17524,5,299000940,8 +17525,5,299000950,8 +17526,5,299000960,8 +17527,5,298000130,8 +17528,5,202000380,8 +17529,5,203000300,8 +17530,5,204000320,8 +17531,5,205000230,8 +17532,5,206000250,8 +17533,5,209000280,8 +17534,5,210000030,8 +17535,5,211000120,8 +17536,5,212000070,8 +17537,5,215000130,8 +17538,5,216000130,8 +17539,5,217000080,8 +17540,5,218000090,8 +17541,5,299001070,8 +17542,5,299001080,8 +17543,5,299001090,8 +17544,5,215000120,8 +17545,5,202000390,8 +17546,5,203000310,8 +17547,5,204000330,8 +17548,5,205000240,8 +17549,5,206000260,8 +17550,5,209000290,8 +17551,5,210000040,8 +17552,5,211000130,8 +17553,5,212000080,8 +17554,5,215000140,8 +17555,5,216000140,8 +17556,5,217000090,8 +17557,5,218000100,8 +17558,5,220000090,8 +17559,5,299001100,8 +17560,5,299001110,8 +17561,5,299001120,8 +17562,5,299001130,8 +17563,5,299001140,8 +17564,5,299001150,8 +17565,5,299000430,8 +17566,5,299000690,8 +17567,5,299000710,8 +17568,5,299000720,8 +17569,5,215000100,8 +17570,5,216000090,8 +17571,5,217000070,8 +17572,5,218000080,8 +17573,5,299000970,8 +17574,5,299000980,8 +17575,5,299000990,8 +17576,5,299001000,8 +17577,5,299001010,8 +17578,5,299001020,8 +17579,5,299001030,8 +17580,5,299001040,8 +17581,3,101000006,2 +17582,3,103000005,2 +17583,4,101000003,2 +17584,4,102000005,2 +17585,4,107000005,2 +17586,4,112000006,2 +17587,5,101000009,2 +17588,5,101000011,2 +17589,5,101000012,2 +17590,5,101000013,2 +17591,5,101000014,2 +17592,5,101000015,2 +17593,5,101000016,2 +17594,5,101000017,2 +17595,5,101000018,2 +17596,5,102000008,2 +17597,5,102000009,2 +17598,5,107000007,2 +17599,5,109000008,2 +17600,5,111000007,2 +17601,5,111000008,2 +17602,5,112000008,2 +17603,5,120000008,2 +17605,2,118000010,1 +17606,3,118000020,1 +17607,3,118000030,1 +17608,3,118000040,1 +17609,4,118000050,1 +17610,4,118000060,1 +17611,5,118000070,1 +17612,1,101000000,2 +17613,3,101000006,2 +17614,3,103000005,2 +17615,4,101000003,2 +17616,4,102000005,2 +17617,4,107000005,2 +17618,4,112000006,2 +17619,5,101000009,2 +17620,5,101000011,2 +17621,5,101000012,2 +17622,5,101000013,2 +17623,5,101000014,2 +17624,5,101000015,2 +17625,5,101000016,2 +17626,5,101000017,2 +17627,5,101000018,2 +17628,5,102000008,2 +17629,5,102000009,2 +17630,5,107000007,2 +17631,5,109000008,2 +17632,5,111000007,2 +17633,5,111000008,2 +17634,5,112000008,2 +17635,5,120000008,2 +17636,3,110540,3 +17637,3,110680,3 +17638,3,110790,3 +17639,3,110800,3 +17640,3,120440,3 +17641,5,110055,3 +17642,5,110241,3 +17643,5,110251,3 +17644,5,110261,3 +17645,5,110271,3 +17646,5,110730,3 +17647,5,111020,3 +17648,5,111100,3 +17649,5,111160,3 +17650,5,120551,3 +17651,5,121160,3 \ No newline at end of file diff --git a/titles/sao/data/YuiMedalShopItems.csv b/titles/sao/data/YuiMedalShopItems.csv new file mode 100644 index 0000000..f5d91fe --- /dev/null +++ b/titles/sao/data/YuiMedalShopItems.csv @@ -0,0 +1,789 @@ +YuiMedalShopItemId,YuiMedalShopId,CommonRewardType,CommonRewardId,CommonRewardNum,Strength,Property1PropertyId,Property1Value1,Property1Value2,Property2PropertyId,Property2Value1,Property2Value2,Property3PropertyId,Property3Value1,Property3Value2,Property4PropertyId,Property4Value1,Property4Value2 +1,1,1,101000190,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +2,2,1,110000150,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +3,3,2,120000003,1,0,302000,10,0,301900,20,0,2,0,0,2,0,0 +4,4,2,101000004,1,0,200400,90,0,200400,90,0,2,0,0,2,0,0 +5,5,2,102000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +6,6,2,103000004,1,0,204100,0,0,204800,5,0,2,0,0,2,0,0 +7,7,2,109000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +8,8,2,112000004,1,0,305000,100,0,1,0,0,2,0,0,2,0,0 +9,9,1,105000160,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +10,10,1,102000210,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +11,11,2,101000002,1,0,202400,75,0,202400,75,0,2,0,0,99999,0,0 +12,12,2,105000004,1,0,202700,50,0,204700,1,0,2,0,0,2,0,0 +13,13,2,108000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +14,14,2,111000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +15,15,2,107000004,1,0,205600,0,0,1,0,0,2,0,0,2,0,0 +16,16,2,115000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +17,17,1,106000180,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +18,18,1,112000090,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +19,19,2,102000002,1,0,202600,50,0,202600,50,0,2,0,0,99999,0,0 +20,20,2,101000004,1,0,200400,90,0,200400,90,0,2,0,0,2,0,0 +21,21,2,102000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +22,22,2,103000004,1,0,204100,0,0,204800,5,0,2,0,0,2,0,0 +23,23,2,109000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +24,24,2,112000004,1,0,305000,100,0,1,0,0,2,0,0,2,0,0 +25,25,1,104000230,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +26,26,1,101000250,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +27,27,1,102000260,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +28,28,1,103000220,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +29,29,1,104000250,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +30,30,1,105000210,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +31,31,1,106000210,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +32,32,1,107000140,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +33,33,1,108000150,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +34,34,1,109000230,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +35,35,1,110000170,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +36,36,1,111000140,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +37,37,1,112000110,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +38,38,3,170004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +39,39,3,180004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +40,40,3,170001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +41,41,3,180001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +42,42,3,170005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +43,43,3,180005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +44,44,3,140000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +45,45,3,150010,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +46,46,3,150020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +47,47,3,150030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +48,48,3,150040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +49,49,3,190000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +50,50,3,200000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +51,51,3,210000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +52,52,1,103000230,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +53,53,1,111000160,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +54,54,1,107000150,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +55,55,1,108000160,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +56,56,8,201000090,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +57,57,8,299000440,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +58,58,8,299000450,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +59,59,8,299000460,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +60,60,8,299000470,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +61,61,8,299000480,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +62,62,3,170004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +63,63,3,180004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +64,64,3,170001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +65,65,3,180001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +66,66,3,170005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +67,67,3,180005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +68,68,3,140000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +69,69,3,150010,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +70,70,3,150020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +71,71,3,150030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +72,72,3,150040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +73,73,3,190000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +74,74,3,200000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +75,75,3,210000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +76,76,2,101000018,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +77,77,2,102000009,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +78,78,2,111000008,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +79,79,8,202000160,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +80,80,8,203000150,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +81,81,8,204000110,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +82,82,8,205000110,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +83,83,8,206000120,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +84,84,8,212000050,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +85,85,3,170004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +86,86,3,180004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +87,87,3,170001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +88,88,3,180001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +89,89,3,170005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +90,90,3,180005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +91,91,3,140000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +92,92,3,150010,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +93,93,3,150020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +94,94,3,150030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +95,95,3,150040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +96,96,3,190000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +97,97,3,200000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +98,98,3,210000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +99,99,1,109000130,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +100,100,8,299000550,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +101,101,8,299000560,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +102,102,8,299000570,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +103,103,8,299000580,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +104,104,8,299000590,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +105,105,8,299000600,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +106,106,3,170004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +107,107,3,180004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +108,108,3,170001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +109,109,3,180001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +110,110,3,170005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +111,111,3,180005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +112,112,3,140000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +113,113,3,150010,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +114,114,3,150020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +115,115,3,150030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +116,116,3,150040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +117,117,3,190000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +118,118,3,200000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +119,119,3,210000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +120,120,1,110000130,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +121,121,3,110060,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +122,122,3,110070,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +123,123,3,110080,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +124,124,3,110090,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +125,125,3,110100,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +126,126,3,110110,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +127,127,3,110120,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +128,128,3,110130,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +129,129,3,110140,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +130,130,3,110150,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +131,131,3,110160,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +132,132,3,110170,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +133,133,3,110180,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +134,134,1,109000240,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +135,135,1,108000160,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +136,136,1,110000150,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +137,137,1,118000100,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +138,138,1,101000190,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +139,139,1,101000300,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +140,140,1,102000360,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +141,141,1,103000310,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +142,142,1,104000370,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +143,143,1,109000290,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +144,144,8,211000060,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +145,145,8,299000610,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +146,146,8,299000620,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +147,147,8,299000630,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +148,148,8,299000640,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +149,149,8,299000650,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +150,150,1,111000080,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +151,151,1,112000080,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +152,152,3,170004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +153,153,3,180004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +154,154,3,170001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +155,155,3,180001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +156,156,3,170005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +157,157,3,180005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +158,158,3,140000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +159,159,3,150010,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +160,160,3,150020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +161,161,3,150030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +162,162,3,150040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +163,163,3,190000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +164,164,3,200000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +165,165,3,210000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +166,166,1,105000160,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +167,167,1,110000190,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +168,168,1,102000210,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +169,169,1,106000270,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +170,170,1,112000090,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +171,171,1,111000220,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +172,172,1,106000180,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +173,173,1,112000140,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +174,174,1,104000230,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +175,175,1,108000170,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +176,176,1,103000230,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +177,177,1,105000280,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +178,178,1,111000160,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +179,179,1,103000360,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +180,180,1,109000240,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +181,181,1,101000380,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +182,182,1,107000150,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +183,183,1,109000370,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +184,184,1,108000160,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +185,185,1,118000170,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +186,186,1,110000190,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +187,187,1,102000500,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +188,188,1,118000100,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +189,189,1,104000470,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +190,190,1,106000270,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +191,191,1,107000180,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +192,192,1,112000140,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +193,193,1,111000170,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +194,194,1,101000190,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +195,195,1,111000220,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +196,196,1,104000330,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +197,197,1,110000150,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +198,198,1,108000170,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +199,199,1,112000130,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +200,200,1,102000210,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +201,201,1,105000280,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +202,202,1,106000240,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +203,203,1,105000160,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +204,204,1,101000380,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +205,205,1,103000300,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +206,206,1,106000180,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +207,207,1,103000360,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +208,208,1,102000340,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +5000,5000,1,101000130,1,70,102600,30,0,1,0,0,2,0,0,2,0,0 +5001,5001,1,102000120,1,70,101700,0,0,1,0,0,2,0,0,2,0,0 +5002,5002,1,105000120,1,70,100500,100,0,1,0,0,2,0,0,2,0,0 +5003,5003,1,109000110,1,70,102600,30,0,1,0,0,2,0,0,2,0,0 +5004,5004,1,118000070,1,70,102600,30,0,1,0,0,2,0,0,2,0,0 +5005,5005,1,110000110,1,70,100500,100,0,1,0,0,2,0,0,2,0,0 +10000,10000,1,101000080,1,70,100200,100,0,100300,100,0,100300,100,0,99999,0,0 +10001,10001,1,102000080,1,70,100200,100,0,100300,100,0,100300,100,0,99999,0,0 +10002,10002,1,103000080,1,70,100200,100,0,100300,100,0,100300,100,0,99999,0,0 +10003,10003,1,104000260,1,70,100200,100,0,100300,100,0,100300,100,0,99999,0,0 +10004,10004,1,105000080,1,70,100200,100,0,100300,100,0,100300,100,0,99999,0,0 +10005,10005,1,106000080,1,70,100200,100,0,100300,100,0,100300,100,0,99999,0,0 +10006,10006,1,107000080,1,70,100200,100,0,100300,100,0,100300,100,0,99999,0,0 +10007,10007,1,108000080,1,70,100200,100,0,100300,100,0,100300,100,0,99999,0,0 +10008,10008,1,109000080,1,70,100200,100,0,100300,100,0,100300,100,0,99999,0,0 +10009,10009,1,110000080,1,70,100200,100,0,100300,100,0,100300,100,0,99999,0,0 +10010,10010,1,111000030,1,70,100200,100,0,100300,100,0,100300,100,0,99999,0,0 +10011,10011,1,112000030,1,70,100200,100,0,100300,100,0,100300,100,0,99999,0,0 +10012,10012,1,118000040,1,70,100200,100,0,100300,100,0,100300,100,0,99999,0,0 +10013,10013,2,101000002,1,0,202400,75,0,202400,75,0,2,0,0,99999,0,0 +10014,10014,2,102000002,1,0,202600,50,0,202600,50,0,2,0,0,99999,0,0 +10015,10015,2,101000004,1,0,200400,90,0,200400,90,0,2,0,0,2,0,0 +10016,10016,2,102000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +10017,10017,2,103000004,1,0,204100,0,0,204800,5,0,2,0,0,2,0,0 +10018,10018,2,105000004,1,0,202700,50,0,204700,1,0,2,0,0,2,0,0 +10019,10019,2,107000004,1,0,205600,0,0,1,0,0,2,0,0,2,0,0 +10020,10020,2,108000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +10021,10021,2,109000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +10022,10022,2,111000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +10023,10023,2,115000004,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +10024,10024,2,112000004,1,0,305000,100,0,1,0,0,2,0,0,2,0,0 +10025,10025,3,110060,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10026,10026,3,110070,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10027,10027,3,110080,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10028,10028,3,110090,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10029,10029,3,110100,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10030,10030,3,110110,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10031,10031,3,110120,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10032,10032,3,110130,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10033,10033,3,110140,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10034,10034,3,110150,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10035,10035,3,110160,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10036,10036,3,110170,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10037,10037,3,110180,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10038,10038,1,101000080,1,70,100200,100,0,100300,100,0,100300,100,0,99999,0,0 +10039,10039,1,102000080,1,70,100200,100,0,100300,100,0,100300,100,0,99999,0,0 +10040,10040,1,103000080,1,70,100200,100,0,100300,100,0,100300,100,0,99999,0,0 +10041,10041,1,104000260,1,70,100200,100,0,100300,100,0,100300,100,0,99999,0,0 +10042,10042,1,105000080,1,70,100200,100,0,100300,100,0,100300,100,0,99999,0,0 +10043,10043,1,106000080,1,70,100200,100,0,100300,100,0,100300,100,0,99999,0,0 +10044,10044,1,107000080,1,70,100200,100,0,100300,100,0,100300,100,0,99999,0,0 +10045,10045,1,108000080,1,70,100200,100,0,100300,100,0,100300,100,0,99999,0,0 +10046,10046,1,109000080,1,70,100200,100,0,100300,100,0,100300,100,0,99999,0,0 +10047,10047,1,110000080,1,70,100200,100,0,100300,100,0,100300,100,0,99999,0,0 +10048,10048,1,111000030,1,70,100200,100,0,100300,100,0,100300,100,0,99999,0,0 +10049,10049,1,112000030,1,70,100200,100,0,100300,100,0,100300,100,0,99999,0,0 +10050,10050,1,118000040,1,70,100200,100,0,100300,100,0,100300,100,0,99999,0,0 +10051,10051,2,101000004,1,70,1,0,0,1,0,0,2,0,0,2,0,0 +10052,10052,2,102000004,1,70,1,0,0,1,0,0,2,0,0,2,0,0 +10053,10053,2,103000004,1,70,204100,0,0,204800,5,0,2,0,0,2,0,0 +10054,10054,2,105000004,1,70,202700,50,0,204700,1,0,2,0,0,2,0,0 +10055,10055,2,107000004,1,70,205600,0,0,1,0,0,2,0,0,2,0,0 +10056,10056,2,108000004,1,70,1,0,0,1,0,0,2,0,0,2,0,0 +10057,10057,2,109000004,1,70,1,0,0,1,0,0,2,0,0,2,0,0 +10058,10058,2,111000004,1,70,1,0,0,1,0,0,2,0,0,2,0,0 +10059,10059,2,115000004,1,70,1,0,0,1,0,0,2,0,0,2,0,0 +10060,10060,2,112000004,1,70,305000,100,0,1,0,0,2,0,0,2,0,0 +10061,10061,2,101000002,1,70,202400,75,0,202400,75,0,2,0,0,99999,0,0 +10062,10062,2,102000002,1,70,202600,50,0,202600,50,0,2,0,0,99999,0,0 +10063,10063,3,110060,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10064,10064,3,110070,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10065,10065,3,110080,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10066,10066,3,110090,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10067,10067,3,110100,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10068,10068,3,110110,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10069,10069,3,110120,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10070,10070,3,110130,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10071,10071,3,110140,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10072,10072,3,110150,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10073,10073,3,110160,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10074,10074,3,110170,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10075,10075,3,110180,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10076,10076,3,120300,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10077,10077,3,120310,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10078,10078,3,120320,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10079,10079,3,120330,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10080,10080,3,120340,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10081,10081,3,120350,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10082,10082,3,120360,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10083,10083,3,120370,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10084,10084,3,120380,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +10085,10085,3,120390,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +15000,15000,1,101000190,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15001,15001,1,101000380,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15002,15002,1,102000210,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15003,15003,1,102000500,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15004,15004,1,103000230,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15005,15005,1,103000360,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15006,15006,1,104000230,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15007,15007,1,104000470,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15008,15008,1,105000160,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15009,15009,1,105000280,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15010,15010,1,106000180,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15011,15011,1,106000270,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15012,15012,1,107000150,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15013,15013,1,107000180,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15014,15014,1,108000160,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15015,15015,1,108000170,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15016,15016,1,109000240,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15017,15017,1,109000370,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15018,15018,1,110000150,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15019,15019,1,110000190,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15020,15020,1,111000160,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15021,15021,1,111000220,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15022,15022,1,112000090,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15023,15023,1,112000140,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15024,15024,1,118000100,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15025,15025,1,118000170,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15026,15026,1,101000340,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15027,15027,1,102000340,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15028,15028,1,102000550,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15029,15029,1,103000300,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15030,15030,1,103000420,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15031,15031,1,104000330,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15032,15032,1,104000430,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15033,15033,1,105000250,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15034,15034,1,105000390,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15035,15035,1,106000240,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15036,15036,1,106000330,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15037,15037,1,109000310,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15038,15038,1,110000220,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15039,15039,1,111000170,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15040,15040,1,112000130,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15041,15041,1,118000130,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15042,15042,1,118000210,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +15043,15043,2,101000004,1,70,1,0,0,1,0,0,2,0,0,2,0,0 +15044,15044,2,102000004,1,70,1,0,0,1,0,0,2,0,0,2,0,0 +15045,15045,2,103000004,1,70,204100,0,0,204800,5,0,2,0,0,2,0,0 +15046,15046,2,105000004,1,70,202700,50,0,204700,1,0,2,0,0,2,0,0 +15047,15047,2,107000004,1,70,205600,0,0,1,0,0,2,0,0,2,0,0 +15048,15048,2,108000004,1,70,1,0,0,1,0,0,2,0,0,2,0,0 +15049,15049,2,109000004,1,70,1,0,0,1,0,0,2,0,0,2,0,0 +15050,15050,2,111000004,1,70,1,0,0,1,0,0,2,0,0,2,0,0 +15051,15051,2,115000004,1,70,1,0,0,1,0,0,2,0,0,2,0,0 +15052,15052,2,112000004,1,70,305000,100,0,1,0,0,2,0,0,2,0,0 +15053,15053,3,140000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +15054,15054,3,150010,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +15055,15055,3,150020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +15056,15056,3,150030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +15057,15057,3,150040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +15058,15058,3,170007,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +15059,15059,3,180007,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +15060,15060,3,190000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +15061,15061,3,200000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +15062,15062,3,210000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +15063,15063,3,220002,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +15064,15064,3,170005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +202901,202901,1,101000310,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +202902,202902,1,102000410,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +202903,202903,8,201000120,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +202904,202904,8,202000240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +202905,202905,8,211000080,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +202906,202906,8,299000730,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +202907,202907,8,299000740,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +202908,202908,8,299000750,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +202909,202909,8,299000760,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +202910,202910,8,299000770,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +202911,202911,1,104000220,1,0,1,0,0,1,0,0,2,0,0,2,0,0 +202912,202912,8,299000410,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +202913,202913,3,170004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +202914,202914,3,180004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +202915,202915,3,170001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +202916,202916,3,180001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +202917,202917,3,170005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +202918,202918,3,180005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +202919,202919,3,140000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +202920,202920,3,150010,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +202921,202921,3,150020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +202922,202922,3,150030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +202923,202923,3,150040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +202924,202924,3,190000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +202925,202925,3,200000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +202926,202926,3,210000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203001,203001,8,204000210,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203002,203002,8,205000150,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203003,203003,8,299000800,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203004,203004,8,299000810,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203005,203005,8,299000820,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203006,203006,8,201000130,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203007,203007,8,299000830,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203008,203008,8,202000270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203009,203009,8,206000190,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203010,203010,8,220000060,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203011,203011,1,109000220,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +203012,203012,1,101000250,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +203013,203013,1,102000260,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +203014,203014,1,103000220,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +203015,203015,1,104000250,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +203016,203016,1,105000210,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +203017,203017,1,106000210,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +203018,203018,1,107000140,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +203019,203019,1,108000150,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +203020,203020,1,109000230,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +203021,203021,1,110000170,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +203022,203022,1,111000140,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +203023,203023,1,112000110,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +203024,203024,1,118000140,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +203025,203025,3,170004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203026,203026,3,180004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203027,203027,3,170001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203028,203028,3,180001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203029,203029,3,170005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203030,203030,3,180005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203031,203031,3,140000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203032,203032,3,150010,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203033,203033,3,150020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203034,203034,3,150030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203035,203035,3,150040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203036,203036,3,190000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203037,203037,3,200000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203038,203038,3,210000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203201,203201,8,206000200,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203202,203202,8,211000090,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203203,203203,8,299000840,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203204,203204,8,299000850,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203205,203205,8,299000860,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203206,203206,8,202000280,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203207,203207,8,204000220,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203208,203208,8,201000140,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203209,203209,8,203000230,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203210,203210,8,205000160,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203211,203211,1,102000280,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +203213,203213,3,170001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203214,203214,3,170004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203215,203215,3,170005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203216,203216,3,180001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203217,203217,3,180004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203218,203218,3,180005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203219,203219,3,140000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203220,203220,3,150010,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203221,203221,3,150020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203222,203222,3,150030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203223,203223,3,150040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203224,203224,3,190000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203225,203225,3,200000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203226,203226,3,210000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203301,203301,8,202000290,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203302,203302,8,203000240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203303,203303,8,299000870,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203304,203304,8,299000880,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203305,203305,8,299000890,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203306,203306,8,212000060,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203307,203307,8,201000150,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203308,203308,8,204000230,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203309,203309,8,209000200,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203310,203310,8,299000900,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203312,203312,8,291000040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203313,203313,3,170001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203314,203314,3,170004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203315,203315,3,170005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203316,203316,3,180001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203317,203317,3,180004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203318,203318,3,180005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203319,203319,3,140000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203320,203320,3,150010,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203321,203321,3,150020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203322,203322,3,150030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203323,203323,3,150040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203324,203324,3,190000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203325,203325,3,200000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203326,203326,3,210000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203501,203501,8,299000920,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203502,203502,8,299000930,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203503,203503,8,299000940,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203504,203504,8,299000950,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203505,203505,8,299000960,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203506,203506,8,206000200,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203507,203507,8,211000090,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203508,203508,8,299000840,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203509,203509,8,299000850,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203510,203510,8,299000860,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203511,203511,8,202000280,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203512,203512,8,204000220,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203513,203513,8,201000140,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203514,203514,8,203000230,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203515,203515,8,205000160,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203516,203516,1,101000270,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +203517,203517,1,111000240,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +203518,203518,1,102000280,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +203519,203519,3,170001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203520,203520,3,170004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203521,203521,3,170005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203522,203522,3,180001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203523,203523,3,180004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203524,203524,3,180005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203525,203525,3,140000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203526,203526,3,150010,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203527,203527,3,150020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203528,203528,3,150030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203529,203529,3,150040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203530,203530,3,190000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203531,203531,3,200000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203532,203532,3,210000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203601,203601,8,202000340,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203602,203602,8,205000190,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203603,203603,8,208000040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203604,203604,8,211000110,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203605,203605,8,220000070,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203606,203606,8,203000280,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203607,203607,8,202000350,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203608,203608,8,205000200,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203609,203609,8,299001050,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203610,203610,8,299001060,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203611,203611,1,103000270,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +203612,203612,1,104000420,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +203613,203613,8,291000040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203614,203614,8,201000090,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203615,203615,8,299000440,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203616,203616,8,299000450,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203617,203617,8,299000460,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203618,203618,8,299000470,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203619,203619,8,299000480,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203620,203620,8,202000290,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203621,203621,8,203000240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203622,203622,8,299000870,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203623,203623,8,299000880,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203624,203624,8,299000890,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203625,203625,8,212000060,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203626,203626,8,201000150,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203627,203627,8,204000230,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203628,203628,8,209000200,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203629,203629,8,299000900,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203630,203630,3,170001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203631,203631,3,170004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203632,203632,3,170005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203633,203633,3,180001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203634,203634,3,180004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203635,203635,3,180005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203636,203636,3,140000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203637,203637,3,150010,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203638,203638,3,150020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203639,203639,3,150030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203640,203640,3,150040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203641,203641,3,190000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203642,203642,3,200000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203643,203643,3,210000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203701,203701,3,170100,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203702,203702,3,170101,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203703,203703,3,170102,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203704,203704,3,170103,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203705,203705,3,170104,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203706,203706,8,203000300,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203707,203707,8,204000320,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203708,203708,8,217000080,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203709,203709,8,218000090,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203710,203710,8,299001070,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203711,203711,8,299001080,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203712,203712,8,298000130,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203713,203713,8,215000120,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203714,203714,1,106000260,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +203715,203715,8,202000160,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203716,203716,8,203000150,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203717,203717,8,204000110,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203718,203718,8,205000110,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203719,203719,8,206000120,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203720,203720,8,212000050,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203721,203721,3,170001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203722,203722,3,170004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203723,203723,3,170005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203724,203724,3,180001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203725,203725,3,180004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203726,203726,3,180005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203727,203727,3,140000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203728,203728,3,150010,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203729,203729,3,150020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203730,203730,3,150030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203731,203731,3,150040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203732,203732,3,190000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203733,203733,3,200000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203734,203734,3,210000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203801,203801,8,202000380,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203802,203802,8,205000230,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203803,203803,8,206000250,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203804,203804,8,210000030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203805,203805,8,299001090,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203806,203806,8,220000090,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203807,203807,8,299001100,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203808,203808,8,299001110,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203809,203809,1,105000240,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +203810,203810,8,299000550,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203811,203811,8,299000560,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203812,203812,8,299000570,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203813,203813,8,299000580,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203814,203814,8,299000590,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203815,203815,8,299000600,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203816,203816,3,170001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203817,203817,3,170004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203818,203818,3,170005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203819,203819,3,180001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203820,203820,3,180004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203821,203821,3,180005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203822,203822,3,140000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203823,203823,3,150010,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203824,203824,3,150020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203825,203825,3,150030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203826,203826,3,150040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203827,203827,3,190000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203828,203828,3,200000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +203829,203829,3,210000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204001,204001,8,202000420,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204002,204002,8,203000340,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204003,204003,8,204000360,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204004,204004,8,211000140,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204005,204005,8,212000090,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204006,204006,1,101000300,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204007,204007,1,102000360,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204008,204008,1,103000310,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204009,204009,1,104000370,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204010,204010,1,109000290,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204011,204011,1,109000130,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204012,204012,1,110000130,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204013,204013,1,111000080,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204014,204014,1,112000080,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204015,204015,1,104000220,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204016,204016,1,109000300,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204017,204017,8,211000060,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204018,204018,8,299000610,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204019,204019,8,299000620,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204020,204020,8,299000630,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204021,204021,8,299000640,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204022,204022,8,299000650,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204023,204023,8,299000410,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204024,204024,8,299000660,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204025,204025,8,215000050,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204026,204026,8,216000060,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204027,204027,8,218000030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204028,204028,8,217000020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204029,204029,8,299000500,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204030,204030,8,299000510,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204031,204031,8,299000520,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204032,204032,8,299000530,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204033,204033,8,299000540,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204034,204034,3,170001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204035,204035,3,170004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204036,204036,3,170005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204037,204037,3,180001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204038,204038,3,180004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204039,204039,3,180005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204040,204040,3,140000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204041,204041,3,150010,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204042,204042,3,150020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204043,204043,3,150030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204044,204044,3,150040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204045,204045,3,120241,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204046,204046,3,120251,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204047,204047,3,120261,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204048,204048,3,120271,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204049,204049,3,120730,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204050,204050,3,190000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204051,204051,3,200000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204052,204052,3,210000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204101,204101,8,209000280,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204102,204102,8,211000120,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204103,204103,8,212000070,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204104,204104,8,215000130,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204105,204105,8,216000130,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204106,204106,8,299001130,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204107,204107,8,299001140,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204108,204108,8,299001150,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204109,204109,1,102000430,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204110,204110,1,109000220,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204111,204111,1,101000310,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204112,204112,1,102000410,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204113,204113,1,101000250,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204114,204114,1,102000260,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204115,204115,1,103000220,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204116,204116,1,104000250,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204117,204117,1,105000210,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204118,204118,1,106000210,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204119,204119,1,107000140,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204120,204120,1,108000150,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204121,204121,1,109000230,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204122,204122,1,110000170,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204123,204123,1,111000140,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204124,204124,1,112000110,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204125,204125,1,118000140,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204126,204126,8,201000120,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204127,204127,8,202000240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204128,204128,8,211000080,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204129,204129,8,299000730,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204130,204130,8,299000740,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204131,204131,8,299000750,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204132,204132,8,299000760,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204133,204133,8,299000770,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204134,204134,3,170001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204135,204135,3,170004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204136,204136,3,170005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204137,204137,3,180001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204138,204138,3,180004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204139,204139,3,180005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204140,204140,3,140000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204141,204141,3,150010,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204142,204142,3,150020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204143,204143,3,150030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204144,204144,3,150040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204145,204145,3,190000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204146,204146,3,200000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204147,204147,3,210000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204201,204201,1,101000430,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204202,204202,1,102000580,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204203,204203,1,103000480,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204204,204204,1,104000520,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204205,204205,1,109000430,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204206,204206,1,111000350,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204207,204207,8,205000240,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204208,204208,8,206000260,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204209,204209,8,215000140,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204210,204210,8,216000140,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204211,204211,8,217000090,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204212,204212,8,218000100,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204213,204213,8,299001120,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204214,204214,1,102000280,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204215,204215,8,291000040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204216,204216,8,204000210,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204217,204217,8,205000150,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204218,204218,8,299000800,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204219,204219,8,299000810,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204220,204220,8,299000820,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204221,204221,8,201000130,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204222,204222,8,299000830,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204223,204223,8,202000270,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204224,204224,8,206000190,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204225,204225,8,220000060,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204226,204226,3,170001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204227,204227,3,170004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204228,204228,3,170005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204229,204229,3,180001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204230,204230,3,180004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204231,204231,3,180005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204232,204232,3,140000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204233,204233,3,150010,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204234,204234,3,150020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204235,204235,3,150030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204236,204236,3,150040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204237,204237,3,190000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204238,204238,3,200000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204239,204239,3,210000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204301,204301,8,202000390,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204302,204302,8,203000310,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204303,204303,8,204000330,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204304,204304,8,209000290,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204305,204305,8,210000040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204306,204306,8,211000130,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204307,204307,8,212000080,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204308,204308,1,101000270,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204309,204309,1,104000420,1,1,1,0,0,1,0,0,2,0,0,2,0,0 +204310,204310,8,206000200,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204311,204311,8,211000090,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204312,204312,8,299000840,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204313,204313,8,299000850,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204314,204314,8,299000860,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204315,204315,8,202000280,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204316,204316,8,204000220,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204317,204317,8,201000140,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204318,204318,8,203000230,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204319,204319,8,205000160,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204320,204320,3,170001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204321,204321,3,170004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204322,204322,3,170005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204323,204323,3,180001,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204324,204324,3,180004,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204325,204325,3,180005,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204326,204326,3,140000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204327,204327,3,150010,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204328,204328,3,150020,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204329,204329,3,150030,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204330,204330,3,150040,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204331,204331,3,190000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204332,204332,3,200000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 +204333,204333,3,210000,1,0,1,0,0,1,0,0,1,0,0,1,0,0 diff --git a/titles/sao/data/YuiMedalShops.csv b/titles/sao/data/YuiMedalShops.csv new file mode 100644 index 0000000..5bac235 --- /dev/null +++ b/titles/sao/data/YuiMedalShops.csv @@ -0,0 +1,789 @@ +YuiMedalShopId,Name,Description,SellingYuiMedal,SellingCol,SellingEventItemId,SellingEventItemNum,SellingTicketNum,PurchaseLimit,PickUpFlag,ProductCategory,SalesType,TargetDays,TargetHour,IntervalHour,SalesStartDate,SalesEndDate,Sort +1,"≪想い馳せる窓辺≫キリト",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2019/08/01","2019/11/01 2:00:00",0 +2,"≪悪戯なオネーサン≫アルゴ",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2019/08/01","2019/11/01 2:00:00",0 +3,"ネックレス",,2,-1,0,-1,-1,-1,False,0,0,0,0,0,"2019/08/01","2019/11/01 2:00:00",0 +4,"エリュシデータ",,5,-1,0,-1,-1,-1,False,0,0,0,0,0,"2019/08/01","2019/11/01 2:00:00",0 +5,"アロンダイト",,5,-1,0,-1,-1,-1,False,0,0,0,0,0,"2019/08/01","2019/11/01 2:00:00",0 +6,"スクレープ",,5,-1,0,-1,-1,-1,False,0,0,0,0,0,"2019/08/01","2019/11/01 2:00:00",0 +7,"ジャイアント・タスク",,5,-1,0,-1,-1,-1,False,0,0,0,0,0,"2019/08/01","2019/11/01 2:00:00",0 +8,"スヴェル",,5,-1,0,-1,-1,-1,False,0,0,0,0,0,"2019/08/01","2019/11/01 2:00:00",0 +9,"≪信頼の口付け≫リズベット",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2019/10/01 7:00:00","2019/12/01 2:00:00",0 +10,"≪愛の誓い≫アスナ",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2019/11/01 7:00:00","2020/01/01 2:00:00",0 +11,"ブラックプレート",,1,-1,0,-1,-1,-1,False,0,0,0,0,0,"2019/11/01 7:00:00","2021/01/01 2:00:00",1 +12,"ムラマサ",,5,-1,0,-1,-1,-1,False,0,0,0,0,0,"2019/11/01 7:00:00","2021/01/01 2:00:00",0 +13,"ヴェンデッタ",,5,-1,0,-1,-1,-1,False,0,0,0,0,0,"2019/11/01 7:00:00","2021/01/01 2:00:00",0 +14,"フェイルノート",,5,-1,0,-1,-1,-1,False,0,0,0,0,0,"2019/11/01 7:00:00","2021/01/01 2:00:00",0 +15,"ヴィズィー・ガジェット",,5,-1,0,-1,-1,-1,False,0,0,0,0,0,"2019/11/01 7:00:00","2021/01/01 2:00:00",0 +16,"MEBRカスタム",,5,-1,0,-1,-1,-1,False,0,0,0,0,0,"2019/11/01 7:00:00","2021/01/01 2:00:00",0 +17,"《宙吊りプラント》シリカ",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2019/12/01 7:00:00","2020/02/01 2:00:00",0 +18,"《奇想の相棒》ユージオ",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2020/01/01 7:00:00","2020/03/01 2:00:00",0 +19,"エペ・ド・マルブル",,1,-1,0,-1,-1,-1,False,0,0,0,0,0,"2020/01/01 7:00:00","2021/01/01 2:00:00",2 +20,"エリュシデータ",,5,-1,0,-1,-1,-1,False,0,0,0,0,0,"2020/01/01 7:00:00","2021/01/01 2:00:00",0 +21,"アロンダイト",,5,-1,0,-1,-1,-1,False,0,0,0,0,0,"2020/01/01 7:00:00","2021/01/01 2:00:00",0 +22,"スクレープ",,5,-1,0,-1,-1,-1,False,0,0,0,0,0,"2020/01/01 7:00:00","2021/01/01 2:00:00",0 +23,"ジャイアント・タスク",,5,-1,0,-1,-1,-1,False,0,0,0,0,0,"2020/01/01 7:00:00","2021/01/01 2:00:00",0 +24,"スヴェル",,5,-1,0,-1,-1,-1,False,0,0,0,0,0,"2020/01/01 7:00:00","2021/01/01 2:00:00",0 +25,"《疾風迅雷》シノン",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2020/02/01 7:00:00","2020/04/01 2:00:00",0 +26,"《愛受のはにかみ》キリト",,-1,-1,10001,3,-1,-1,False,0,0,0,0,0,"2020/02/11 7:00:00","2020/05/15 13:59:59",20 +27,"《甘い贈り物》アスナ",,-1,-1,10001,3,-1,-1,False,0,0,0,0,0,"2020/02/11 7:00:00","2020/05/15 13:59:59",19 +28,"《恋慕の微笑》リーファ",,-1,-1,10001,3,-1,-1,False,0,0,0,0,0,"2020/02/11 7:00:00","2020/05/15 13:59:59",18 +29,"《静穏の秘め事》シノン",,-1,-1,10001,3,-1,-1,False,0,0,0,0,0,"2020/02/11 7:00:00","2020/05/15 13:59:59",17 +30,"《親愛の鍛冶屋》リズベット",,-1,-1,10001,3,-1,-1,False,0,0,0,0,0,"2020/02/11 7:00:00","2020/05/15 13:59:59",16 +31,"《純情な恋路》シリカ",,-1,-1,10001,3,-1,-1,False,0,0,0,0,0,"2020/02/11 7:00:00","2020/05/15 13:59:59",15 +32,"《歓喜の照笑》クライン",,-1,-1,10001,3,-1,-1,False,0,0,0,0,0,"2020/02/11 7:00:00","2020/05/15 13:59:59",14 +33,"《義理堅い甘露》エギル",,-1,-1,10001,3,-1,-1,False,0,0,0,0,0,"2020/02/11 7:00:00","2020/05/15 13:59:59",13 +34,"《純粋な愛情》ユウキ",,-1,-1,10001,3,-1,-1,False,0,0,0,0,0,"2020/02/11 7:00:00","2020/05/15 13:59:59",12 +35,"《秘密の贈り物》アルゴ",,-1,-1,10001,3,-1,-1,False,0,0,0,0,0,"2020/02/11 7:00:00","2020/05/15 13:59:59",11 +36,"《裏腹な恋心》アリス",,-1,-1,10001,3,-1,-1,False,0,0,0,0,0,"2020/02/11 7:00:00","2020/05/15 13:59:59",10 +37,"《心届く笑顔》ユージオ",,-1,-1,10001,3,-1,-1,False,0,0,0,0,0,"2020/02/11 7:00:00","2020/05/15 13:59:59",9 +38,"EXPチップ《テラ》",,-1,-1,10001,3,-1,-1,False,0,0,0,0,0,"2020/02/11 7:00:00","2020/05/15 13:59:59",7 +39,"ゴールド《テラ》",,-1,-1,10001,3,-1,-1,False,0,0,0,0,0,"2020/02/11 7:00:00","2020/05/15 13:59:59",4 +40,"EXPチップ《キロ》",,-1,-1,10001,1,-1,-1,False,0,0,0,0,0,"2020/02/11 7:00:00","2020/05/15 13:59:59",8 +41,"ゴールド《キロ》",,-1,-1,10001,1,-1,-1,False,0,0,0,0,0,"2020/02/11 7:00:00","2020/05/15 13:59:59",5 +42,"EXPメモリ",,-1,-1,10001,5,-1,-1,False,0,0,0,0,0,"2020/02/11 7:00:00","2020/05/15 13:59:59",6 +43,"プラチナ",,-1,-1,10001,5,-1,-1,False,0,0,0,0,0,"2020/02/11 7:00:00","2020/05/15 13:59:59",3 +44,"プロパティアペンド",,-1,-1,10001,3,-1,-1,False,0,0,0,0,0,"2020/02/11 7:00:00","2020/05/15 13:59:59",0 +45,"コードイレイサー:1",,-1,-1,10001,3,-1,-1,False,0,0,0,0,0,"2020/02/11 7:00:00","2020/05/15 13:59:59",0 +46,"コードイレイサー:2",,-1,-1,10001,3,-1,-1,False,0,0,0,0,0,"2020/02/11 7:00:00","2020/05/15 13:59:59",0 +47,"コードイレイサー:3",,-1,-1,10001,3,-1,-1,False,0,0,0,0,0,"2020/02/11 7:00:00","2020/05/15 13:59:59",0 +48,"コードイレイサー:4",,-1,-1,10001,3,-1,-1,False,0,0,0,0,0,"2020/02/11 7:00:00","2020/05/15 13:59:59",0 +49,"ヒーローレベルアペンド《1》",,-1,-1,10001,20,-1,-1,False,0,0,0,0,0,"2020/02/11 7:00:00","2020/05/15 13:59:59",0 +50,"ウェポンレベルアペンド《1》",,-1,-1,10001,20,-1,-1,False,0,0,0,0,0,"2020/02/11 7:00:00","2020/05/15 13:59:59",0 +51,"アクセサリレベルアペンド《1》",,-1,-1,10001,20,-1,-1,False,0,0,0,0,0,"2020/02/11 7:00:00","2020/05/15 13:59:59",0 +52,"《秘めた胸奥》リーファ",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2020/03/01 7:00:00","2020/07/01 2:00:00",1 +53,"《固守の決意》アリス",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2020/06/09 7:00:00","2020/09/01 2:00:00",2 +54,"《武人の少憩》クライン",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2020/07/01 7:00:00","2020/09/01 2:00:00",3 +55,"《金剛なる体躯》エギル",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2020/09/01 7:00:00","2020/11/01 2:00:00",4 +56,"《異世界の英雄》キリト",,-1,-1,10002,20,-1,-1,False,0,0,0,0,0,"2020/07/14 7:00:00","2020/08/11 2:00:00",0 +57,"《彼岸花の君》メディナ",,-1,-1,10002,20,-1,-1,False,0,0,0,0,0,"2020/07/14 7:00:00","2020/08/11 2:00:00",0 +58,"《柔靭なる剣士》ソルティリーナ",,-1,-1,10002,20,-1,-1,False,0,0,0,0,0,"2020/07/14 7:00:00","2020/08/11 2:00:00",0 +59,"《図書室の賢者》カーディナル",,-1,-1,10002,20,-1,-1,False,0,0,0,0,0,"2020/07/14 7:00:00","2020/08/11 2:00:00",0 +60,"《絶対的支配者》アドミニストレータ",,-1,-1,10002,20,-1,-1,False,0,0,0,0,0,"2020/07/14 7:00:00","2020/08/11 2:00:00",0 +61,"《愛の忠臣》チュデルキン",,-1,-1,10002,20,-1,-1,False,0,0,0,0,0,"2020/07/14 7:00:00","2020/08/11 2:00:00",0 +62,"EXPチップ《テラ》",,-1,-1,10002,3,-1,-1,False,0,0,0,0,0,"2020/07/14 7:00:00","2020/08/11 2:00:00",0 +63,"ゴールド《テラ》",,-1,-1,10002,3,-1,-1,False,0,0,0,0,0,"2020/07/14 7:00:00","2020/08/11 2:00:00",0 +64,"EXPチップ《キロ》",,-1,-1,10002,1,-1,-1,False,0,0,0,0,0,"2020/07/14 7:00:00","2020/08/11 2:00:00",0 +65,"ゴールド《キロ》",,-1,-1,10002,1,-1,-1,False,0,0,0,0,0,"2020/07/14 7:00:00","2020/08/11 2:00:00",0 +66,"EXPメモリ",,-1,-1,10002,5,-1,-1,False,0,0,0,0,0,"2020/07/14 7:00:00","2020/08/11 2:00:00",0 +67,"プラチナ",,-1,-1,10002,5,-1,-1,False,0,0,0,0,0,"2020/07/14 7:00:00","2020/08/11 2:00:00",0 +68,"プロパティアペンド",,-1,-1,10002,3,-1,-1,False,0,0,0,0,0,"2020/07/14 7:00:00","2020/08/11 2:00:00",0 +69,"コードイレイサー:1",,-1,-1,10002,3,-1,-1,False,0,0,0,0,0,"2020/07/14 7:00:00","2020/08/11 2:00:00",0 +70,"コードイレイサー:2",,-1,-1,10002,3,-1,-1,False,0,0,0,0,0,"2020/07/14 7:00:00","2020/08/11 2:00:00",0 +71,"コードイレイサー:3",,-1,-1,10002,3,-1,-1,False,0,0,0,0,0,"2020/07/14 7:00:00","2020/08/11 2:00:00",0 +72,"コードイレイサー:4",,-1,-1,10002,3,-1,-1,False,0,0,0,0,0,"2020/07/14 7:00:00","2020/08/11 2:00:00",0 +73,"ヒーローレベルアペンド《1》",,-1,-1,10002,20,-1,-1,False,0,0,0,0,0,"2020/07/14 7:00:00","2020/08/11 2:00:00",0 +74,"ウェポンレベルアペンド《1》",,-1,-1,10002,20,-1,-1,False,0,0,0,0,0,"2020/07/14 7:00:00","2020/08/11 2:00:00",0 +75,"アクセサリレベルアペンド《1》",,-1,-1,10002,20,-1,-1,False,0,0,0,0,0,"2020/07/14 7:00:00","2020/08/11 2:00:00",0 +76,"ヴァーデュラス・アニマ",,-1,-1,10002,8,-1,-1,False,0,0,0,0,0,"2020/07/14 7:00:00","2020/08/11 2:00:00",22 +77,"ラディアント・ライト",,-1,-1,10002,8,-1,-1,False,0,0,0,0,0,"2020/07/14 7:00:00","2020/08/11 2:00:00",21 +78,"アニヒレート・レイ",,-1,-1,10002,8,-1,-1,False,0,0,0,0,0,"2020/07/14 7:00:00","2020/08/11 2:00:00",20 +79,"《愛する人へ》アスナ",,-1,-1,10003,3,-1,-1,False,0,0,0,0,0,"2020/08/11 7:00:00","2020/09/08 2:00:00",20 +80,"《大切な家族》リーファ",,-1,-1,10003,3,-1,-1,False,0,0,0,0,0,"2020/08/11 7:00:00","2020/09/08 2:00:00",19 +81,"《強さを知って》シノン",,-1,-1,10003,3,-1,-1,False,0,0,0,0,0,"2020/08/11 7:00:00","2020/09/08 2:00:00",18 +82,"《この温度を君に》リズベット",,-1,-1,10003,3,-1,-1,False,0,0,0,0,0,"2020/08/11 7:00:00","2020/09/08 2:00:00",17 +83,"《今度はあたしが》シリカ",,-1,-1,10003,3,-1,-1,False,0,0,0,0,0,"2020/08/11 7:00:00","2020/09/08 2:00:00",16 +84,"《傍らの相棒》ユージオ",,-1,-1,10003,3,-1,-1,False,0,0,0,0,0,"2020/08/11 7:00:00","2020/09/08 2:00:00",15 +85,"EXPチップ《テラ》",,-1,-1,10003,3,-1,-1,False,0,0,0,0,0,"2020/08/11 7:00:00","2020/09/08 2:00:00",12 +86,"ゴールド《テラ》",,-1,-1,10003,3,-1,-1,False,0,0,0,0,0,"2020/08/11 7:00:00","2020/09/08 2:00:00",9 +87,"EXPチップ《キロ》",,-1,-1,10003,1,-1,-1,False,0,0,0,0,0,"2020/08/11 7:00:00","2020/09/08 2:00:00",13 +88,"ゴールド《キロ》",,-1,-1,10003,1,-1,-1,False,0,0,0,0,0,"2020/08/11 7:00:00","2020/09/08 2:00:00",10 +89,"EXPメモリ",,-1,-1,10003,5,-1,-1,False,0,0,0,0,0,"2020/08/11 7:00:00","2020/09/08 2:00:00",11 +90,"プラチナ",,-1,-1,10003,5,-1,-1,False,0,0,0,0,0,"2020/08/11 7:00:00","2020/09/08 2:00:00",8 +91,"プロパティアペンド",,-1,-1,10003,3,-1,-1,False,0,0,0,0,0,"2020/08/11 7:00:00","2020/09/08 2:00:00",7 +92,"コードイレイサー:1",,-1,-1,10003,3,-1,-1,False,0,0,0,0,0,"2020/08/11 7:00:00","2020/09/08 2:00:00",6 +93,"コードイレイサー:2",,-1,-1,10003,3,-1,-1,False,0,0,0,0,0,"2020/08/11 7:00:00","2020/09/08 2:00:00",5 +94,"コードイレイサー:3",,-1,-1,10003,3,-1,-1,False,0,0,0,0,0,"2020/08/11 7:00:00","2020/09/08 2:00:00",4 +95,"コードイレイサー:4",,-1,-1,10003,3,-1,-1,False,0,0,0,0,0,"2020/08/11 7:00:00","2020/09/08 2:00:00",3 +96,"ヒーローレベルアペンド《1》",,-1,-1,10003,20,-1,-1,False,0,0,0,0,0,"2020/08/11 7:00:00","2020/09/08 2:00:00",2 +97,"ウェポンレベルアペンド《1》",,-1,-1,10003,20,-1,-1,False,0,0,0,0,0,"2020/08/11 7:00:00","2020/09/08 2:00:00",1 +98,"アクセサリレベルアペンド《1》",,-1,-1,10003,20,-1,-1,False,0,0,0,0,0,"2020/08/11 7:00:00","2020/09/08 2:00:00",0 +99,"≪蒼穹を駆ける英傑≫ユウキ",,-1,-1,10003,6,-1,-1,False,0,0,0,0,0,"2020/08/11 7:00:00","2020/09/08 2:00:00",14 +100,"《整合騎士長》ベルクーリ",,-1,-1,10004,3,-1,-1,False,0,0,0,0,0,"2020/09/08 7:00:00","2020/10/13 2:00:00",20 +101,"《天穿の光》ファナティオ",,-1,-1,10004,3,-1,-1,False,0,0,0,0,0,"2020/09/08 7:00:00","2020/10/13 2:00:00",19 +102,"《焔の弓騎士》デュソルバート",,-1,-1,10004,3,-1,-1,False,0,0,0,0,0,"2020/09/08 7:00:00","2020/10/13 2:00:00",18 +103,"《師への敬愛》エルドリエ",,-1,-1,10004,3,-1,-1,False,0,0,0,0,0,"2020/09/08 7:00:00","2020/10/13 2:00:00",17 +104,"《黒百合の一閃》シェータ",,-1,-1,10004,3,-1,-1,False,0,0,0,0,0,"2020/09/08 7:00:00","2020/10/13 2:00:00",16 +105,"《失くした片翼》レンリ",,-1,-1,10004,3,-1,-1,False,0,0,0,0,0,"2020/09/08 7:00:00","2020/10/13 2:00:00",15 +106,"EXPチップ《テラ》",,-1,-1,10004,3,-1,-1,False,0,0,0,0,0,"2020/09/08 7:00:00","2020/10/13 2:00:00",12 +107,"ゴールド《テラ》",,-1,-1,10004,3,-1,-1,False,0,0,0,0,0,"2020/09/08 7:00:00","2020/10/13 2:00:00",9 +108,"EXPチップ《キロ》",,-1,-1,10004,1,-1,-1,False,0,0,0,0,0,"2020/09/08 7:00:00","2020/10/13 2:00:00",13 +109,"ゴールド《キロ》",,-1,-1,10004,1,-1,-1,False,0,0,0,0,0,"2020/09/08 7:00:00","2020/10/13 2:00:00",10 +110,"EXPメモリ",,-1,-1,10004,5,-1,-1,False,0,0,0,0,0,"2020/09/08 7:00:00","2020/10/13 2:00:00",11 +111,"プラチナ",,-1,-1,10004,5,-1,-1,False,0,0,0,0,0,"2020/09/08 7:00:00","2020/10/13 2:00:00",8 +112,"プロパティアペンド",,-1,-1,10004,3,-1,-1,False,0,0,0,0,0,"2020/09/08 7:00:00","2020/10/13 2:00:00",7 +113,"コードイレイサー:1",,-1,-1,10004,3,-1,-1,False,0,0,0,0,0,"2020/09/08 7:00:00","2020/10/13 2:00:00",6 +114,"コードイレイサー:2",,-1,-1,10004,3,-1,-1,False,0,0,0,0,0,"2020/09/08 7:00:00","2020/10/13 2:00:00",5 +115,"コードイレイサー:3",,-1,-1,10004,3,-1,-1,False,0,0,0,0,0,"2020/09/08 7:00:00","2020/10/13 2:00:00",4 +116,"コードイレイサー:4",,-1,-1,10004,3,-1,-1,False,0,0,0,0,0,"2020/09/08 7:00:00","2020/10/13 2:00:00",3 +117,"ヒーローレベルアペンド《1》",,-1,-1,10004,20,-1,-1,False,0,0,0,0,0,"2020/09/08 7:00:00","2020/10/13 2:00:00",2 +118,"ウェポンレベルアペンド《1》",,-1,-1,10004,20,-1,-1,False,0,0,0,0,0,"2020/09/08 7:00:00","2020/10/13 2:00:00",1 +119,"アクセサリレベルアペンド《1》",,-1,-1,10004,20,-1,-1,False,0,0,0,0,0,"2020/09/08 7:00:00","2020/10/13 2:00:00",0 +120,"≪万天に通ずる鼠≫アルゴ",,-1,-1,10004,6,-1,-1,False,0,0,0,0,0,"2020/09/08 7:00:00","2020/10/13 2:00:00",14 +121,"ヒーローコード:斬耐性",,-1,200000,0,-1,-1,-1,False,0,0,0,0,0,"2020/07/28 7:00:00","2021/01/01 2:00:00",0 +122,"ヒーローコード:打耐性",,-1,200000,0,-1,-1,-1,False,0,0,0,0,0,"2020/07/28 7:00:00","2021/01/01 2:00:00",0 +123,"ヒーローコード:突耐性",,-1,200000,0,-1,-1,-1,False,0,0,0,0,0,"2020/07/28 7:00:00","2021/01/01 2:00:00",0 +124,"ヒーローコード:火耐性",,-1,200000,0,-1,-1,-1,False,0,0,0,0,0,"2020/07/28 7:00:00","2021/01/01 2:00:00",0 +125,"ヒーローコード:水耐性",,-1,200000,0,-1,-1,-1,False,0,0,0,0,0,"2020/07/28 7:00:00","2021/01/01 2:00:00",0 +126,"ヒーローコード:風耐性",,-1,200000,0,-1,-1,-1,False,0,0,0,0,0,"2020/07/28 7:00:00","2021/01/01 2:00:00",0 +127,"ヒーローコード:土耐性",,-1,200000,0,-1,-1,-1,False,0,0,0,0,0,"2020/07/28 7:00:00","2021/01/01 2:00:00",0 +128,"ヒーローコード:聖耐性",,-1,200000,0,-1,-1,-1,False,0,0,0,0,0,"2020/07/28 7:00:00","2021/01/01 2:00:00",0 +129,"ヒーローコード:闇耐性",,-1,200000,0,-1,-1,-1,False,0,0,0,0,0,"2020/07/28 7:00:00","2021/01/01 2:00:00",0 +130,"ヒーローコード:毒無効",,-1,200000,0,-1,-1,-1,False,0,0,0,0,0,"2020/07/28 7:00:00","2021/01/01 2:00:00",0 +131,"ヒーローコード:混乱無効",,-1,200000,0,-1,-1,-1,False,0,0,0,0,0,"2020/07/28 7:00:00","2021/01/01 2:00:00",0 +132,"ヒーローコード:封印無効",,-1,200000,0,-1,-1,-1,False,0,0,0,0,0,"2020/07/28 7:00:00","2021/01/01 2:00:00",0 +133,"ヒーローコード:地震無効",,-1,200000,0,-1,-1,-1,False,0,0,0,0,0,"2020/07/28 7:00:00","2021/01/01 2:00:00",0 +134,"≪君との内緒事≫ユウキ",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2020/08/01 7:00:00","2020/10/01 2:00:00",1 +135,"《金剛なる体躯》エギル",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2020/09/01 7:00:00","2020/11/01 2:00:00",2 +136,"≪悪戯なオネーサン≫アルゴ",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2020/09/01 7:00:00","2020/10/01 2:00:00",0 +137,"≪赤面プラクティカ≫レイン",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2020/10/01 7:00:00","2020/12/01 2:00:00",3 +138,"≪想い馳せる窓辺≫キリト",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2020/10/01 7:00:00","2020/11/01 2:00:00",0 +139,"《共に歩む時》キリト",,-1,-1,10005,3,-1,-1,False,0,0,0,0,0,"2020/10/20 7:00:00","2020/12/01 2:00:00",26 +140,"《立ち上がる時》アスナ",,-1,-1,10005,3,-1,-1,False,0,0,0,0,0,"2020/10/20 7:00:00","2020/12/01 2:00:00",25 +141,"《心繋がる時》リーファ",,-1,-1,10005,3,-1,-1,False,0,0,0,0,0,"2020/10/20 7:00:00","2020/12/01 2:00:00",24 +142,"《意志示す時》シノン",,-1,-1,10005,3,-1,-1,False,0,0,0,0,0,"2020/10/20 7:00:00","2020/12/01 2:00:00",23 +143,"《背中押す時》ユウキ",,-1,-1,10005,3,-1,-1,False,0,0,0,0,0,"2020/10/20 7:00:00","2020/12/01 2:00:00",22 +144,"《誇り高き騎士》アリス",,-1,-1,10005,3,-1,-1,False,0,0,0,0,0,"2020/10/20 7:00:00","2020/12/01 2:00:00",21 +145,"《姉想う祈り》セルカ",,-1,-1,10005,3,-1,-1,False,0,0,0,0,0,"2020/10/20 7:00:00","2020/12/01 2:00:00",20 +146,"《正しき道》ロニエ",,-1,-1,10005,3,-1,-1,False,0,0,0,0,0,"2020/10/20 7:00:00","2020/12/01 2:00:00",19 +147,"《受け継ぐ希望》ティーゼ",,-1,-1,10005,3,-1,-1,False,0,0,0,0,0,"2020/10/20 7:00:00","2020/12/01 2:00:00",18 +148,"《無邪気な毒牙》フィゼル",,-1,-1,10005,3,-1,-1,False,0,0,0,0,0,"2020/10/20 7:00:00","2020/12/01 2:00:00",17 +149,"《穏やかな殺戮》リネル",,-1,-1,10005,3,-1,-1,False,0,0,0,0,0,"2020/10/20 7:00:00","2020/12/01 2:00:00",16 +150,"《心通う邂逅》アリス",,-1,-1,10005,10,-1,-1,False,0,0,0,0,0,"2020/10/20 7:00:00","2020/12/01 2:00:00",15 +151,"《かの剣技の継承者》ユージオ",,-1,-1,10005,10,-1,-1,False,0,0,0,0,0,"2020/10/20 7:00:00","2020/12/01 2:00:00",14 +152,"EXPチップ《テラ》",,-1,-1,10005,3,-1,-1,False,0,0,0,0,0,"2020/10/20 7:00:00","2020/12/01 2:00:00",12 +153,"ゴールド《テラ》",,-1,-1,10005,3,-1,-1,False,0,0,0,0,0,"2020/10/20 7:00:00","2020/12/01 2:00:00",9 +154,"EXPチップ《キロ》",,-1,-1,10005,1,-1,-1,False,0,0,0,0,0,"2020/10/20 7:00:00","2020/12/01 2:00:00",13 +155,"ゴールド《キロ》",,-1,-1,10005,1,-1,-1,False,0,0,0,0,0,"2020/10/20 7:00:00","2020/12/01 2:00:00",10 +156,"EXPメモリ",,-1,-1,10005,5,-1,-1,False,0,0,0,0,0,"2020/10/20 7:00:00","2020/12/01 2:00:00",11 +157,"プラチナ",,-1,-1,10005,5,-1,-1,False,0,0,0,0,0,"2020/10/20 7:00:00","2020/12/01 2:00:00",8 +158,"プロパティアペンド",,-1,-1,10005,3,-1,-1,False,0,0,0,0,0,"2020/10/20 7:00:00","2020/12/01 2:00:00",7 +159,"コードイレイサー:1",,-1,-1,10005,3,-1,-1,False,0,0,0,0,0,"2020/10/20 7:00:00","2020/12/01 2:00:00",6 +160,"コードイレイサー:2",,-1,-1,10005,3,-1,-1,False,0,0,0,0,0,"2020/10/20 7:00:00","2020/12/01 2:00:00",5 +161,"コードイレイサー:3",,-1,-1,10005,3,-1,-1,False,0,0,0,0,0,"2020/10/20 7:00:00","2020/12/01 2:00:00",4 +162,"コードイレイサー:4",,-1,-1,10005,3,-1,-1,False,0,0,0,0,0,"2020/10/20 7:00:00","2020/12/01 2:00:00",3 +163,"ヒーローレベルアペンド《1》",,-1,-1,10005,20,-1,-1,False,0,0,0,0,0,"2020/10/20 7:00:00","2020/12/01 2:00:00",2 +164,"ウェポンレベルアペンド《1》",,-1,-1,10005,20,-1,-1,False,0,0,0,0,0,"2020/10/20 7:00:00","2020/12/01 2:00:00",1 +165,"アクセサリレベルアペンド《1》",,-1,-1,10005,20,-1,-1,False,0,0,0,0,0,"2020/10/20 7:00:00","2020/12/01 2:00:00",0 +166,"《信頼の口付け》リズベット",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2020/11/01 7:00:00","2020/12/01 2:00:00",0 +167,"《隠された素顔》アルゴ",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2020/11/01 7:00:00","2021/01/01 2:00:00",0 +168,"《愛の誓い》アスナ",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2020/12/01 7:00:00","2021/01/01 2:00:00",0 +169,"《あどけない笑み》シリカ",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2020/12/01 7:00:00","2021/02/01 2:00:00",0 +170,"《奇想の相棒》ユージオ",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2021/01/01 7:00:00","2021/02/01 2:00:00",0 +171,"《光の巫女》アリス",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2021/01/01 7:00:00","2021/03/01 2:00:00",0 +172,"《宙吊りプラント》シリカ",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2021/02/01 7:00:00","2021/03/01 2:00:00",0 +173,"《休剣の手入れ》ユージオ",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2021/02/01 7:00:00","2021/04/01 2:00:00",0 +174,"《疾風迅雷》シノン",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2021/03/01 7:00:00","2021/04/01 2:00:00",0 +175,"《勇猛な巨躯》エギル",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2021/03/01 7:00:00","2021/05/01 2:00:00",0 +176,"《秘めた胸奥》リーファ",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2021/04/01 7:00:00","2021/05/01 2:00:00",0 +177,"《恋するお人形》リズベット",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2021/04/01 7:00:00","2021/06/01 2:00:00",0 +178,"《固守の決意》アリス",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2021/05/01 7:00:00","2021/06/01 2:00:00",0 +179,"《南風に吹かれて》リーファ",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2021/05/01 7:00:00","2021/07/01 2:00:00",0 +180,"《君との内緒事》ユウキ",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2021/06/01 7:00:00","2021/07/01 2:00:00",0 +181,"《妖精の決闘》キリト",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2021/06/01 7:00:00","2021/08/01 2:00:00",1 +182,"《武人の少憩》クライン",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2021/07/01 7:00:00","2021/08/01 2:00:00",0 +183,"《おすわりリトル》ユウキ",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2021/07/01 7:00:00","2021/09/01 2:00:00",2 +184,"《金剛なる体躯》エギル",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2021/08/01 7:00:00","2021/09/01 2:00:00",0 +185,"《胸躍る一歩》レイン",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2021/08/01 7:00:00","2021/10/01 2:00:00",3 +186,"《隠された素顔》アルゴ",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2021/09/01 7:00:00","2021/10/01 2:00:00",0 +187,"《水妖精の導き》アスナ",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2021/09/01 7:00:00","2021/11/01 2:00:00",4 +188,"《赤面プラクティカ》レイン",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2021/10/01 7:00:00","2021/11/01 2:00:00",0 +189,"《同朋の待望》シノン",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2021/10/01 7:00:00","2021/12/01 2:00:00",5 +190,"《あどけない笑み》シリカ",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2021/11/01 7:00:00","2021/12/01 2:00:00",0 +191,"《跳躍の托生》クライン",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2021/11/01 7:00:00","2022/01/01 2:00:00",6 +192,"《休剣の手入れ》ユージオ",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2021/12/01 7:00:00","2022/01/01 2:00:00",7 +193,"《物思い耽る午後》アリス",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2021/12/01 7:00:00","2022/01/01 2:00:00",8 +194,"《想い馳せる窓辺》キリト",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2022/01/01 7:00:00","2022/02/01 2:00:00",9 +195,"《光の巫女》アリス",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2022/01/01 7:00:00","2022/02/01 2:00:00",10 +196,"《悪戯な視線》シノン",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2022/01/01 7:00:00","2022/02/01 2:00:00",11 +197,"《悪戯なオネーサン》アルゴ",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2022/02/01 7:00:00","2022/03/01 2:00:00",12 +198,"《勇猛な巨躯》エギル",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2022/02/01 7:00:00","2022/03/01 2:00:00",13 +199,"《花舞う昼下がり》ユージオ",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2022/02/01 7:00:00","2022/03/01 2:00:00",14 +200,"《愛の誓い》アスナ",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2022/03/01 7:00:00","2022/04/01 2:00:00",15 +201,"《恋するお人形》リズベット",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2022/03/01 7:00:00","2022/04/01 2:00:00",16 +202,"《おやつ・タイム!》シリカ",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2022/03/01 7:00:00","2022/04/01 2:00:00",17 +203,"《信頼の口付け》リズベット",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2022/04/01 7:00:00","2022/05/01 2:00:00",18 +204,"《妖精の決闘》キリト",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2022/04/01 7:00:00","2022/05/01 2:00:00",19 +205,"《妖精の戯れ》リーファ",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2022/04/01 7:00:00","2022/05/01 2:00:00",20 +206,"《宙吊りプラント》シリカ",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2022/05/01 7:00:00","2022/06/07 2:00:00",21 +207,"《南風に吹かれて》リーファ",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2022/05/01 7:00:00","2022/06/07 2:00:00",22 +208,"《穏やかな一時》アスナ",,5,-1,0,-1,-1,-1,True,0,0,0,0,0,"2022/05/01 7:00:00","2022/06/07 2:00:00",23 +5000,"《神域の双剣使い》キリト",,-1,-1,0,-1,5,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/03/08 2:00:00",99 +5001,"《流麗なる剣士》アスナ",,-1,-1,0,-1,5,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/03/08 2:00:00",98 +5002,"《反撃の烽火》リズベット",,-1,-1,0,-1,5,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/03/08 2:00:00",97 +5003,"《神速の黒き閃光》ユウキ",,-1,-1,0,-1,5,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/06/07 2:00:00",96 +5004,"《空島繋ぐ架け橋》レイン",,-1,-1,0,-1,5,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/06/07 2:00:00",95 +5005,"《陽光香る笑み》アルゴ",,-1,-1,0,-1,5,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/06/07 2:00:00",94 +10000,"《黒の剣士》キリト",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2020/12/01 7:00:00","2021/12/14 2:00:00",37 +10001,"《一条の閃光》アスナ",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2020/12/01 7:00:00","2021/12/14 2:00:00",36 +10002,"《薫風の瞳》リーファ",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2020/12/01 7:00:00","2021/12/14 2:00:00",35 +10003,"《冥界の女神》シノン",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2020/12/01 7:00:00","2021/12/14 2:00:00",34 +10004,"《征野の刀工》リズベット",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2020/12/01 7:00:00","2021/12/14 2:00:00",33 +10005,"《可憐なる竜使い》シリカ",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2020/12/01 7:00:00","2021/12/14 2:00:00",32 +10006,"《真朱の武人》クライン",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2020/12/01 7:00:00","2021/12/14 2:00:00",31 +10007,"《剛腕の威風》エギル",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2020/12/01 7:00:00","2021/12/14 2:00:00",30 +10008,"《絶剣》ユウキ",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2020/12/01 7:00:00","2021/12/14 2:00:00",29 +10009,"《香染の諜報者》アルゴ",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2020/12/01 7:00:00","2021/12/14 2:00:00",28 +10010,"《人界の守り手》アリス",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2020/12/01 7:00:00","2021/12/14 2:00:00",27 +10011,"《精勤す修剣士》ユージオ",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2020/12/01 7:00:00","2021/12/14 2:00:00",26 +10012,"《多刀流の使い手》レイン",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2020/12/01 7:00:00","2021/12/14 2:00:00",25 +10013,"ブラックプレート",,1,-1,0,-1,-1,-1,False,0,0,0,0,0,"2021/01/01 7:00:00","2021/12/14 2:00:00",24 +10014,"エペ・ド・マルブル",,1,-1,0,-1,-1,-1,False,0,0,0,0,0,"2021/01/01 7:00:00","2021/12/14 2:00:00",23 +10015,"エリュシデータ",,5,-1,0,-1,-1,-1,False,0,0,0,0,0,"2021/01/01 7:00:00","2021/12/14 2:00:00",22 +10016,"アロンダイト",,5,-1,0,-1,-1,-1,False,0,0,0,0,0,"2021/01/01 7:00:00","2021/12/14 2:00:00",21 +10017,"スクレープ",,5,-1,0,-1,-1,-1,False,0,0,0,0,0,"2021/01/01 7:00:00","2021/12/14 2:00:00",20 +10018,"ムラマサ",,5,-1,0,-1,-1,-1,False,0,0,0,0,0,"2021/01/01 7:00:00","2021/12/14 2:00:00",19 +10019,"ヴィズィー・ガジェット",,5,-1,0,-1,-1,-1,False,0,0,0,0,0,"2021/01/01 7:00:00","2021/12/14 2:00:00",18 +10020,"ヴェンデッタ",,5,-1,0,-1,-1,-1,False,0,0,0,0,0,"2021/01/01 7:00:00","2021/12/14 2:00:00",17 +10021,"ジャイアント・タスク",,5,-1,0,-1,-1,-1,False,0,0,0,0,0,"2021/01/01 7:00:00","2021/12/14 2:00:00",16 +10022,"フェイルノート",,5,-1,0,-1,-1,-1,False,0,0,0,0,0,"2021/01/01 7:00:00","2021/12/14 2:00:00",15 +10023,"MEBRカスタム",,5,-1,0,-1,-1,-1,False,0,0,0,0,0,"2021/01/01 7:00:00","2021/12/14 2:00:00",14 +10024,"スヴェル",,5,-1,0,-1,-1,-1,False,0,0,0,0,0,"2021/01/01 7:00:00","2021/12/14 2:00:00",13 +10025,"ヒーローコード:斬耐性",,-1,200000,0,-1,-1,-1,False,0,0,0,0,0,"2021/01/01 7:00:00","2021/12/14 2:00:00",12 +10026,"ヒーローコード:打耐性",,-1,200000,0,-1,-1,-1,False,0,0,0,0,0,"2021/01/01 7:00:00","2021/12/14 2:00:00",11 +10027,"ヒーローコード:突耐性",,-1,200000,0,-1,-1,-1,False,0,0,0,0,0,"2021/01/01 7:00:00","2021/12/14 2:00:00",10 +10028,"ヒーローコード:火耐性",,-1,200000,0,-1,-1,-1,False,0,0,0,0,0,"2021/01/01 7:00:00","2021/12/14 2:00:00",9 +10029,"ヒーローコード:水耐性",,-1,200000,0,-1,-1,-1,False,0,0,0,0,0,"2021/01/01 7:00:00","2021/12/14 2:00:00",8 +10030,"ヒーローコード:風耐性",,-1,200000,0,-1,-1,-1,False,0,0,0,0,0,"2021/01/01 7:00:00","2021/12/14 2:00:00",7 +10031,"ヒーローコード:土耐性",,-1,200000,0,-1,-1,-1,False,0,0,0,0,0,"2021/01/01 7:00:00","2021/12/14 2:00:00",6 +10032,"ヒーローコード:聖耐性",,-1,200000,0,-1,-1,-1,False,0,0,0,0,0,"2021/01/01 7:00:00","2021/12/14 2:00:00",5 +10033,"ヒーローコード:闇耐性",,-1,200000,0,-1,-1,-1,False,0,0,0,0,0,"2021/01/01 7:00:00","2021/12/14 2:00:00",4 +10034,"ヒーローコード:毒無効",,-1,200000,0,-1,-1,-1,False,0,0,0,0,0,"2021/01/01 7:00:00","2021/12/14 2:00:00",3 +10035,"ヒーローコード:混乱無効",,-1,200000,0,-1,-1,-1,False,0,0,0,0,0,"2021/01/01 7:00:00","2021/12/14 2:00:00",2 +10036,"ヒーローコード:封印無効",,-1,200000,0,-1,-1,-1,False,0,0,0,0,0,"2021/01/01 7:00:00","2021/12/14 2:00:00",1 +10037,"ヒーローコード:地震無効",,-1,200000,0,-1,-1,-1,False,0,0,0,0,0,"2021/01/01 7:00:00","2021/12/14 2:00:00",0 +10038,"《黒の剣士》キリト",,-1,-1,0,-1,1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",47 +10039,"《一条の閃光》アスナ",,-1,-1,0,-1,1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",46 +10040,"《薫風の瞳》リーファ",,-1,-1,0,-1,1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",45 +10041,"《冥界の女神》シノン",,-1,-1,0,-1,1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",44 +10042,"《征野の刀工》リズベット",,-1,-1,0,-1,1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",43 +10043,"《可憐なる竜使い》シリカ",,-1,-1,0,-1,1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",42 +10044,"《真朱の武人》クライン",,-1,-1,0,-1,1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",41 +10045,"《剛腕の威風》エギル",,-1,-1,0,-1,1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",40 +10046,"《絶剣》ユウキ",,-1,-1,0,-1,1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",39 +10047,"《香染の諜報者》アルゴ",,-1,-1,0,-1,1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",38 +10048,"《人界の守り手》アリス",,-1,-1,0,-1,1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",37 +10049,"《精勤す修剣士》ユージオ",,-1,-1,0,-1,1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",36 +10050,"《多刀流の使い手》レイン",,-1,-1,0,-1,1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",35 +10051,"エリュシデータ",,-1,-1,0,-1,2,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",34 +10052,"アロンダイト",,-1,-1,0,-1,2,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",33 +10053,"スクレープ",,-1,-1,0,-1,2,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",32 +10054,"ムラマサ",,-1,-1,0,-1,2,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",31 +10055,"ウィズィー・ガジェット",,-1,-1,0,-1,2,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",30 +10056,"ヴェンデッタ",,-1,-1,0,-1,2,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",29 +10057,"ジャイアント・タスク",,-1,-1,0,-1,2,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",28 +10058,"フェイルノート",,-1,-1,0,-1,2,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",27 +10059,"MEBRマニアック",,-1,-1,0,-1,2,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",26 +10060,"スヴェル",,-1,-1,0,-1,2,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",25 +10061,"ブラックプレート",,1,-1,0,-1,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",24 +10062,"エペ・ド・マルブル",,1,-1,0,-1,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",23 +10063,"ヒーローコード:斬耐性",,-1,50000,0,-1,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",22 +10064,"ヒーローコード:打耐性",,-1,50000,0,-1,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",21 +10065,"ヒーローコード:突耐性",,-1,50000,0,-1,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",20 +10066,"ヒーローコード:火耐性",,-1,50000,0,-1,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",19 +10067,"ヒーローコード:水耐性",,-1,50000,0,-1,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",18 +10068,"ヒーローコード:風耐性",,-1,50000,0,-1,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",17 +10069,"ヒーローコード:土耐性",,-1,50000,0,-1,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",16 +10070,"ヒーローコード:聖耐性",,-1,50000,0,-1,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",15 +10071,"ヒーローコード:闇耐性",,-1,50000,0,-1,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",14 +10072,"ヒーローコード:毒無効",,-1,50000,0,-1,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",13 +10073,"ヒーローコード:混乱無効",,-1,50000,0,-1,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",12 +10074,"ヒーローコード:封印無効",,-1,50000,0,-1,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",11 +10075,"ヒーローコード:地震無効",,-1,50000,0,-1,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",10 +10076,"ウェポンコード:火属性",,-1,50000,0,-1,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",9 +10077,"ウェポンコード:水属性",,-1,50000,0,-1,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",8 +10078,"ウェポンコード:風属性",,-1,50000,0,-1,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",7 +10079,"ウェポンコード:土属性",,-1,50000,0,-1,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",6 +10080,"ウェポンコード:聖属性",,-1,50000,0,-1,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",5 +10081,"ウェポンコード:闇属性",,-1,50000,0,-1,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",4 +10082,"ウェポンコード:毒",,-1,50000,0,-1,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",3 +10083,"ウェポンコード:麻痺",,-1,50000,0,-1,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",2 +10084,"ウェポンコード:混乱",,-1,50000,0,-1,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",1 +10085,"ウェポンコード:封印",,-1,50000,0,-1,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/06/07 2:00:00",0 +15000,"《想い馳せる窓辺》キリト",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",64 +15001,"《妖精の決闘》キリト",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",63 +15002,"《愛の誓い》アスナ",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",62 +15003,"《水妖精の導き》アスナ",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",61 +15004,"《秘めた胸奥》リーファ",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",60 +15005,"《南風に吹かれて》リーファ",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",59 +15006,"《疾風迅雷》シノン",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",58 +15007,"《同朋の待望》シノン",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",57 +15008,"《信頼の口付け》リズベット",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",56 +15009,"《恋するお人形》リズベット",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",55 +15010,"《宙吊りプラント》シリカ",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",54 +15011,"《あどけない笑み》シリカ",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",53 +15012,"《武人の少憩》クライン",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",52 +15013,"《跳躍の托生》クライン",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",51 +15014,"《金剛なる体躯》エギル",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",50 +15015,"《勇猛な巨躯》エギル",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",49 +15016,"《君との内緒事》ユウキ",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",48 +15017,"《おすわりリトル》ユウキ",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",47 +15018,"《悪戯なオネーサン》アルゴ",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",46 +15019,"《隠された素顔》アルゴ",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",45 +15020,"《固守の決意》アリス",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",44 +15021,"《光の巫女》アリス",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",43 +15022,"《奇想の相棒》ユージオ",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",42 +15023,"《休剣の手入れ》ユージオ",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",41 +15024,"《赤面プラクティカ》レイン",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",40 +15025,"《胸躍る一歩》レイン",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",39 +15026,"《安らぎの我が家》キリト",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",38 +15027,"《穏やかな一時》アスナ",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",37 +15028,"《小径の花環》アスナ",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",36 +15029,"《妖精の戯れ》リーファ",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",35 +15030,"《寛ぎの我が家》リーファ",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",34 +15031,"《悪戯な視線》シノン",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",33 +15032,"《幸せの青い鳥》シノン",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",32 +15033,"《休日の談笑》リズベット",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",31 +15034,"《約束の花束》リズベット",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",30 +15035,"《おやつ・タイム!》シリカ",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",29 +15036,"《一輪の幼花》シリカ",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",28 +15037,"《さかさま花世界》ユウキ",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",27 +15038,"《秘密の甘邸》アルゴ",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",26 +15039,"《物思い耽る午後》アリス",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",25 +15040,"《花舞う昼下がり》ユージオ",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",24 +15041,"《晴天の君》レイン",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",23 +15042,"《虹架かる団らん》レイン",,-1,-1,0,-1,3,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",22 +15043,"エリュシデータ",,-1,-1,0,-1,2,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",21 +15044,"アロンダイト",,-1,-1,0,-1,2,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",20 +15045,"スクレープ",,-1,-1,0,-1,2,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",19 +15046,"ムラマサ",,-1,-1,0,-1,2,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",18 +15047,"ウィズィー・ガジェット",,-1,-1,0,-1,2,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",17 +15048,"ヴェンデッタ",,-1,-1,0,-1,2,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",16 +15049,"ジャイアント・タスク",,-1,-1,0,-1,2,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",15 +15050,"フェイルノート",,-1,-1,0,-1,2,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",14 +15051,"MEBRマニアック",,-1,-1,0,-1,2,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",13 +15052,"スヴェル",,-1,-1,0,-1,2,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",12 +15053,"プロパティアペンド",,-1,10000,0,-1,-1,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",8 +15054,"コードイレイサー:1",,-1,10000,0,-1,-1,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",7 +15055,"コードイレイサー:2",,-1,10000,0,-1,-1,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",6 +15056,"コードイレイサー:3",,-1,10000,0,-1,-1,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",5 +15057,"コードイレイサー:4",,-1,10000,0,-1,-1,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",4 +15058,"EXPメモリ《グラン》",,-1,-1,0,-1,1,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",11 +15059,"プラチナ《グラン》",,-1,-1,0,-1,1,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",10 +15060,"ヒーローレベルアペンド《1》",,1,-1,0,-1,-1,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",3 +15061,"ウェポンレベルアペンド《1》",,1,-1,0,-1,-1,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",2 +15062,"アクセサリレベルアペンド《1》",,1,-1,0,-1,-1,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",1 +15063,"サンキューギフト",,10,-1,0,-1,-1,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",0 +15064,"EXPメモリ",,-1,50000,0,-1,-1,-1,False,0,0,0,0,0,"2022/06/07 7:00:00","2030/01/01",9 +202901,"《伝説の始まり》キリト",,-1,-1,10006,3,-1,-1,False,0,0,0,0,0,"2020/12/08 7:00:00","2021/01/19 2:00:00",25 +202902,"《閃光の救世主》アスナ",,-1,-1,10006,3,-1,-1,False,0,0,0,0,0,"2020/12/08 7:00:00","2021/01/19 2:00:00",24 +202903,"《世界を結ぶ》キリト",,-1,-1,10006,3,-1,-1,False,0,0,0,0,0,"2020/12/08 7:00:00","2021/01/19 2:00:00",23 +202904,"《永遠を共に》アスナ",,-1,-1,10006,3,-1,-1,False,0,0,0,0,0,"2020/12/08 7:00:00","2021/01/19 2:00:00",22 +202905,"《見通せぬ未来》アリス",,-1,-1,10006,3,-1,-1,False,0,0,0,0,0,"2020/12/08 7:00:00","2021/01/19 2:00:00",21 +202906,"《決意を秘めて》イーディス",,-1,-1,10006,3,-1,-1,False,0,0,0,0,0,"2020/12/08 7:00:00","2021/01/19 2:00:00",20 +202907,"《立ち向かう意志》ロニエ",,-1,-1,10006,3,-1,-1,False,0,0,0,0,0,"2020/12/08 7:00:00","2021/01/19 2:00:00",19 +202908,"《惑う心》ティーゼ",,-1,-1,10006,3,-1,-1,False,0,0,0,0,0,"2020/12/08 7:00:00","2021/01/19 2:00:00",18 +202909,"《薄青の精鋭》エントキア",,-1,-1,10006,3,-1,-1,False,0,0,0,0,0,"2020/12/08 7:00:00","2021/01/19 2:00:00",17 +202910,"《萌ゆる深緑》ネルギウス",,-1,-1,10006,3,-1,-1,False,0,0,0,0,0,"2020/12/08 7:00:00","2021/01/19 2:00:00",16 +202911,"《華綻ぶはにかみ》シノン",,-1,-1,10006,10,-1,-1,False,0,0,0,0,0,"2020/12/08 7:00:00","2021/01/19 2:00:00",15 +202912,"《咲き誇る技巧》スズネ",,-1,-1,10006,10,-1,-1,False,0,0,0,0,0,"2020/12/08 7:00:00","2021/01/19 2:00:00",14 +202913,"EXPチップ《テラ》",,-1,-1,10006,3,-1,-1,False,0,0,0,0,0,"2020/12/08 7:00:00","2021/01/19 2:00:00",12 +202914,"ゴールド《テラ》",,-1,-1,10006,3,-1,-1,False,0,0,0,0,0,"2020/12/08 7:00:00","2021/01/19 2:00:00",9 +202915,"EXPチップ《キロ》",,-1,-1,10006,1,-1,-1,False,0,0,0,0,0,"2020/12/08 7:00:00","2021/01/19 2:00:00",13 +202916,"ゴールド《キロ》",,-1,-1,10006,1,-1,-1,False,0,0,0,0,0,"2020/12/08 7:00:00","2021/01/19 2:00:00",10 +202917,"EXPメモリ",,-1,-1,10006,5,-1,-1,False,0,0,0,0,0,"2020/12/08 7:00:00","2021/01/19 2:00:00",11 +202918,"プラチナ",,-1,-1,10006,5,-1,-1,False,0,0,0,0,0,"2020/12/08 7:00:00","2021/01/19 2:00:00",8 +202919,"プロパティアペンド",,-1,-1,10006,3,-1,-1,False,0,0,0,0,0,"2020/12/08 7:00:00","2021/01/19 2:00:00",7 +202920,"コードイレイサー:1",,-1,-1,10006,3,-1,-1,False,0,0,0,0,0,"2020/12/08 7:00:00","2021/01/19 2:00:00",6 +202921,"コードイレイサー:2",,-1,-1,10006,3,-1,-1,False,0,0,0,0,0,"2020/12/08 7:00:00","2021/01/19 2:00:00",5 +202922,"コードイレイサー:3",,-1,-1,10006,3,-1,-1,False,0,0,0,0,0,"2020/12/08 7:00:00","2021/01/19 2:00:00",4 +202923,"コードイレイサー:4",,-1,-1,10006,3,-1,-1,False,0,0,0,0,0,"2020/12/08 7:00:00","2021/01/19 2:00:00",3 +202924,"ヒーローレベルアペンド《1》",,-1,-1,10006,20,-1,-1,False,0,0,0,0,0,"2020/12/08 7:00:00","2021/01/19 2:00:00",2 +202925,"ウェポンレベルアペンド《1》",,-1,-1,10006,20,-1,-1,False,0,0,0,0,0,"2020/12/08 7:00:00","2021/01/19 2:00:00",1 +202926,"アクセサリレベルアペンド《1》",,-1,-1,10006,20,-1,-1,False,0,0,0,0,0,"2020/12/08 7:00:00","2021/01/19 2:00:00",0 +203001,"《男らしく見えて…》シノン",,-1,-1,10007,3,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",36 +203002,"《温かな距離》リズベット",,-1,-1,10007,3,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",35 +203003,"《目と目を合わせて》メディナ",,-1,-1,10007,3,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",34 +203004,"《幼妹の添寝》セルカ",,-1,-1,10007,3,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",33 +203005,"《人との接し方》シェータ",,-1,-1,10007,3,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",32 +203006,"《夜空に煌く星》キリト",,-1,-1,10007,3,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",31 +203007,"《死の天使》サトライザー",,-1,-1,10007,3,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",30 +203008,"《女神の笑顔》アスナ",,-1,-1,10007,3,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",29 +203009,"《再生の花》シリカ",,-1,-1,10007,3,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",28 +203010,"《憩いの場》サチ",,-1,-1,10007,3,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",27 +203011,"《まごころショコラティエ》ユウキ",,-1,-1,10007,10,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",26 +203012,"《愛受のはにかみ》キリト",,-1,-1,10007,3,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",25 +203013,"《甘い贈り物》アスナ",,-1,-1,10007,3,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",24 +203014,"《恋慕の微笑》リーファ",,-1,-1,10007,3,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",23 +203015,"《静穏の秘め事》シノン",,-1,-1,10007,3,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",22 +203016,"《親愛の鍛冶屋》リズベット",,-1,-1,10007,3,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",21 +203017,"《純情な恋路》シリカ",,-1,-1,10007,3,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",20 +203018,"《歓喜の照笑》クライン",,-1,-1,10007,3,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",19 +203019,"《義理堅い甘露》エギル",,-1,-1,10007,3,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",18 +203020,"《純粋な愛情》ユウキ",,-1,-1,10007,3,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",17 +203021,"《秘密の贈り物》アルゴ",,-1,-1,10007,3,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",16 +203022,"《裏腹な恋心》アリス",,-1,-1,10007,3,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",15 +203023,"《心届く笑顔》ユージオ",,-1,-1,10007,3,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",14 +203024,"《繋ぐ想い》レイン",,-1,-1,10007,3,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",37 +203025,"EXPチップ《テラ》",,-1,-1,10007,3,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",13 +203026,"ゴールド《テラ》",,-1,-1,10007,3,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",12 +203027,"EXPチップ《キロ》",,-1,-1,10007,1,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",11 +203028,"ゴールド《キロ》",,-1,-1,10007,1,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",10 +203029,"EXPメモリ",,-1,-1,10007,5,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",9 +203030,"プラチナ",,-1,-1,10007,5,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",8 +203031,"プロパティアペンド",,-1,-1,10007,3,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",7 +203032,"コードイレイサー:1",,-1,-1,10007,3,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",6 +203033,"コードイレイサー:2",,-1,-1,10007,3,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",5 +203034,"コードイレイサー:3",,-1,-1,10007,3,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",4 +203035,"コードイレイサー:4",,-1,-1,10007,3,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",3 +203036,"ヒーローレベルアペンド《1》",,-1,-1,10007,20,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",2 +203037,"ウェポンレベルアペンド《1》",,-1,-1,10007,20,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",1 +203038,"アクセサリレベルアペンド《1》",,-1,-1,10007,20,-1,-1,False,0,0,0,0,0,"2021/02/02 7:00:00","2021/03/23 2:00:00",0 +203201,"《抱きしめ幼愛》シリカ",,-1,-1,10008,3,-1,-1,False,0,0,0,0,0,"2021/03/30 7:00:00","2021/04/27 2:00:00",25 +203202,"《折り重なる感情》アリス",,-1,-1,10008,3,-1,-1,False,0,0,0,0,0,"2021/03/30 7:00:00","2021/04/27 2:00:00",24 +203203,"《憧れの先輩と》ロニエ",,-1,-1,10008,3,-1,-1,False,0,0,0,0,0,"2021/03/30 7:00:00","2021/04/27 2:00:00",23 +203204,"《悪戯な捕食者》フィゼル&リネル",,-1,-1,10008,3,-1,-1,False,0,0,0,0,0,"2021/03/30 7:00:00","2021/04/27 2:00:00",22 +203205,"《坊やの傍ら》ファナティオ",,-1,-1,10008,3,-1,-1,False,0,0,0,0,0,"2021/03/30 7:00:00","2021/04/27 2:00:00",21 +203206,"《攻略の最前線へ》アスナ",,-1,-1,10008,3,-1,-1,False,0,0,0,0,0,"2021/03/30 7:00:00","2021/04/27 2:00:00",20 +203207,"《切り拓く弾道》シノン",,-1,-1,10008,3,-1,-1,False,0,0,0,0,0,"2021/03/30 7:00:00","2021/04/27 2:00:00",19 +203208,"《浮遊城の救世主》キリト",,-1,-1,10008,3,-1,-1,False,0,0,0,0,0,"2021/03/30 7:00:00","2021/04/27 2:00:00",18 +203209,"《華麗なる妖精》リーファ",,-1,-1,10008,3,-1,-1,False,0,0,0,0,0,"2021/03/30 7:00:00","2021/04/27 2:00:00",17 +203210,"《鍛冶屋の恋心》リズベット",,-1,-1,10008,3,-1,-1,False,0,0,0,0,0,"2021/03/30 7:00:00","2021/04/27 2:00:00",16 +203211,"《異界の女神》アスナ",,-1,-1,10008,10,-1,-1,False,0,0,0,0,0,"2021/03/30 7:00:00","2021/04/27 2:00:00",15 +203213,"EXPチップ《キロ》",,-1,-1,10008,1,-1,-1,False,0,0,0,0,0,"2021/03/30 7:00:00","2021/04/27 2:00:00",13 +203214,"EXPチップ《テラ》",,-1,-1,10008,3,-1,-1,False,0,0,0,0,0,"2021/03/30 7:00:00","2021/04/27 2:00:00",12 +203215,"EXPメモリ",,-1,-1,10008,5,-1,-1,False,0,0,0,0,0,"2021/03/30 7:00:00","2021/04/27 2:00:00",11 +203216,"ゴールド《キロ》",,-1,-1,10008,1,-1,-1,False,0,0,0,0,0,"2021/03/30 7:00:00","2021/04/27 2:00:00",10 +203217,"ゴールド《テラ》",,-1,-1,10008,3,-1,-1,False,0,0,0,0,0,"2021/03/30 7:00:00","2021/04/27 2:00:00",9 +203218,"プラチナ",,-1,-1,10008,5,-1,-1,False,0,0,0,0,0,"2021/03/30 7:00:00","2021/04/27 2:00:00",8 +203219,"プロパティアペンド",,-1,-1,10008,3,-1,-1,False,0,0,0,0,0,"2021/03/30 7:00:00","2021/04/27 2:00:00",7 +203220,"コードイレイサー:1",,-1,-1,10008,3,-1,-1,False,0,0,0,0,0,"2021/03/30 7:00:00","2021/04/27 2:00:00",6 +203221,"コードイレイサー:2",,-1,-1,10008,3,-1,-1,False,0,0,0,0,0,"2021/03/30 7:00:00","2021/04/27 2:00:00",5 +203222,"コードイレイサー:3",,-1,-1,10008,3,-1,-1,False,0,0,0,0,0,"2021/03/30 7:00:00","2021/04/27 2:00:00",4 +203223,"コードイレイサー:4",,-1,-1,10008,3,-1,-1,False,0,0,0,0,0,"2021/03/30 7:00:00","2021/04/27 2:00:00",3 +203224,"ヒーローレベルアペンド《1》",,-1,-1,10008,20,-1,-1,False,0,0,0,0,0,"2021/03/30 7:00:00","2021/04/27 2:00:00",2 +203225,"ウェポンレベルアペンド《1》",,-1,-1,10008,20,-1,-1,False,0,0,0,0,0,"2021/03/30 7:00:00","2021/04/27 2:00:00",1 +203226,"アクセサリレベルアペンド《1》",,-1,-1,10008,20,-1,-1,False,0,0,0,0,0,"2021/03/30 7:00:00","2021/04/27 2:00:00",0 +203301,"《愛情の眼差し》アスナ",,-1,-1,10009,3,-1,-1,False,0,0,0,0,0,"2021/04/27 7:00:00","2021/06/08 2:00:00",25 +203302,"《親愛なる兄と》リーファ",,-1,-1,10009,3,-1,-1,False,0,0,0,0,0,"2021/04/27 7:00:00","2021/06/08 2:00:00",24 +203303,"《尊敬する先輩》ティーゼ",,-1,-1,10009,3,-1,-1,False,0,0,0,0,0,"2021/04/27 7:00:00","2021/06/08 2:00:00",23 +203304,"《流麗なる華》ソルティリーナ",,-1,-1,10009,3,-1,-1,False,0,0,0,0,0,"2021/04/27 7:00:00","2021/06/08 2:00:00",22 +203305,"《内に潜む想い》カーディナル",,-1,-1,10009,3,-1,-1,False,0,0,0,0,0,"2021/04/27 7:00:00","2021/06/08 2:00:00",21 +203306,"《寄り添う親友》ユージオ",,-1,-1,10009,3,-1,-1,False,0,0,0,0,0,"2021/04/27 7:00:00","2021/06/08 2:00:00",20 +203307,"《剣に宿る絆》キリト",,-1,-1,10009,3,-1,-1,False,0,0,0,0,0,"2021/04/27 7:00:00","2021/06/08 2:00:00",19 +203308,"《ショート・ブレイク》シノン",,-1,-1,10009,3,-1,-1,False,0,0,0,0,0,"2021/04/27 7:00:00","2021/06/08 2:00:00",18 +203309,"《剣姫の願い》ユウキ",,-1,-1,10009,3,-1,-1,False,0,0,0,0,0,"2021/04/27 7:00:00","2021/06/08 2:00:00",17 +203310,"《栁絮の守護》コハル",,-1,-1,10009,3,-1,-1,False,0,0,0,0,0,"2021/04/27 7:00:00","2021/06/08 2:00:00",16 +203312,"《マスターの贈り物》リコ",,-1,-1,10009,10,-1,-1,False,0,0,0,0,0,"2021/04/27 7:00:00","2021/06/08 2:00:00",14 +203313,"EXPチップ《キロ》",,-1,-1,10009,1,-1,-1,False,0,0,0,0,0,"2021/04/27 7:00:00","2021/06/08 2:00:00",13 +203314,"EXPチップ《テラ》",,-1,-1,10009,3,-1,-1,False,0,0,0,0,0,"2021/04/27 7:00:00","2021/06/08 2:00:00",12 +203315,"EXPメモリ",,-1,-1,10009,5,-1,-1,False,0,0,0,0,0,"2021/04/27 7:00:00","2021/06/08 2:00:00",11 +203316,"ゴールド《キロ》",,-1,-1,10009,1,-1,-1,False,0,0,0,0,0,"2021/04/27 7:00:00","2021/06/08 2:00:00",10 +203317,"ゴールド《テラ》",,-1,-1,10009,3,-1,-1,False,0,0,0,0,0,"2021/04/27 7:00:00","2021/06/08 2:00:00",9 +203318,"プラチナ",,-1,-1,10009,5,-1,-1,False,0,0,0,0,0,"2021/04/27 7:00:00","2021/06/08 2:00:00",8 +203319,"プロパティアペンド",,-1,-1,10009,3,-1,-1,False,0,0,0,0,0,"2021/04/27 7:00:00","2021/06/08 2:00:00",7 +203320,"コードイレイサー:1",,-1,-1,10009,3,-1,-1,False,0,0,0,0,0,"2021/04/27 7:00:00","2021/06/08 2:00:00",6 +203321,"コードイレイサー:2",,-1,-1,10009,3,-1,-1,False,0,0,0,0,0,"2021/04/27 7:00:00","2021/06/08 2:00:00",5 +203322,"コードイレイサー:3",,-1,-1,10009,3,-1,-1,False,0,0,0,0,0,"2021/04/27 7:00:00","2021/06/08 2:00:00",4 +203323,"コードイレイサー:4",,-1,-1,10009,3,-1,-1,False,0,0,0,0,0,"2021/04/27 7:00:00","2021/06/08 2:00:00",3 +203324,"ヒーローレベルアペンド《1》",,-1,-1,10009,20,-1,-1,False,0,0,0,0,0,"2021/04/27 7:00:00","2021/06/08 2:00:00",2 +203325,"ウェポンレベルアペンド《1》",,-1,-1,10009,20,-1,-1,False,0,0,0,0,0,"2021/04/27 7:00:00","2021/06/08 2:00:00",1 +203326,"アクセサリレベルアペンド《1》",,-1,-1,10009,20,-1,-1,False,0,0,0,0,0,"2021/04/27 7:00:00","2021/06/08 2:00:00",0 +203501,"《真》インフィニティ・モーメント",,-1,-1,10010,3,-1,-1,False,0,0,0,0,0,"2021/06/08 7:00:00","2021/07/27 2:00:00",31 +203502,"《裏》ホロウ・フラグメント",,-1,-1,10010,3,-1,-1,False,0,0,0,0,0,"2021/06/08 7:00:00","2021/07/27 2:00:00",30 +203503,"《絆》ロスト・ソング",,-1,-1,10010,3,-1,-1,False,0,0,0,0,0,"2021/06/08 7:00:00","2021/07/27 2:00:00",29 +203504,"《心》ホロウ・リアリゼーション",,-1,-1,10010,3,-1,-1,False,0,0,0,0,0,"2021/06/08 7:00:00","2021/07/27 2:00:00",28 +203505,"《銃》フェイタル・バレット",,-1,-1,10010,3,-1,-1,False,0,0,0,0,0,"2021/06/08 7:00:00","2021/07/27 2:00:00",27 +203506,"《抱きしめ幼愛》シリカ",,-1,-1,10010,3,-1,-1,False,0,0,0,0,0,"2021/06/08 7:00:00","2021/07/27 2:00:00",26 +203507,"《折り重なる感情》アリス",,-1,-1,10010,3,-1,-1,False,0,0,0,0,0,"2021/06/08 7:00:00","2021/07/27 2:00:00",25 +203508,"《憧れの先輩と》ロニエ",,-1,-1,10010,3,-1,-1,False,0,0,0,0,0,"2021/06/08 7:00:00","2021/07/27 2:00:00",24 +203509,"《悪戯な捕食者》フィゼル&リネル",,-1,-1,10010,3,-1,-1,False,0,0,0,0,0,"2021/06/08 7:00:00","2021/07/27 2:00:00",23 +203510,"《坊やの傍ら》ファナティオ",,-1,-1,10010,3,-1,-1,False,0,0,0,0,0,"2021/06/08 7:00:00","2021/07/27 2:00:00",22 +203511,"《攻略の最前線へ》アスナ",,-1,-1,10010,3,-1,-1,False,0,0,0,0,0,"2021/06/08 7:00:00","2021/07/27 2:00:00",21 +203512,"《切り拓く弾道》シノン",,-1,-1,10010,3,-1,-1,False,0,0,0,0,0,"2021/06/08 7:00:00","2021/07/27 2:00:00",20 +203513,"《浮遊城の救世主》キリト",,-1,-1,10010,3,-1,-1,False,0,0,0,0,0,"2021/06/08 7:00:00","2021/07/27 2:00:00",19 +203514,"《華麗なる妖精》リーファ",,-1,-1,10010,3,-1,-1,False,0,0,0,0,0,"2021/06/08 7:00:00","2021/07/27 2:00:00",18 +203515,"《鍛冶屋の恋心》リズベット",,-1,-1,10010,3,-1,-1,False,0,0,0,0,0,"2021/06/08 7:00:00","2021/07/27 2:00:00",17 +203516,"《クロス・イメージ》キリト",,-1,-1,10010,10,-1,-1,False,0,0,0,0,0,"2021/06/08 7:00:00","2021/07/27 2:00:00",16 +203517,"《ふたとせ廻りて》アリス",,-1,-1,10010,10,-1,-1,False,0,0,0,0,0,"2021/06/08 7:00:00","2021/07/27 2:00:00",15 +203518,"《異界の女神》アスナ",,-1,-1,10010,10,-1,-1,False,0,0,0,0,0,"2021/06/08 7:00:00","2021/07/27 2:00:00",14 +203519,"EXPチップ《キロ》",,-1,-1,10010,1,-1,-1,False,0,0,0,0,0,"2021/06/08 7:00:00","2021/07/27 2:00:00",13 +203520,"EXPチップ《テラ》",,-1,-1,10010,3,-1,-1,False,0,0,0,0,0,"2021/06/08 7:00:00","2021/07/27 2:00:00",12 +203521,"EXPメモリ",,-1,-1,10010,5,-1,-1,False,0,0,0,0,0,"2021/06/08 7:00:00","2021/07/27 2:00:00",11 +203522,"ゴールド《キロ》",,-1,-1,10010,1,-1,-1,False,0,0,0,0,0,"2021/06/08 7:00:00","2021/07/27 2:00:00",10 +203523,"ゴールド《テラ》",,-1,-1,10010,3,-1,-1,False,0,0,0,0,0,"2021/06/08 7:00:00","2021/07/27 2:00:00",9 +203524,"プラチナ",,-1,-1,10010,5,-1,-1,False,0,0,0,0,0,"2021/06/08 7:00:00","2021/07/27 2:00:00",8 +203525,"プロパティアペンド",,-1,-1,10010,3,-1,-1,False,0,0,0,0,0,"2021/06/08 7:00:00","2021/07/27 2:00:00",7 +203526,"コードイレイサー:1",,-1,-1,10010,3,-1,-1,False,0,0,0,0,0,"2021/06/08 7:00:00","2021/07/27 2:00:00",6 +203527,"コードイレイサー:2",,-1,-1,10010,3,-1,-1,False,0,0,0,0,0,"2021/06/08 7:00:00","2021/07/27 2:00:00",5 +203528,"コードイレイサー:3",,-1,-1,10010,3,-1,-1,False,0,0,0,0,0,"2021/06/08 7:00:00","2021/07/27 2:00:00",4 +203529,"コードイレイサー:4",,-1,-1,10010,3,-1,-1,False,0,0,0,0,0,"2021/06/08 7:00:00","2021/07/27 2:00:00",3 +203530,"ヒーローレベルアペンド《1》",,-1,-1,10010,20,-1,-1,False,0,0,0,0,0,"2021/06/08 7:00:00","2021/07/27 2:00:00",2 +203531,"ウェポンレベルアペンド《1》",,-1,-1,10010,20,-1,-1,False,0,0,0,0,0,"2021/06/08 7:00:00","2021/07/27 2:00:00",1 +203532,"アクセサリレベルアペンド《1》",,-1,-1,10010,20,-1,-1,False,0,0,0,0,0,"2021/06/08 7:00:00","2021/07/27 2:00:00",0 +203601,"《スイカ・サースト》アスナ",,-1,-1,10011,3,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",42 +203602,"《スイカ・ストライク》リズベット",,-1,-1,10011,3,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",41 +203603,"《不可避の魔球》エギル",,-1,-1,10011,3,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",40 +203604,"《スイカ・セーバー》アリス",,-1,-1,10011,3,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",39 +203605,"《空砲に駆けて》サチ",,-1,-1,10011,3,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",38 +203606,"《湖上の大輪》リーファ",,-1,-1,10011,3,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",37 +203607,"《海中散策》アスナ",,-1,-1,10011,3,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",36 +203608,"《海上遊歩》リズベット",,-1,-1,10011,3,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",35 +203609,"《海飛沫ガード》ロニエ",,-1,-1,10011,3,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",34 +203610,"《海飛沫アタック》ティーゼ",,-1,-1,10011,3,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",33 +203611,"《灼熱!アタックガール》リーファ",,-1,-1,10011,10,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",32 +203612,"《私の現実》シノン",,-1,-1,10011,10,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",31 +203613,"《マスターの贈り物》リコ",,-1,-1,10011,10,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",30 +203614,"《異世界の英雄》キリト",,-1,-1,10011,3,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",29 +203615,"《彼岸花の君》メディナ",,-1,-1,10011,3,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",28 +203616,"《柔靭なる剣士》ソルティリーナ",,-1,-1,10011,3,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",27 +203617,"《図書室の賢者》カーディナル",,-1,-1,10011,3,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",26 +203618,"《絶対的支配者》アドミニストレータ",,-1,-1,10011,3,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",25 +203619,"《愛の忠臣》チュデルキン",,-1,-1,10011,3,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",24 +203620,"《愛情の眼差し》アスナ",,-1,-1,10011,3,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",23 +203621,"《親愛なる兄と》リーファ",,-1,-1,10011,3,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",22 +203622,"《尊敬する先輩》ティーゼ",,-1,-1,10011,3,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",21 +203623,"《流麗なる華》ソルティリーナ",,-1,-1,10011,3,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",20 +203624,"《内に潜む想い》カーディナル",,-1,-1,10011,3,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",19 +203625,"《寄り添う親友》ユージオ",,-1,-1,10011,3,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",18 +203626,"《剣に宿る絆》キリト",,-1,-1,10011,3,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",17 +203627,"《ショート・ブレイク》シノン",,-1,-1,10011,3,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",16 +203628,"《剣姫の願い》ユウキ",,-1,-1,10011,3,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",15 +203629,"《栁絮の守護》コハル",,-1,-1,10011,3,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",14 +203630,"EXPチップ《キロ》",,-1,-1,10011,1,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",13 +203631,"EXPチップ《テラ》",,-1,-1,10011,3,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",12 +203632,"EXPメモリ",,-1,-1,10011,5,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",11 +203633,"ゴールド《キロ》",,-1,-1,10011,1,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",10 +203634,"ゴールド《テラ》",,-1,-1,10011,3,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",9 +203635,"プラチナ",,-1,-1,10011,5,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",8 +203636,"プロパティアペンド",,-1,-1,10011,3,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",7 +203637,"コードイレイサー:1",,-1,-1,10011,3,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",6 +203638,"コードイレイサー:2",,-1,-1,10011,3,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",5 +203639,"コードイレイサー:3",,-1,-1,10011,3,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",4 +203640,"コードイレイサー:4",,-1,-1,10011,3,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",3 +203641,"ヒーローレベルアペンド《1》",,-1,-1,10011,20,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",2 +203642,"ウェポンレベルアペンド《1》",,-1,-1,10011,20,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",1 +203643,"アクセサリレベルアペンド《1》",,-1,-1,10011,20,-1,-1,False,0,0,0,0,0,"2021/07/27 7:00:00","2021/09/28 2:00:00",0 +203701,"BET:アスナ",,-1,-1,10012,1,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",33 +203702,"BET:リーファ",,-1,-1,10012,1,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",32 +203703,"BET:シノン",,-1,-1,10012,1,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",31 +203704,"BET:シリカ",,-1,-1,10012,1,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",30 +203705,"BET:アリス",,-1,-1,10012,1,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",29 +203706,"《心弛の寝息》リーファ",,-1,-1,10012,3,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",28 +203707,"《安堵の就眠》シノン",,-1,-1,10012,3,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",27 +203708,"《ご馳走ドリーム》セブン",,-1,-1,10012,3,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",26 +203709,"《胸の旋律》レイン",,-1,-1,10012,3,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",25 +203710,"《和みの理趣》プレミア",,-1,-1,10012,3,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",24 +203711,"《慥かな腕枕》ティア",,-1,-1,10012,3,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",23 +203712,"《兎の歓待》リズベット&シリカ",,-1,-1,10012,3,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",22 +203713,"《博打ごっこ》ストレア",,-1,-1,10012,3,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",21 +203714,"《0と1の狭間で》シリカ",,-1,-1,10012,10,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",20 +203715,"《愛する人へ》アスナ",,-1,-1,10012,3,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",19 +203716,"《大切な家族》リーファ",,-1,-1,10012,3,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",18 +203717,"《強さを知って》シノン",,-1,-1,10012,3,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",17 +203718,"《この温度を君に》リズベット",,-1,-1,10012,3,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",16 +203719,"《今度はあたしが》シリカ",,-1,-1,10012,3,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",15 +203720,"《傍らの相棒》ユージオ",,-1,-1,10012,3,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",14 +203721,"EXPチップ《キロ》",,-1,-1,10012,1,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",13 +203722,"EXPチップ《テラ》",,-1,-1,10012,3,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",12 +203723,"EXPメモリ",,-1,-1,10012,5,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",11 +203724,"ゴールド《キロ》",,-1,-1,10012,1,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",10 +203725,"ゴールド《テラ》",,-1,-1,10012,3,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",9 +203726,"プラチナ",,-1,-1,10012,5,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",8 +203727,"プロパティアペンド",,-1,-1,10012,3,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",7 +203728,"コードイレイサー:1",,-1,-1,10012,3,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",6 +203729,"コードイレイサー:2",,-1,-1,10012,3,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",5 +203730,"コードイレイサー:3",,-1,-1,10012,3,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",4 +203731,"コードイレイサー:4",,-1,-1,10012,3,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",3 +203732,"ヒーローレベルアペンド《1》",,-1,-1,10012,20,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",2 +203733,"ウェポンレベルアペンド《1》",,-1,-1,10012,20,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",1 +203734,"アクセサリレベルアペンド《1》",,-1,-1,10012,20,-1,-1,False,0,0,0,0,0,"2021/09/28 7:00:00","2021/11/02 2:00:00",0 +203801,"《深まる夜》アスナ",,-1,-1,10013,3,-1,-1,False,0,0,0,0,0,"2021/11/02 7:00:00","2021/12/14 2:00:00",28 +203802,"《消えゆく憂心》リズベット",,-1,-1,10013,3,-1,-1,False,0,0,0,0,0,"2021/11/02 7:00:00","2021/12/14 2:00:00",27 +203803,"《抱擁の慕情》シリカ",,-1,-1,10013,3,-1,-1,False,0,0,0,0,0,"2021/11/02 7:00:00","2021/12/14 2:00:00",26 +203804,"《たまゆらの逢瀬》アルゴ",,-1,-1,10013,3,-1,-1,False,0,0,0,0,0,"2021/11/02 7:00:00","2021/12/14 2:00:00",25 +203805,"《溶融の招》キズメル",,-1,-1,10013,3,-1,-1,False,0,0,0,0,0,"2021/11/02 7:00:00","2021/12/14 2:00:00",24 +203806,"《泡沫の安臥》サチ",,-1,-1,10013,3,-1,-1,False,0,0,0,0,0,"2021/11/02 7:00:00","2021/12/14 2:00:00",23 +203807,"《打ち明ける閨》エイジ",,-1,-1,10013,3,-1,-1,False,0,0,0,0,0,"2021/11/02 7:00:00","2021/12/14 2:00:00",22 +203808,"《浮きたつ真意》ユナ",,-1,-1,10013,3,-1,-1,False,0,0,0,0,0,"2021/11/02 7:00:00","2021/12/14 2:00:00",21 +203809,"《変わらないもの》リズベット",,-1,-1,10013,10,-1,-1,False,0,0,0,0,0,"2021/11/02 7:00:00","2021/12/14 2:00:00",20 +203810,"《整合騎士長》ベルクーリ",,-1,-1,10013,3,-1,-1,False,0,0,0,0,0,"2021/11/02 7:00:00","2021/12/14 2:00:00",19 +203811,"《天穿の光》ファナティオ",,-1,-1,10013,3,-1,-1,False,0,0,0,0,0,"2021/11/02 7:00:00","2021/12/14 2:00:00",18 +203812,"《焔の弓騎士》デュソルバート",,-1,-1,10013,3,-1,-1,False,0,0,0,0,0,"2021/11/02 7:00:00","2021/12/14 2:00:00",17 +203813,"《師への敬愛》エルドリエ",,-1,-1,10013,3,-1,-1,False,0,0,0,0,0,"2021/11/02 7:00:00","2021/12/14 2:00:00",16 +203814,"《黒百合の一閃》シェータ",,-1,-1,10013,3,-1,-1,False,0,0,0,0,0,"2021/11/02 7:00:00","2021/12/14 2:00:00",15 +203815,"《失くした片翼》レンリ",,-1,-1,10013,3,-1,-1,False,0,0,0,0,0,"2021/11/02 7:00:00","2021/12/14 2:00:00",14 +203816,"EXPチップ《キロ》",,-1,-1,10013,1,-1,-1,False,0,0,0,0,0,"2021/11/02 7:00:00","2021/12/14 2:00:00",13 +203817,"EXPチップ《テラ》",,-1,-1,10013,3,-1,-1,False,0,0,0,0,0,"2021/11/02 7:00:00","2021/12/14 2:00:00",12 +203818,"EXPメモリ",,-1,-1,10013,5,-1,-1,False,0,0,0,0,0,"2021/11/02 7:00:00","2021/12/14 2:00:00",11 +203819,"ゴールド《キロ》",,-1,-1,10013,1,-1,-1,False,0,0,0,0,0,"2021/11/02 7:00:00","2021/12/14 2:00:00",10 +203820,"ゴールド《テラ》",,-1,-1,10013,3,-1,-1,False,0,0,0,0,0,"2021/11/02 7:00:00","2021/12/14 2:00:00",9 +203821,"プラチナ",,-1,-1,10013,5,-1,-1,False,0,0,0,0,0,"2021/11/02 7:00:00","2021/12/14 2:00:00",8 +203822,"プロパティアペンド",,-1,-1,10013,3,-1,-1,False,0,0,0,0,0,"2021/11/02 7:00:00","2021/12/14 2:00:00",7 +203823,"コードイレイサー:1",,-1,-1,10013,3,-1,-1,False,0,0,0,0,0,"2021/11/02 7:00:00","2021/12/14 2:00:00",6 +203824,"コードイレイサー:2",,-1,-1,10013,3,-1,-1,False,0,0,0,0,0,"2021/11/02 7:00:00","2021/12/14 2:00:00",5 +203825,"コードイレイサー:3",,-1,-1,10013,3,-1,-1,False,0,0,0,0,0,"2021/11/02 7:00:00","2021/12/14 2:00:00",4 +203826,"コードイレイサー:4",,-1,-1,10013,3,-1,-1,False,0,0,0,0,0,"2021/11/02 7:00:00","2021/12/14 2:00:00",3 +203827,"ヒーローレベルアペンド《1》",,-1,-1,10013,20,-1,-1,False,0,0,0,0,0,"2021/11/02 7:00:00","2021/12/14 2:00:00",2 +203828,"ウェポンレベルアペンド《1》",,-1,-1,10013,20,-1,-1,False,0,0,0,0,0,"2021/11/02 7:00:00","2021/12/14 2:00:00",1 +203829,"アクセサリレベルアペンド《1》",,-1,-1,10013,20,-1,-1,False,0,0,0,0,0,"2021/11/02 7:00:00","2021/12/14 2:00:00",0 +204001,"《聖夜は白く》アスナ",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",51 +204002,"《想望の家路》リーファ",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",50 +204003,"《宿り木の下で》シノン",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",49 +204004,"《星に願いを》アリス",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",48 +204005,"《駈走る雪空》ユージオ",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",47 +204006,"《共に歩む時》キリト",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",46 +204007,"《立ち上がる時》アスナ",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",45 +204008,"《心繋がる時》リーファ",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",44 +204009,"《意志示す時》シノン",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",43 +204010,"《背中押す時》ユウキ",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",42 +204011,"《蒼穹を駆ける英傑》ユウキ",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",41 +204012,"《万天に通ずる鼠》アルゴ",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",40 +204013,"《心通う邂逅》アリス",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",39 +204014,"《かの剣技の継承者》ユージオ",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",38 +204015,"《華綻ぶはにかみ》シノン",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",37 +204016,"《サンタさん大作戦!》ユウキ",,-1,-1,10014,10,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",36 +204017,"《誇り高き騎士》アリス",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",35 +204018,"《姉想う祈り》セルカ",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",34 +204019,"《正しき道》ロニエ",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",33 +204020,"《受け継ぐ希望》ティーゼ",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",32 +204021,"《無邪気な毒牙》フィゼル",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",31 +204022,"《穏やかな殺戮》リネル",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",30 +204023,"《咲き誇る技巧》スズネ",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",29 +204024,"《攻略者憩う店》テリア",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",28 +204025,"《無邪気な愛》ストレア",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",27 +204026,"《宝物はここに》フィリア",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",26 +204027,"《七色の夢》レイン",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",25 +204028,"《真の繋がり》セブン",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",24 +204029,"《芽吹きの時》プレミア",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",23 +204030,"《私の証明》クレハ",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",22 +204031,"《無冠の女王》ツェリスカ",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",21 +204032,"《特別な存在》イツキ",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",20 +204033,"《踏み出す一歩》コハル",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",19 +204034,"EXPチップ《キロ》",,-1,-1,10014,1,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",18 +204035,"EXPチップ《テラ》",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",17 +204036,"EXPメモリ",,-1,-1,10014,5,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",16 +204037,"ゴールド《キロ》",,-1,-1,10014,1,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",15 +204038,"ゴールド《テラ》",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",14 +204039,"プラチナ",,-1,-1,10014,5,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",13 +204040,"プロパティアペンド",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",12 +204041,"コードイレイサー:1",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",11 +204042,"コードイレイサー:2",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",10 +204043,"コードイレイサー:3",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",9 +204044,"コードイレイサー:4",,-1,-1,10014,3,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",8 +204045,"ウェポンコード:ボスキラー",,-1,-1,10014,10,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",7 +204046,"ウェポンコード:Mobキラー",,-1,-1,10014,10,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",6 +204047,"ウェポンコード:HP最大時強化",,-1,-1,10014,10,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",5 +204048,"ウェポンコード:HP低下時強化",,-1,-1,10014,10,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",4 +204049,"ウェポンコード:弱点攻撃強化",,-1,-1,10014,10,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",3 +204050,"ヒーローレベルアペンド《1》",,-1,-1,10014,20,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",2 +204051,"ウェポンレベルアペンド《1》",,-1,-1,10014,20,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",1 +204052,"アクセサリレベルアペンド《1》",,-1,-1,10014,20,-1,-1,False,0,0,0,0,0,"2021/12/14 7:00:00","2022/01/25 2:00:00",0 +204101,"《寧静の眠り》ユウキ",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",46 +204102,"《高まる体温》アリス",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",45 +204103,"《分け合う心》ユージオ",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",44 +204104,"《癒しの懐抱》ストレア",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",43 +204105,"《臥房の什宝》フィリア",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",42 +204106,"《夢の浮橋》イツキ",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",41 +204107,"《浮雲の温度》ツェリスカ",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",40 +204108,"《色葉散らず》クレハ",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",39 +204109,"《いとしのショコラティエ》アスナ",,-1,-1,10015,10,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",38 +204110,"《まごころショコラティエ》ユウキ",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",37 +204111,"《伝説の始まり》キリト",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",36 +204112,"《閃光の救世主》アスナ",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",35 +204113,"《愛受のはにかみ》キリト",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",34 +204114,"《甘い贈り物》アスナ",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",33 +204115,"《恋慕の微笑》リーファ",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",32 +204116,"《静穏の秘め事》シノン",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",31 +204117,"《親愛の鍛冶屋》リズベット",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",30 +204118,"《純情な恋路》シリカ",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",29 +204119,"《歓喜の照笑》クライン",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",28 +204120,"《義理堅い甘露》エギル",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",27 +204121,"《純粋な愛情》ユウキ",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",26 +204122,"《秘密の贈り物》アルゴ",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",25 +204123,"《裏腹な恋心》アリス",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",24 +204124,"《心届く笑顔》ユージオ",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",23 +204125,"《繋ぐ想い》レイン",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",22 +204126,"《世界を結ぶ》キリト",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",21 +204127,"《永遠を共に》アスナ",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",20 +204128,"《見通せぬ未来》アリス",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",19 +204129,"《決意を秘めて》イーディス",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",18 +204130,"《立ち向かう意志》ロニエ",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",17 +204131,"《惑う心》ティーゼ",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",16 +204132,"《薄青の精鋭》エントキア",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",15 +204133,"《萌ゆる深緑》ネルギウス",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",14 +204134,"EXPチップ《キロ》",,-1,-1,10015,1,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",13 +204135,"EXPチップ《テラ》",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",12 +204136,"EXPメモリ",,-1,-1,10015,5,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",11 +204137,"ゴールド《キロ》",,-1,-1,10015,1,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",10 +204138,"ゴールド《テラ》",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",9 +204139,"プラチナ",,-1,-1,10015,5,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",8 +204140,"プロパティアペンド",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",7 +204141,"コードイレイサー:1",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",6 +204142,"コードイレイサー:2",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",5 +204143,"コードイレイサー:3",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",4 +204144,"コードイレイサー:4",,-1,-1,10015,3,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",3 +204145,"ヒーローレベルアペンド《1》",,-1,-1,10015,20,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",2 +204146,"ウェポンレベルアペンド《1》",,-1,-1,10015,20,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",1 +204147,"アクセサリレベルアペンド《1》",,-1,-1,10015,20,-1,-1,False,0,0,0,0,0,"2022/01/25 7:00:00","2022/03/08 2:00:00",0 +204201,"《受け止める軌跡》キリト",,-1,-1,10016,3,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",38 +204202,"《見つめる軌跡》アスナ",,-1,-1,10016,3,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",37 +204203,"《信じきる軌跡》リーファ",,-1,-1,10016,3,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",36 +204204,"《向き合う軌跡》シノン",,-1,-1,10016,3,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",35 +204205,"《確かめる軌跡》ユウキ",,-1,-1,10016,3,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",34 +204206,"《思い返す軌跡》アリス",,-1,-1,10016,3,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",33 +204207,"《安眠の廉》リズベット",,-1,-1,10016,3,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",32 +204208,"《守られる吉夢》シリカ",,-1,-1,10016,3,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",31 +204209,"《開かれる愁眉》ストレア",,-1,-1,10016,3,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",30 +204210,"《安らぎの香》フィリア",,-1,-1,10016,3,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",29 +204211,"《聞き澄ます愛吟》セブン",,-1,-1,10016,3,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",28 +204212,"《傾慕の子守歌》レイン",,-1,-1,10016,3,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",27 +204213,"《受ける薫陶》プレミア",,-1,-1,10016,3,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",26 +204214,"《異界の女神》アスナ",,-1,-1,10016,3,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",25 +204215,"《マスターの贈り物》リコ",,-1,-1,10016,3,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",24 +204216,"《男らしく見えて…》シノン",,-1,-1,10016,3,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",23 +204217,"《温かな距離》リズベット",,-1,-1,10016,3,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",22 +204218,"《目と目を合わせて》メディナ",,-1,-1,10016,3,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",21 +204219,"《幼妹の添寝》セルカ",,-1,-1,10016,3,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",20 +204220,"《人との接し方》シェータ",,-1,-1,10016,3,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",19 +204221,"《夜空に煌く星》キリト",,-1,-1,10016,3,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",18 +204222,"《死の天使》サトライザー",,-1,-1,10016,3,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",17 +204223,"《女神の笑顔》アスナ",,-1,-1,10016,3,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",16 +204224,"《再生の花》シリカ",,-1,-1,10016,3,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",15 +204225,"《憩いの場》サチ",,-1,-1,10016,3,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",14 +204226,"EXPチップ《キロ》",,-1,-1,10016,1,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",13 +204227,"EXPチップ《テラ》",,-1,-1,10016,3,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",12 +204228,"EXPメモリ",,-1,-1,10016,5,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",11 +204229,"ゴールド《キロ》",,-1,-1,10016,1,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",10 +204230,"ゴールド《テラ》",,-1,-1,10016,3,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",9 +204231,"プラチナ",,-1,-1,10016,5,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",8 +204232,"プロパティアペンド",,-1,-1,10016,3,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",7 +204233,"コードイレイサー:1",,-1,-1,10016,3,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",6 +204234,"コードイレイサー:2",,-1,-1,10016,3,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",5 +204235,"コードイレイサー:3",,-1,-1,10016,3,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",4 +204236,"コードイレイサー:4",,-1,-1,10016,3,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",3 +204237,"ヒーローレベルアペンド《1》",,-1,-1,10016,20,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",2 +204238,"ウェポンレベルアペンド《1》",,-1,-1,10016,20,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",1 +204239,"アクセサリレベルアペンド《1》",,-1,-1,10016,20,-1,-1,False,0,0,0,0,0,"2022/03/08 7:00:00","2022/04/26 2:00:00",0 +204301,"《目配せのしとね》アスナ",,-1,-1,10017,3,-1,-1,False,0,0,0,0,0,"2022/04/26 7:00:00","2022/06/07 2:00:00",32 +204302,"《不壊の縁故》リーファ",,-1,-1,10017,3,-1,-1,False,0,0,0,0,0,"2022/04/26 7:00:00","2022/06/07 2:00:00",31 +204303,"《解きほぐす内心》シノン",,-1,-1,10017,3,-1,-1,False,0,0,0,0,0,"2022/04/26 7:00:00","2022/06/07 2:00:00",30 +204304,"《東雲の契り》ユウキ",,-1,-1,10017,3,-1,-1,False,0,0,0,0,0,"2022/04/26 7:00:00","2022/06/07 2:00:00",29 +204305,"《密かな処方》アルゴ",,-1,-1,10017,3,-1,-1,False,0,0,0,0,0,"2022/04/26 7:00:00","2022/06/07 2:00:00",28 +204306,"《二人寝は繁く》アリス",,-1,-1,10017,3,-1,-1,False,0,0,0,0,0,"2022/04/26 7:00:00","2022/06/07 2:00:00",27 +204307,"《追懐の夜語り》ユージオ",,-1,-1,10017,3,-1,-1,False,0,0,0,0,0,"2022/04/26 7:00:00","2022/06/07 2:00:00",26 +204308,"《クロス・イメージ》キリト",,-1,-1,10017,3,-1,-1,False,0,0,0,0,0,"2022/04/26 7:00:00","2022/06/07 2:00:00",25 +204309,"《私の現実》シノン",,-1,-1,10017,10,-1,-1,False,0,0,0,0,0,"2022/04/26 7:00:00","2022/06/07 2:00:00",24 +204310,"《抱きしめ幼愛》シリカ",,-1,-1,10017,3,-1,-1,False,0,0,0,0,0,"2022/04/26 7:00:00","2022/06/07 2:00:00",23 +204311,"《折り重なる感情》アリス",,-1,-1,10017,3,-1,-1,False,0,0,0,0,0,"2022/04/26 7:00:00","2022/06/07 2:00:00",22 +204312,"《憧れの先輩と》ロニエ",,-1,-1,10017,3,-1,-1,False,0,0,0,0,0,"2022/04/26 7:00:00","2022/06/07 2:00:00",21 +204313,"《悪戯な捕食者》フィゼル&リネル",,-1,-1,10017,3,-1,-1,False,0,0,0,0,0,"2022/04/26 7:00:00","2022/06/07 2:00:00",20 +204314,"《坊やの傍ら》ファナティオ",,-1,-1,10017,3,-1,-1,False,0,0,0,0,0,"2022/04/26 7:00:00","2022/06/07 2:00:00",19 +204315,"《攻略の最前線へ》アスナ",,-1,-1,10017,3,-1,-1,False,0,0,0,0,0,"2022/04/26 7:00:00","2022/06/07 2:00:00",18 +204316,"《切り拓く弾道》シノン",,-1,-1,10017,3,-1,-1,False,0,0,0,0,0,"2022/04/26 7:00:00","2022/06/07 2:00:00",17 +204317,"《浮遊城の救世主》キリト",,-1,-1,10017,3,-1,-1,False,0,0,0,0,0,"2022/04/26 7:00:00","2022/06/07 2:00:00",16 +204318,"《華麗なる妖精》リーファ",,-1,-1,10017,3,-1,-1,False,0,0,0,0,0,"2022/04/26 7:00:00","2022/06/07 2:00:00",15 +204319,"《鍛冶屋の恋心》リズベット",,-1,-1,10017,3,-1,-1,False,0,0,0,0,0,"2022/04/26 7:00:00","2022/06/07 2:00:00",14 +204320,"EXPチップ《キロ》",,-1,-1,10017,1,-1,-1,False,0,0,0,0,0,"2022/04/26 7:00:00","2022/06/07 2:00:00",13 +204321,"EXPチップ《テラ》",,-1,-1,10017,3,-1,-1,False,0,0,0,0,0,"2022/04/26 7:00:00","2022/06/07 2:00:00",12 +204322,"EXPメモリ",,-1,-1,10017,5,-1,-1,False,0,0,0,0,0,"2022/04/26 7:00:00","2022/06/07 2:00:00",11 +204323,"ゴールド《キロ》",,-1,-1,10017,1,-1,-1,False,0,0,0,0,0,"2022/04/26 7:00:00","2022/06/07 2:00:00",10 +204324,"ゴールド《テラ》",,-1,-1,10017,3,-1,-1,False,0,0,0,0,0,"2022/04/26 7:00:00","2022/06/07 2:00:00",9 +204325,"プラチナ",,-1,-1,10017,5,-1,-1,False,0,0,0,0,0,"2022/04/26 7:00:00","2022/06/07 2:00:00",8 +204326,"プロパティアペンド",,-1,-1,10017,3,-1,-1,False,0,0,0,0,0,"2022/04/26 7:00:00","2022/06/07 2:00:00",7 +204327,"コードイレイサー:1",,-1,-1,10017,3,-1,-1,False,0,0,0,0,0,"2022/04/26 7:00:00","2022/06/07 2:00:00",6 +204328,"コードイレイサー:2",,-1,-1,10017,3,-1,-1,False,0,0,0,0,0,"2022/04/26 7:00:00","2022/06/07 2:00:00",5 +204329,"コードイレイサー:3",,-1,-1,10017,3,-1,-1,False,0,0,0,0,0,"2022/04/26 7:00:00","2022/06/07 2:00:00",4 +204330,"コードイレイサー:4",,-1,-1,10017,3,-1,-1,False,0,0,0,0,0,"2022/04/26 7:00:00","2022/06/07 2:00:00",3 +204331,"ヒーローレベルアペンド《1》",,-1,-1,10017,20,-1,-1,False,0,0,0,0,0,"2022/04/26 7:00:00","2022/06/07 2:00:00",2 +204332,"ウェポンレベルアペンド《1》",,-1,-1,10017,20,-1,-1,False,0,0,0,0,0,"2022/04/26 7:00:00","2022/06/07 2:00:00",1 +204333,"アクセサリレベルアペンド《1》",,-1,-1,10017,20,-1,-1,False,0,0,0,0,0,"2022/04/26 7:00:00","2022/06/07 2:00:00",0 diff --git a/titles/sao/database.py b/titles/sao/database.py new file mode 100644 index 0000000..b7026fb --- /dev/null +++ b/titles/sao/database.py @@ -0,0 +1,13 @@ +from core.data import Data +from core.config import CoreConfig + +from .schema import * + + +class SaoData(Data): + def __init__(self, cfg: CoreConfig) -> None: + super().__init__(cfg) + + self.item = SaoItemData(cfg, self.session) + self.profile = SaoProfileData(cfg, self.session) + self.static = SaoStaticData(cfg, self.session) \ No newline at end of file diff --git a/titles/sao/handlers/__init__.py b/titles/sao/handlers/__init__.py new file mode 100644 index 0000000..4d38136 --- /dev/null +++ b/titles/sao/handlers/__init__.py @@ -0,0 +1,2 @@ +from .base import * +from .helpers import * \ No newline at end of file diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py new file mode 100644 index 0000000..4178640 --- /dev/null +++ b/titles/sao/handlers/base.py @@ -0,0 +1,3232 @@ +import struct +from datetime import datetime +from typing import List +from construct import * +from .helpers import * +import csv +from csv import * + +class SaoRequestHeader: + def __init__(self, data: bytes) -> None: + collection = struct.unpack_from("!HHIIII16sI", data) + self.cmd: int = collection[0] + self.err_status = collection[1] + self.error_type = collection[2] + self.vendor_id: int = collection[3] + self.game_id: int = collection[4] + self.version_id: int = collection[5] + self.hash: str = collection[6] + self.data_len: str = collection[7] + +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: + self.cmd = cmd_id + self.err_status = 0 + self.error_type = 0 + self.vendor_id = 5 + self.game_id = 1 + self.version_id = 1 + self.length = 1 + + def make(self) -> bytes: + return struct.pack("!HHIIIII", self.cmd, self.err_status, self.error_type, self.vendor_id, self.game_id, self.version_id, self.length) + +class SaoBaseResponse: + def __init__(self, cmd_id: int) -> None: + self.header = SaoResponseHeader(cmd_id) + + def make(self) -> bytes: + return self.header.make() + +class SaoNoopResponse(SaoBaseResponse): + def __init__(self, cmd: int) -> None: + super().__init__(cmd) + self.result = 1 + self.length = 5 + + def make(self) -> bytes: + return super().make() + struct.pack("!bI", self.result, 0) + +class SaoGetMaintRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + # TODO: The rest of the mait info request + +class SaoGetMaintResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.maint_begin = datetime.fromtimestamp(0) + self.maint_begin_int_ct = 6 + self.maint_end = datetime.fromtimestamp(0) + self.maint_end_int_ct = 6 + self.dt_format = "%Y%m%d%H%M%S" + + def make(self) -> bytes: + maint_begin_list = [x for x in datetime.strftime(self.maint_begin, self.dt_format)] + maint_end_list = [x for x in datetime.strftime(self.maint_end, self.dt_format)] + self.maint_begin_int_ct = len(maint_begin_list) * 2 + self.maint_end_int_ct = len(maint_end_list) * 2 + + maint_begin_bytes = b"" + maint_end_bytes = b"" + + for x in maint_begin_list: + maint_begin_bytes += struct.pack(" None: + super().__init__(header, data) + +class SaoCommonAcCabinetBootNotificationResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + ) + + resp_data = resp_struct.build(dict( + result=self.result, + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoMasterDataVersionCheckRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + +class SaoMasterDataVersionCheckResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.update_flag = 0 + self.data_version = 100 + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "update_flag" / Int8ul, # result is either 0 or 1 + "data_version" / Int32ub, + ) + + resp_data = resp_struct.build(dict( + result=self.result, + update_flag=self.update_flag, + data_version=self.data_version, + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoCommonGetAppVersionsRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + +class SaoCommonGetAppVersionsRequest(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.data_list_size = 1 # Number of arrays + + self.version_app_id = 1 + self.applying_start_date = "20230520193000" + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "data_list_size" / Int32ub, + + "version_app_id" / Int32ub, + "applying_start_date_size" / Int32ub, # big endian + "applying_start_date" / Int16ul[len(self.applying_start_date)], + ) + + resp_data = resp_struct.build(dict( + result=self.result, + data_list_size=self.data_list_size, + + version_app_id=self.version_app_id, + applying_start_date_size=len(self.applying_start_date) * 2, + applying_start_date=[ord(x) for x in self.applying_start_date], + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoCommonPayingPlayStartRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + +class SaoCommonPayingPlayStartRequest(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.paying_session_id = "1" + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "paying_session_id_size" / Int32ub, # big endian + "paying_session_id" / Int16ul[len(self.paying_session_id)], + ) + + resp_data = resp_struct.build(dict( + result=self.result, + paying_session_id_size=len(self.paying_session_id) * 2, + paying_session_id=[ord(x) for x in self.paying_session_id], + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetAuthCardDataRequest(SaoBaseRequest): + def __init__(self, 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) + off += BYTE_OFF + + store_id = decode_str(data, off) + self.store_id = store_id[0] + off += store_id[1] + + serial_no = decode_str(data, off) + self.serial_no = serial_no[0] + off += serial_no[1] + + access_code = decode_str(data, off) + self.access_code = access_code[0] + off += access_code[1] + + chip_id = decode_str(data, off) + 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) + + self.result = 1 + self.unused_card_flag = "" + self.first_play_flag = 0 + self.tutorial_complete_flag = 1 + self.nick_name = profile_data['nick_name'] # nick_name field #4 + self.personal_id = str(profile_data['user']) + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "unused_card_flag_size" / Int32ub, # big endian + "unused_card_flag" / Int16ul[len(self.unused_card_flag)], + "first_play_flag" / Int8ul, # result is either 0 or 1 + "tutorial_complete_flag" / Int8ul, # result is either 0 or 1 + "nick_name_size" / Int32ub, # big endian + "nick_name" / Int16ul[len(self.nick_name)], + "personal_id_size" / Int32ub, # big endian + "personal_id" / Int16ul[len(self.personal_id)] + ) + + resp_data = resp_struct.build(dict( + result=self.result, + unused_card_flag_size=len(self.unused_card_flag) * 2, + unused_card_flag=[ord(x) for x in self.unused_card_flag], + first_play_flag=self.first_play_flag, + tutorial_complete_flag=self.tutorial_complete_flag, + nick_name_size=len(self.nick_name) * 2, + nick_name=[ord(x) for x in self.nick_name], + personal_id_size=len(self.personal_id) * 2, + personal_id=[ord(x) for x in self.personal_id] + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoHomeCheckAcLoginBonusRequest(SaoBaseRequest): + def __init__(self, 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): + 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) + off += BYTE_OFF + + store_id = decode_str(data, off) + self.store_id = store_id[0] + off += store_id[1] + + store_name = decode_str(data, off) + self.store_name = store_name[0] + off += store_name[1] + + serial_no = decode_str(data, off) + self.serial_no = serial_no[0] + off += serial_no[1] + + access_code = decode_str(data, off) + self.access_code = access_code[0] + off += access_code[1] + + chip_id = decode_str(data, off) + self.chip_id = chip_id[0] + off += chip_id[1] + + 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) + self.result = 1 + self.user_id = str(profile_data['user']) + self.first_play_flag = 0 + self.grantable_free_ticket_flag = 1 + self.login_reward_vp = 99 + self.today_paying_flag = 1 + + def make(self) -> bytes: + # create a resp struct + ''' + bool = Int8ul + short = Int16ub + int = Int32ub + ''' + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "user_id_size" / Int32ub, # big endian + "user_id" / Int16ul[len(self.user_id)], + "first_play_flag" / Int8ul, # result is either 0 or 1 + "grantable_free_ticket_flag" / Int8ul, # result is either 0 or 1 + "login_reward_vp" / Int16ub, + "today_paying_flag" / Int8ul, # result is either 0 or 1 + ) + + resp_data = resp_struct.build(dict( + result=self.result, + user_id_size=len(self.user_id) * 2, + user_id=[ord(x) for x in self.user_id], + first_play_flag=self.first_play_flag, + grantable_free_ticket_flag=self.grantable_free_ticket_flag, + login_reward_vp=self.login_reward_vp, + today_paying_flag=self.today_paying_flag, + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoCheckComebackEventRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + +class SaoCheckComebackEventRequest(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.get_flag_ = 1 + self.get_comeback_event_id_list = "" # Array of events apparently + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "get_flag_" / Int8ul, # result is either 0 or 1 + "get_comeback_event_id_list_size" / Int32ub, # big endian + "get_comeback_event_id_list" / Int16ul[len(self.get_comeback_event_id_list)], + ) + + resp_data = resp_struct.build(dict( + result=self.result, + get_flag_=self.get_flag_, + get_comeback_event_id_list_size=len(self.get_comeback_event_id_list) * 2, + get_comeback_event_id_list=[ord(x) for x in self.get_comeback_event_id_list], + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetUserBasicDataRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + self.user_id = decode_str(data, 0)[0] + +class SaoGetUserBasicDataResponse(SaoBaseResponse): + def __init__(self, cmd, profile_data) -> None: + super().__init__(cmd) + self.result = 1 + self.user_basic_data_size = 1 # Number of arrays + self.user_type = profile_data['user_type'] + self.nick_name = profile_data['nick_name'] + self.rank_num = profile_data['rank_num'] + self.rank_exp = profile_data['rank_exp'] + self.own_col = profile_data['own_col'] + self.own_vp = profile_data['own_vp'] + self.own_yui_medal = profile_data['own_yui_medal'] + self.setting_title_id = profile_data['setting_title_id'] + self.favorite_user_hero_log_id = "" + self.favorite_user_support_log_id = "" + self.my_store_id = "1" + self.my_store_name = "ARTEMiS" + self.user_reg_date = "20230101120000" + + def make(self) -> bytes: + # create a resp struct + ''' + bool = Int8ul + short = Int16ub + int = Int32ub + ''' + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "user_basic_data_size" / Int32ub, + + "user_type" / Int16ub, + "nick_name_size" / Int32ub, # big endian + "nick_name" / Int16ul[len(self.nick_name)], + "rank_num" / Int16ub, + "rank_exp" / Int32ub, + "own_col" / Int32ub, + "own_vp" / Int32ub, + "own_yui_medal" / Int32ub, + "setting_title_id" / Int32ub, + "favorite_user_hero_log_id_size" / Int32ub, # big endian + "favorite_user_hero_log_id" / Int16ul[len(str(self.favorite_user_hero_log_id))], + "favorite_user_support_log_id_size" / Int32ub, # big endian + "favorite_user_support_log_id" / Int16ul[len(str(self.favorite_user_support_log_id))], + "my_store_id_size" / Int32ub, # big endian + "my_store_id" / Int16ul[len(str(self.my_store_id))], + "my_store_name_size" / Int32ub, # big endian + "my_store_name" / Int16ul[len(str(self.my_store_name))], + "user_reg_date_size" / Int32ub, # big endian + "user_reg_date" / Int16ul[len(self.user_reg_date)] + + ) + + resp_data = resp_struct.build(dict( + result=self.result, + user_basic_data_size=self.user_basic_data_size, + + user_type=self.user_type, + nick_name_size=len(self.nick_name) * 2, + nick_name=[ord(x) for x in self.nick_name], + rank_num=self.rank_num, + rank_exp=self.rank_exp, + own_col=self.own_col, + own_vp=self.own_vp, + own_yui_medal=self.own_yui_medal, + setting_title_id=self.setting_title_id, + favorite_user_hero_log_id_size=len(self.favorite_user_hero_log_id) * 2, + favorite_user_hero_log_id=[ord(x) for x in str(self.favorite_user_hero_log_id)], + favorite_user_support_log_id_size=len(self.favorite_user_support_log_id) * 2, + favorite_user_support_log_id=[ord(x) for x in str(self.favorite_user_support_log_id)], + my_store_id_size=len(self.my_store_id) * 2, + my_store_id=[ord(x) for x in str(self.my_store_id)], + my_store_name_size=len(self.my_store_name) * 2, + my_store_name=[ord(x) for x in str(self.my_store_name)], + user_reg_date_size=len(self.user_reg_date) * 2, + user_reg_date=[ord(x) for x in self.user_reg_date], + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetHeroLogUserDataListRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + self.user_id = decode_str(data, 0)[0] + +class SaoGetHeroLogUserDataListResponse(SaoBaseResponse): + def __init__(self, cmd, hero_data) -> None: + super().__init__(cmd) + self.result = 1 + + self.user_hero_log_id = [] + self.log_level = [] + self.max_log_level_extended_num = [] + self.log_exp = [] + self.last_set_skill_slot1_skill_id = [] + self.last_set_skill_slot2_skill_id = [] + self.last_set_skill_slot3_skill_id = [] + self.last_set_skill_slot4_skill_id = [] + self.last_set_skill_slot5_skill_id = [] + + for i in range(len(hero_data)): + + # Calculate level based off experience and the CSV list + with open(r'titles/sao/data/HeroLogLevel.csv') as csv_file: + csv_reader = csv.reader(csv_file, delimiter=',') + line_count = 0 + data = [] + rowf = False + for row in csv_reader: + if rowf==False: + rowf=True + else: + data.append(row) + + exp = hero_data[i][4] + + for e in range(0,len(data)): + if exp>=int(data[e][1]) and exp bytes: + #new stuff + + hero_log_user_data_list_struct = Struct( + "user_hero_log_id_size" / Int32ub, # big endian + "user_hero_log_id" / Int16ul[9], #string + "hero_log_id" / Int32ub, #int + "log_level" / Int16ub, #short + "max_log_level_extended_num" / Int16ub, #short + "log_exp" / Int32ub, #int + "possible_awakening_flag" / Int8ul, # result is either 0 or 1 + "awakening_stage" / Int16ub, #short + "awakening_exp" / Int32ub, #int + "skill_slot_correction_value" / Int8ul, # result is either 0 or 1 + "last_set_skill_slot1_skill_id" / Int16ub, #short + "last_set_skill_slot2_skill_id" / Int16ub, #short + "last_set_skill_slot3_skill_id" / Int16ub, #short + "last_set_skill_slot4_skill_id" / Int16ub, #short + "last_set_skill_slot5_skill_id" / Int16ub, #short + "property1_property_id" / Int32ub, + "property1_value1" / Int32ub, + "property1_value2" / Int32ub, + "property2_property_id" / Int32ub, + "property2_value1" / Int32ub, + "property2_value2" / Int32ub, + "property3_property_id" / Int32ub, + "property3_value1" / Int32ub, + "property3_value2" / Int32ub, + "property4_property_id" / Int32ub, + "property4_value1" / Int32ub, + "property4_value2" / Int32ub, + "converted_card_num" / Int16ub, + "shop_purchase_flag" / Int8ul, # result is either 0 or 1 + "protect_flag" / Int8ul, # result is either 0 or 1 + "get_date_size" / Int32ub, # big endian + "get_date" / Int16ul[len(self.get_date)], + ) + + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "hero_log_user_data_list_size" / Rebuild(Int32ub, len_(this.hero_log_user_data_list)), # big endian + "hero_log_user_data_list" / Array(this.hero_log_user_data_list_size, hero_log_user_data_list_struct), + ) + + resp_data = resp_struct.parse(resp_struct.build(dict( + result=self.result, + hero_log_user_data_list_size=0, + hero_log_user_data_list=[], + ))) + + for i in range(len(self.hero_log_id)): + hero_data = dict( + user_hero_log_id_size=len(self.user_hero_log_id[i]) * 2, + user_hero_log_id=[ord(x) for x in self.user_hero_log_id[i]], + hero_log_id=self.hero_log_id[i], + log_level=self.log_level[i], + max_log_level_extended_num=self.max_log_level_extended_num[i], + log_exp=self.log_exp[i], + possible_awakening_flag=self.possible_awakening_flag, + awakening_stage=self.awakening_stage, + awakening_exp=self.awakening_exp, + skill_slot_correction_value=self.skill_slot_correction_value, + last_set_skill_slot1_skill_id=self.last_set_skill_slot1_skill_id[i], + last_set_skill_slot2_skill_id=self.last_set_skill_slot2_skill_id[i], + last_set_skill_slot3_skill_id=self.last_set_skill_slot3_skill_id[i], + last_set_skill_slot4_skill_id=self.last_set_skill_slot4_skill_id[i], + last_set_skill_slot5_skill_id=self.last_set_skill_slot5_skill_id[i], + property1_property_id=self.property1_property_id, + property1_value1=self.property1_value1, + property1_value2=self.property1_value2, + property2_property_id=self.property2_property_id, + property2_value1=self.property2_value1, + property2_value2=self.property2_value2, + property3_property_id=self.property3_property_id, + property3_value1=self.property3_value1, + property3_value2=self.property3_value2, + property4_property_id=self.property4_property_id, + property4_value1=self.property4_value1, + property4_value2=self.property4_value2, + converted_card_num=self.converted_card_num, + shop_purchase_flag=self.shop_purchase_flag, + protect_flag=self.protect_flag, + get_date_size=len(self.get_date) * 2, + get_date=[ord(x) for x in self.get_date], + + ) + + resp_data.hero_log_user_data_list.append(hero_data) + + 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] + +class SaoGetEquipmentUserDataListResponse(SaoBaseResponse): + def __init__(self, cmd, equipment_data) -> None: + super().__init__(cmd) + 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] + +class SaoGetItemUserDataListResponse(SaoBaseResponse): + def __init__(self, cmd, item_data) -> None: + super().__init__(cmd) + self.result = 1 + + self.user_item_id = [] + + for i in range(len(item_data)): + self.user_item_id.append(item_data[i][2]) + + # item_user_data_list + self.user_item_id = list(map(str,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 + + 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) + +class SaoGetSupportLogUserDataListResponse(SaoBaseResponse): + def __init__(self, cmd, supportIdsData) -> None: + super().__init__(cmd) + self.result = 1 + + # support_log_user_data_list + self.user_support_log_id = list(map(str,supportIdsData)) #str + self.support_log_id = supportIdsData #int + self.possible_awakening_flag = 0 + self.awakening_stage = 0 + self.awakening_exp = 0 + self.converted_card_num = 0 + self.shop_purchase_flag = 0 + self.protect_flag = 0 #byte + self.get_date = "20230101120000" #str + + def make(self) -> bytes: + support_log_user_data_list_struct = Struct( + "user_support_log_id_size" / Int32ub, # big endian + "user_support_log_id" / Int16ul[9], + "support_log_id" / Int32ub, #int + "possible_awakening_flag" / Int8ul, # result is either 0 or 1 + "awakening_stage" / Int16ub, #short + "awakening_exp" / Int32ub, # int + "converted_card_num" / Int16ub, #short + "shop_purchase_flag" / Int8ul, # result is either 0 or 1 + "protect_flag" / Int8ul, # result is either 0 or 1 + "get_date_size" / Int32ub, # big endian + "get_date" / Int16ul[len(self.get_date)], + ) + + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "support_log_user_data_list_size" / Rebuild(Int32ub, len_(this.support_log_user_data_list)), # big endian + "support_log_user_data_list" / Array(this.support_log_user_data_list_size, support_log_user_data_list_struct), + ) + + resp_data = resp_struct.parse(resp_struct.build(dict( + result=self.result, + support_log_user_data_list_size=0, + support_log_user_data_list=[], + ))) + + for i in range(len(self.support_log_id)): + support_data = dict( + user_support_log_id_size=len(self.user_support_log_id[i]) * 2, + user_support_log_id=[ord(x) for x in self.user_support_log_id[i]], + support_log_id=self.support_log_id[i], + possible_awakening_flag=self.possible_awakening_flag, + awakening_stage=self.awakening_stage, + awakening_exp=self.awakening_exp, + converted_card_num=self.converted_card_num, + shop_purchase_flag=self.shop_purchase_flag, + protect_flag=self.protect_flag, + get_date_size=len(self.get_date) * 2, + get_date=[ord(x) for x in self.get_date], + + ) + + resp_data.support_log_user_data_list.append(support_data) + + resp_data["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) + +class SaoGetTitleUserDataListResponse(SaoBaseResponse): + def __init__(self, cmd, titleIdsData) -> None: + super().__init__(cmd) + self.result = 1 + + # title_user_data_list + self.user_title_id = list(map(str,titleIdsData)) #str + self.title_id = titleIdsData #int + + def make(self) -> bytes: + title_user_data_list_struct = Struct( + "user_title_id_size" / Int32ub, # big endian + "user_title_id" / Int16ul[6], #string + "title_id" / Int32ub, #int + ) + + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "title_user_data_list_size" / Rebuild(Int32ub, len_(this.title_user_data_list)), # big endian + "title_user_data_list" / Array(this.title_user_data_list_size, title_user_data_list_struct), + ) + + resp_data = resp_struct.parse(resp_struct.build(dict( + result=self.result, + title_user_data_list_size=0, + title_user_data_list=[], + ))) + + for i in range(len(self.title_id)): + title_data = dict( + user_title_id_size=len(self.user_title_id[i]) * 2, + user_title_id=[ord(x) for x in self.user_title_id[i]], + title_id=self.title_id[i], + ) + + resp_data.title_user_data_list.append(title_data) + + 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] + +class SaoGetEpisodeAppendDataListResponse(SaoBaseResponse): + def __init__(self, cmd, profile_data) -> None: + super().__init__(cmd) + self.length = None + self.result = 1 + + self.user_episode_append_id_list = ["10001", "10002", "10003", "10004", "10005"] + self.user_id_list = [str(profile_data["user"]), str(profile_data["user"]), str(profile_data["user"]), str(profile_data["user"]), str(profile_data["user"])] + self.episode_append_id_list = [10001, 10002, 10003, 10004, 10005] + self.own_num_list = [3, 3, 3, 3 ,3] + + def make(self) -> bytes: + episode_data_struct = Struct( + "user_episode_append_id_size" / Rebuild(Int32ub, len_(this.user_episode_append_id) * 2), # calculates the length of the user_episode_append_id + "user_episode_append_id" / PaddedString(this.user_episode_append_id_size, "utf_16_le"), # user_episode_append_id is a (zero) padded string + "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id + "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string + "episode_append_id" / Int32ub, + "own_num" / Int32ub, + ) + + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "episode_append_data_list_size" / Rebuild(Int32ub, len_(this.episode_append_data_list)), # big endian + "episode_append_data_list" / Array(this.episode_append_data_list_size, episode_data_struct), + ) + + # really dump to parse the build resp, but that creates a new object + # and is nicer to twork with + resp_data = resp_struct.parse(resp_struct.build(dict( + result=self.result, + episode_append_data_list_size=0, + episode_append_data_list=[], + ))) + + if len(self.user_episode_append_id_list) != len(self.user_id_list) != len(self.episode_append_id_list) != len(self.own_num_list): + raise ValueError("all lists must be of the same length") + + for i in range(len(self.user_id_list)): + # add the episode_data_struct to the resp_struct.episode_append_data_list + resp_data.episode_append_data_list.append(dict( + user_episode_append_id=self.user_episode_append_id_list[i], + user_id=self.user_id_list[i], + episode_append_id=self.episode_append_id_list[i], + own_num=self.own_num_list[i], + )) + + 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) + + self.result = 1 + self.party_data_list_size = 1 # Number of arrays + + self.user_party_id = "0" + self.team_no = 0 + self.party_team_data_list_size = 3 # Number of arrays + + self.user_party_team_id_1 = "0" + self.arrangement_num_1 = 0 + self.user_hero_log_id_1 = str(hero1_data[2]) + self.main_weapon_user_equipment_id_1 = str(hero1_data[5]) + self.sub_equipment_user_equipment_id_1 = str(hero1_data[6]) + self.skill_slot1_skill_id_1 = hero1_data[7] + self.skill_slot2_skill_id_1 = hero1_data[8] + self.skill_slot3_skill_id_1 = hero1_data[9] + self.skill_slot4_skill_id_1 = hero1_data[10] + self.skill_slot5_skill_id_1 = hero1_data[11] + + self.user_party_team_id_2 = "0" + self.arrangement_num_2 = 0 + self.user_hero_log_id_2 = str(hero2_data[2]) + self.main_weapon_user_equipment_id_2 = str(hero2_data[5]) + self.sub_equipment_user_equipment_id_2 = str(hero2_data[6]) + self.skill_slot1_skill_id_2 = hero2_data[7] + self.skill_slot2_skill_id_2 = hero2_data[8] + self.skill_slot3_skill_id_2 = hero2_data[9] + self.skill_slot4_skill_id_2 = hero2_data[10] + self.skill_slot5_skill_id_2 = hero2_data[11] + + self.user_party_team_id_3 = "0" + self.arrangement_num_3 = 0 + self.user_hero_log_id_3 = str(hero3_data[2]) + self.main_weapon_user_equipment_id_3 = str(hero3_data[5]) + self.sub_equipment_user_equipment_id_3 = str(hero3_data[6]) + self.skill_slot1_skill_id_3 = hero3_data[7] + self.skill_slot2_skill_id_3 = hero3_data[8] + self.skill_slot3_skill_id_3 = hero3_data[9] + self.skill_slot4_skill_id_3 = hero3_data[10] + self.skill_slot5_skill_id_3 = hero3_data[11] + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "party_data_list_size" / Int32ub, # big endian + + "user_party_id_size" / Int32ub, # big endian + "user_party_id" / Int16ul[len(self.user_party_id)], + "team_no" / Int8ul, # result is either 0 or 1 + "party_team_data_list_size" / Int32ub, # big endian + + "user_party_team_id_1_size" / Int32ub, # big endian + "user_party_team_id_1" / Int16ul[len(self.user_party_team_id_1)], + "arrangement_num_1" / Int8ul, # big endian + "user_hero_log_id_1_size" / Int32ub, # big endian + "user_hero_log_id_1" / Int16ul[len(self.user_hero_log_id_1)], + "main_weapon_user_equipment_id_1_size" / Int32ub, # big endian + "main_weapon_user_equipment_id_1" / Int16ul[len(self.main_weapon_user_equipment_id_1)], + "sub_equipment_user_equipment_id_1_size" / Int32ub, # big endian + "sub_equipment_user_equipment_id_1" / Int16ul[len(self.sub_equipment_user_equipment_id_1)], + "skill_slot1_skill_id_1" / Int32ub, + "skill_slot2_skill_id_1" / Int32ub, + "skill_slot3_skill_id_1" / Int32ub, + "skill_slot4_skill_id_1" / Int32ub, + "skill_slot5_skill_id_1" / Int32ub, + + "user_party_team_id_2_size" / Int32ub, # big endian + "user_party_team_id_2" / Int16ul[len(self.user_party_team_id_2)], + "arrangement_num_2" / Int8ul, # result is either 0 or 1 + "user_hero_log_id_2_size" / Int32ub, # big endian + "user_hero_log_id_2" / Int16ul[len(self.user_hero_log_id_2)], + "main_weapon_user_equipment_id_2_size" / Int32ub, # big endian + "main_weapon_user_equipment_id_2" / Int16ul[len(self.main_weapon_user_equipment_id_2)], + "sub_equipment_user_equipment_id_2_size" / Int32ub, # big endian + "sub_equipment_user_equipment_id_2" / Int16ul[len(self.sub_equipment_user_equipment_id_2)], + "skill_slot1_skill_id_2" / Int32ub, + "skill_slot2_skill_id_2" / Int32ub, + "skill_slot3_skill_id_2" / Int32ub, + "skill_slot4_skill_id_2" / Int32ub, + "skill_slot5_skill_id_2" / Int32ub, + + "user_party_team_id_3_size" / Int32ub, # big endian + "user_party_team_id_3" / Int16ul[len(self.user_party_team_id_3)], + "arrangement_num_3" / Int8ul, # result is either 0 or 1 + "user_hero_log_id_3_size" / Int32ub, # big endian + "user_hero_log_id_3" / Int16ul[len(self.user_hero_log_id_3)], + "main_weapon_user_equipment_id_3_size" / Int32ub, # big endian + "main_weapon_user_equipment_id_3" / Int16ul[len(self.main_weapon_user_equipment_id_3)], + "sub_equipment_user_equipment_id_3_size" / Int32ub, # big endian + "sub_equipment_user_equipment_id_3" / Int16ul[len(self.sub_equipment_user_equipment_id_3)], + "skill_slot1_skill_id_3" / Int32ub, + "skill_slot2_skill_id_3" / Int32ub, + "skill_slot3_skill_id_3" / Int32ub, + "skill_slot4_skill_id_3" / Int32ub, + "skill_slot5_skill_id_3" / Int32ub, + + ) + + resp_data = resp_struct.build(dict( + result=self.result, + party_data_list_size=self.party_data_list_size, + + user_party_id_size=len(self.user_party_id) * 2, + user_party_id=[ord(x) for x in self.user_party_id], + team_no=self.team_no, + party_team_data_list_size=self.party_team_data_list_size, + + user_party_team_id_1_size=len(self.user_party_team_id_1) * 2, + user_party_team_id_1=[ord(x) for x in self.user_party_team_id_1], + arrangement_num_1=self.arrangement_num_1, + user_hero_log_id_1_size=len(self.user_hero_log_id_1) * 2, + user_hero_log_id_1=[ord(x) for x in self.user_hero_log_id_1], + main_weapon_user_equipment_id_1_size=len(self.main_weapon_user_equipment_id_1) * 2, + main_weapon_user_equipment_id_1=[ord(x) for x in self.main_weapon_user_equipment_id_1], + sub_equipment_user_equipment_id_1_size=len(self.sub_equipment_user_equipment_id_1) * 2, + sub_equipment_user_equipment_id_1=[ord(x) for x in self.sub_equipment_user_equipment_id_1], + skill_slot1_skill_id_1=self.skill_slot1_skill_id_1, + skill_slot2_skill_id_1=self.skill_slot2_skill_id_1, + skill_slot3_skill_id_1=self.skill_slot3_skill_id_1, + skill_slot4_skill_id_1=self.skill_slot4_skill_id_1, + skill_slot5_skill_id_1=self.skill_slot5_skill_id_1, + + user_party_team_id_2_size=len(self.user_party_team_id_2) * 2, + user_party_team_id_2=[ord(x) for x in self.user_party_team_id_2], + arrangement_num_2=self.arrangement_num_2, + user_hero_log_id_2_size=len(self.user_hero_log_id_2) * 2, + user_hero_log_id_2=[ord(x) for x in self.user_hero_log_id_2], + main_weapon_user_equipment_id_2_size=len(self.main_weapon_user_equipment_id_2) * 2, + main_weapon_user_equipment_id_2=[ord(x) for x in self.main_weapon_user_equipment_id_2], + sub_equipment_user_equipment_id_2_size=len(self.sub_equipment_user_equipment_id_2) * 2, + sub_equipment_user_equipment_id_2=[ord(x) for x in self.sub_equipment_user_equipment_id_2], + skill_slot1_skill_id_2=self.skill_slot1_skill_id_2, + skill_slot2_skill_id_2=self.skill_slot2_skill_id_2, + skill_slot3_skill_id_2=self.skill_slot3_skill_id_2, + skill_slot4_skill_id_2=self.skill_slot4_skill_id_2, + skill_slot5_skill_id_2=self.skill_slot5_skill_id_2, + + user_party_team_id_3_size=len(self.user_party_team_id_3) * 2, + user_party_team_id_3=[ord(x) for x in self.user_party_team_id_3], + arrangement_num_3=self.arrangement_num_3, + user_hero_log_id_3_size=len(self.user_hero_log_id_3) * 2, + user_hero_log_id_3=[ord(x) for x in self.user_hero_log_id_3], + main_weapon_user_equipment_id_3_size=len(self.main_weapon_user_equipment_id_3) * 2, + main_weapon_user_equipment_id_3=[ord(x) for x in self.main_weapon_user_equipment_id_3], + sub_equipment_user_equipment_id_3_size=len(self.sub_equipment_user_equipment_id_3) * 2, + sub_equipment_user_equipment_id_3=[ord(x) for x in self.sub_equipment_user_equipment_id_3], + skill_slot1_skill_id_3=self.skill_slot1_skill_id_3, + skill_slot2_skill_id_3=self.skill_slot2_skill_id_3, + skill_slot3_skill_id_3=self.skill_slot3_skill_id_3, + skill_slot4_skill_id_3=self.skill_slot4_skill_id_3, + skill_slot5_skill_id_3=self.skill_slot5_skill_id_3, + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetQuestScenePrevScanProfileCardRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + +class SaoGetQuestScenePrevScanProfileCardResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.profile_card_data = 1 # number of arrays + + self.profile_card_code = "" + self.nick_name = "" + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "profile_card_data" / Int32ub, # big endian + + "profile_card_code_size" / Int32ub, # big endian + "profile_card_code" / Int16ul[len(self.profile_card_code)], + "nick_name_size" / Int32ub, # big endian + "nick_name" / Int16ul[len(self.nick_name)], + "rank_num" / Int16ub, #short + "setting_title_id" / Int32ub, # int + "skill_id" / Int16ub, # short + "hero_log_hero_log_id" / Int32ub, # int + "hero_log_log_level" / Int16ub, # short + "hero_log_awakening_stage" / Int16ub, # short + "hero_log_property1_property_id" / Int32ub, # int + "hero_log_property1_value1" / Int32ub, # int + "hero_log_property1_value2" / Int32ub, # int + "hero_log_property2_property_id" / Int32ub, # int + "hero_log_property2_value1" / Int32ub, # int + "hero_log_property2_value2" / Int32ub, # int + "hero_log_property3_property_id" / Int32ub, # int + "hero_log_property3_value1" / Int32ub, # int + "hero_log_property3_value2" / Int32ub, # int + "hero_log_property4_property_id" / Int32ub, # int + "hero_log_property4_value1" / Int32ub, # int + "hero_log_property4_value2" / Int32ub, # int + "main_weapon_equipment_id" / Int32ub, # int + "main_weapon_enhancement_value" / Int16ub, # short + "main_weapon_awakening_stage" / Int16ub, # short + "main_weapon_property1_property_id" / Int32ub, # int + "main_weapon_property1_value1" / Int32ub, # int + "main_weapon_property1_value2" / Int32ub, # int + "main_weapon_property2_property_id" / Int32ub, # int + "main_weapon_property2_value1" / Int32ub, # int + "main_weapon_property2_value2" / Int32ub, # int + "main_weapon_property3_property_id" / Int32ub, # int + "main_weapon_property3_value1" / Int32ub, # int + "main_weapon_property3_value2" / Int32ub, # int + "main_weapon_property4_property_id" / Int32ub, # int + "main_weapon_property4_value1" / Int32ub, # int + "main_weapon_property4_value2" / Int32ub, # int + "sub_equipment_equipment_id" / Int32ub, # int + "sub_equipment_enhancement_value" / Int16ub, # short + "sub_equipment_awakening_stage" / Int16ub, # short + "sub_equipment_property1_property_id" / Int32ub, # int + "sub_equipment_property1_value1" / Int32ub, # int + "sub_equipment_property1_value2" / Int32ub, # int + "sub_equipment_property2_property_id" / Int32ub, # int + "sub_equipment_property2_value1" / Int32ub, # int + "sub_equipment_property2_value2" / Int32ub, # int + "sub_equipment_property3_property_id" / Int32ub, # int + "sub_equipment_property3_value1" / Int32ub, # int + "sub_equipment_property3_value2" / Int32ub, # int + "sub_equipment_property4_property_id" / Int32ub, # int + "sub_equipment_property4_value1" / Int32ub, # int + "sub_equipment_property4_value2" / Int32ub, # int + "holographic_flag" / Int8ul, # result is either 0 or 1 + ) + + resp_data = resp_struct.build(dict( + result=self.result, + profile_card_data=self.profile_card_data, + + profile_card_code_size=len(self.profile_card_code) * 2, + profile_card_code=[ord(x) for x in self.profile_card_code], + nick_name_size=len(self.nick_name) * 2, + nick_name=[ord(x) for x in self.nick_name], + rank_num=0, + setting_title_id=0, + skill_id=0, + hero_log_hero_log_id=0, + hero_log_log_level=0, + hero_log_awakening_stage=0, + hero_log_property1_property_id=0, + hero_log_property1_value1=0, + hero_log_property1_value2=0, + hero_log_property2_property_id=0, + hero_log_property2_value1=0, + hero_log_property2_value2=0, + hero_log_property3_property_id=0, + hero_log_property3_value1=0, + hero_log_property3_value2=0, + hero_log_property4_property_id=0, + hero_log_property4_value1=0, + hero_log_property4_value2=0, + main_weapon_equipment_id=0, + main_weapon_enhancement_value=0, + main_weapon_awakening_stage=0, + main_weapon_property1_property_id=0, + main_weapon_property1_value1=0, + main_weapon_property1_value2=0, + main_weapon_property2_property_id=0, + main_weapon_property2_value1=0, + main_weapon_property2_value2=0, + main_weapon_property3_property_id=0, + main_weapon_property3_value1=0, + main_weapon_property3_value2=0, + main_weapon_property4_property_id=0, + main_weapon_property4_value1=0, + main_weapon_property4_value2=0, + sub_equipment_equipment_id=0, + sub_equipment_enhancement_value=0, + sub_equipment_awakening_stage=0, + sub_equipment_property1_property_id=0, + sub_equipment_property1_value1=0, + sub_equipment_property1_value2=0, + sub_equipment_property2_property_id=0, + sub_equipment_property2_value1=0, + sub_equipment_property2_value2=0, + sub_equipment_property3_property_id=0, + sub_equipment_property3_value1=0, + sub_equipment_property3_value2=0, + sub_equipment_property4_property_id=0, + sub_equipment_property4_value1=0, + sub_equipment_property4_value2=0, + holographic_flag=0, + + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetResourcePathInfoRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + +class SaoGetResourcePathInfoResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.resource_base_url = "http://localhost:9000/SDEW/100/" + self.gasha_base_dir = "a" + self.ad_base_dir = "b" + self.event_base_dir = "c" + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "resource_base_url_size" / Int32ub, # big endian + "resource_base_url" / Int16ul[len(self.resource_base_url)], + "gasha_base_dir_size" / Int32ub, # big endian + "gasha_base_dir" / Int16ul[len(self.gasha_base_dir)], + "ad_base_dir_size" / Int32ub, # big endian + "ad_base_dir" / Int16ul[len(self.ad_base_dir)], + "event_base_dir_size" / Int32ub, # big endian + "event_base_dir" / Int16ul[len(self.event_base_dir)], + ) + + resp_data = resp_struct.build(dict( + result=self.result, + resource_base_url_size=len(self.resource_base_url) * 2, + resource_base_url=[ord(x) for x in self.resource_base_url], + gasha_base_dir_size=len(self.gasha_base_dir) * 2, + gasha_base_dir=[ord(x) for x in self.gasha_base_dir], + ad_base_dir_size=len(self.ad_base_dir) * 2, + ad_base_dir=[ord(x) for x in self.ad_base_dir], + event_base_dir_size=len(self.event_base_dir) * 2, + event_base_dir=[ord(x) for x in self.event_base_dir], + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoEpisodePlayStartRequest(SaoBaseRequest): + def __init__(self, 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] + + self.episode_id = decode_int(data, off) + off += INT_OFF + + 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[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) + +class SaoEpisodePlayStartResponse(SaoBaseResponse): + def __init__(self, cmd, profile_data) -> None: + super().__init__(cmd) + self.result = 1 + self.play_start_response_data_size = 1 # Number of arrays (minimum 1 mandatory) + self.multi_play_start_response_data_size = 0 # Number of arrays (set 0 due to single play) + + self.appearance_player_trace_data_list_size = 1 + + self.user_quest_scene_player_trace_id = "1003" + self.nick_name = profile_data["nick_name"] + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "play_start_response_data_size" / Int32ub, + "multi_play_start_response_data_size" / Int32ub, + + "appearance_player_trace_data_list_size" / Int32ub, + + "user_quest_scene_player_trace_id_size" / Int32ub, # big endian + "user_quest_scene_player_trace_id" / Int16ul[len(self.user_quest_scene_player_trace_id)], + "nick_name_size" / Int32ub, # big endian + "nick_name" / Int16ul[len(self.nick_name)], + ) + + resp_data = resp_struct.build(dict( + result=self.result, + play_start_response_data_size=self.play_start_response_data_size, + multi_play_start_response_data_size=self.multi_play_start_response_data_size, + + appearance_player_trace_data_list_size=self.appearance_player_trace_data_list_size, + + user_quest_scene_player_trace_id_size=len(self.user_quest_scene_player_trace_id) * 2, + user_quest_scene_player_trace_id=[ord(x) for x in self.user_quest_scene_player_trace_id], + nick_name_size=len(self.nick_name) * 2, + nick_name=[ord(x) for x in self.nick_name], + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoEpisodePlayEndRequest(SaoBaseRequest): + def __init__(self, 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] + + 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: 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) + +class SaoEpisodePlayEndResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.play_end_response_data_size = 1 # Number of arrays + self.multi_play_end_response_data_size = 1 # Unused on solo play + + self.dummy_1 = 0 + self.dummy_2 = 0 + self.dummy_3 = 0 + + self.rarity_up_occurrence_flag = 0 + self.adventure_ex_area_occurrences_flag = 0 + self.ex_bonus_data_list_size = 1 # Number of arrays + self.play_end_player_trace_reward_data_list_size = 0 # Number of arrays + + self.ex_bonus_table_id = 0 # ExBonusTable.csv values, dont care for now + self.achievement_status = 1 + + self.common_reward_data_size = 1 # Number of arrays + + self.common_reward_type = 0 # dummy values from 2,101000000,1 from RewardTable.csv + self.common_reward_id = 0 + self.common_reward_num = 0 + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "play_end_response_data_size" / Int32ub, # big endian + + "rarity_up_occurrence_flag" / Int8ul, # result is either 0 or 1 + "adventure_ex_area_occurrences_flag" / Int8ul, # result is either 0 or 1 + "ex_bonus_data_list_size" / Int32ub, # big endian + "play_end_player_trace_reward_data_list_size" / Int32ub, # big endian + + # ex_bonus_data_list + "ex_bonus_table_id" / Int32ub, + "achievement_status" / Int8ul, # result is either 0 or 1 + + # play_end_player_trace_reward_data_list + "common_reward_data_size" / Int32ub, + + # common_reward_data + "common_reward_type" / Int16ub, # short + "common_reward_id" / Int32ub, + "common_reward_num" / Int32ub, + + "multi_play_end_response_data_size" / Int32ub, # big endian + + # multi_play_end_response_data + "dummy_1" / Int8ul, # result is either 0 or 1 + "dummy_2" / Int8ul, # result is either 0 or 1 + "dummy_3" / Int8ul, # result is either 0 or 1 + ) + + resp_data = resp_struct.build(dict( + result=self.result, + play_end_response_data_size=self.play_end_response_data_size, + + rarity_up_occurrence_flag=self.rarity_up_occurrence_flag, + adventure_ex_area_occurrences_flag=self.adventure_ex_area_occurrences_flag, + ex_bonus_data_list_size=self.ex_bonus_data_list_size, + play_end_player_trace_reward_data_list_size=self.play_end_player_trace_reward_data_list_size, + + ex_bonus_table_id=self.ex_bonus_table_id, + achievement_status=self.achievement_status, + + common_reward_data_size=self.common_reward_data_size, + + common_reward_type=self.common_reward_type, + common_reward_id=self.common_reward_id, + common_reward_num=self.common_reward_num, + + multi_play_end_response_data_size=self.multi_play_end_response_data_size, + + dummy_1=self.dummy_1, + dummy_2=self.dummy_2, + dummy_3=self.dummy_3, + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoTrialTowerPlayStartRequest(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] + + self.trial_tower_id = decode_int(data, off) + off += INT_OFF + + 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[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) + +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] + + user_id = decode_str(data, off) + self.user_id = user_id[0] + off += user_id[1] + + 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: 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) + +class SaoTrialTowerPlayEndResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.play_end_response_data_size = 1 # Number of arrays + self.multi_play_end_response_data_size = 1 # Unused on solo play + self.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.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 + +class SaoEpisodePlayEndUnanalyzedLogFixedRequest(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] + + self.episode_id = decode_int(data, off) + off += INT_OFF + + self.rarity_up_exec_flag = decode_byte(data, off) + off += BYTE_OFF + +class SaoEpisodePlayEndUnanalyzedLogFixedResponse(SaoBaseResponse): + def __init__(self, cmd, end_session_data) -> None: + super().__init__(cmd) + self.result = 1 + + 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] + +class SaoGetQuestSceneUserDataListResponse(SaoBaseResponse): + def __init__(self, cmd, quest_data) -> None: + super().__init__(cmd) + self.length = None + self.result = 1 + + # 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 + +class SaoCheckYuiMedalGetConditionRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + +class SaoCheckYuiMedalGetConditionResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.get_flag = 1 + self.elapsed_days = 0 + self.get_yui_medal_num = 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 + "elapsed_days" / Int16ub, #short + "get_yui_medal_num" / Int16ub, #short + ) + + resp_data = resp_struct.build(dict( + result=self.result, + get_flag=self.get_flag, + elapsed_days=self.elapsed_days, + get_yui_medal_num=self.get_yui_medal_num, + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetYuiMedalBonusUserDataRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + +class SaoGetYuiMedalBonusUserDataResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.data_size = 1 # number of arrays + + self.elapsed_days = 1 + self.loop_num = 1 + self.last_check_date = "20230520193000" + self.last_get_date = "20230520193000" + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "data_size" / Int32ub, # big endian + + "elapsed_days" / Int32ub, # big endian + "loop_num" / Int32ub, # big endian + "last_check_date_size" / Int32ub, # big endian + "last_check_date" / Int16ul[len(self.last_check_date)], + "last_get_date_size" / Int32ub, # big endian + "last_get_date" / Int16ul[len(self.last_get_date)], + ) + + resp_data = resp_struct.build(dict( + result=self.result, + data_size=self.data_size, + + elapsed_days=self.elapsed_days, + loop_num=self.loop_num, + last_check_date_size=len(self.last_check_date) * 2, + last_check_date=[ord(x) for x in self.last_check_date], + last_get_date_size=len(self.last_get_date) * 2, + last_get_date=[ord(x) for x in self.last_get_date], + + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoCheckProfileCardUsedRewardRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + +class SaoCheckProfileCardUsedRewardResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.get_flag = 1 + self.used_num = 0 + self.get_vp = 1 + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "get_flag" / Int8ul, # result is either 0 or 1 + "used_num" / Int32ub, # big endian + "get_vp" / Int32ub, # big endian + ) + + resp_data = resp_struct.build(dict( + result=self.result, + get_flag=self.get_flag, + used_num=self.used_num, + get_vp=self.get_vp, + + )) + + self.length = len(resp_data) + return super().make() + resp_data + +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] + + user_id = decode_str(data, off) + self.user_id = user_id[0] + off += user_id[1] + + 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.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) + +class SaoSynthesizeEnhancementHeroLogResponse(SaoBaseResponse): + def __init__(self, cmd, hero_data) -> None: + super().__init__(cmd) + self.result = 1 + + # Calculate level based off experience and the CSV list + with open(r'titles/sao/data/HeroLogLevel.csv') as csv_file: + csv_reader = csv.reader(csv_file, delimiter=',') + line_count = 0 + data = [] + rowf = False + for row in csv_reader: + if rowf==False: + rowf=True + else: + data.append(row) + + exp = hero_data[4] + + for e in range(0,len(data)): + if exp>=int(data[e][1]) and exp bytes: + #new stuff + + hero_log_user_data_list_struct = Struct( + "user_hero_log_id_size" / Int32ub, # big endian + "user_hero_log_id" / Int16ul[9], #string + "hero_log_id" / Int32ub, #int + "log_level" / Int16ub, #short + "max_log_level_extended_num" / Int16ub, #short + "log_exp" / Int32ub, #int + "possible_awakening_flag" / Int8ul, # result is either 0 or 1 + "awakening_stage" / Int16ub, #short + "awakening_exp" / Int32ub, #int + "skill_slot_correction_value" / Int8ul, # result is either 0 or 1 + "last_set_skill_slot1_skill_id" / Int16ub, #short + "last_set_skill_slot2_skill_id" / Int16ub, #short + "last_set_skill_slot3_skill_id" / Int16ub, #short + "last_set_skill_slot4_skill_id" / Int16ub, #short + "last_set_skill_slot5_skill_id" / Int16ub, #short + "property1_property_id" / Int32ub, + "property1_value1" / Int32ub, + "property1_value2" / Int32ub, + "property2_property_id" / Int32ub, + "property2_value1" / Int32ub, + "property2_value2" / Int32ub, + "property3_property_id" / Int32ub, + "property3_value1" / Int32ub, + "property3_value2" / Int32ub, + "property4_property_id" / Int32ub, + "property4_value1" / Int32ub, + "property4_value2" / Int32ub, + "converted_card_num" / Int16ub, + "shop_purchase_flag" / Int8ul, # result is either 0 or 1 + "protect_flag" / Int8ul, # result is either 0 or 1 + "get_date_size" / Int32ub, # big endian + "get_date" / Int16ul[len(self.get_date)], + ) + + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "hero_log_user_data_list_size" / Rebuild(Int32ub, len_(this.hero_log_user_data_list)), # big endian + "hero_log_user_data_list" / Array(this.hero_log_user_data_list_size, hero_log_user_data_list_struct), + ) + + resp_data = resp_struct.parse(resp_struct.build(dict( + result=self.result, + hero_log_user_data_list_size=0, + hero_log_user_data_list=[], + ))) + + hero_data = dict( + user_hero_log_id_size=len(self.user_hero_log_id) * 2, + user_hero_log_id=[ord(x) for x in self.user_hero_log_id], + hero_log_id=self.hero_log_id, + log_level=self.log_level, + max_log_level_extended_num=self.max_log_level_extended_num, + log_exp=self.log_exp, + possible_awakening_flag=self.possible_awakening_flag, + awakening_stage=self.awakening_stage, + awakening_exp=self.awakening_exp, + skill_slot_correction_value=self.skill_slot_correction_value, + last_set_skill_slot1_skill_id=self.last_set_skill_slot1_skill_id, + last_set_skill_slot2_skill_id=self.last_set_skill_slot2_skill_id, + last_set_skill_slot3_skill_id=self.last_set_skill_slot3_skill_id, + last_set_skill_slot4_skill_id=self.last_set_skill_slot4_skill_id, + last_set_skill_slot5_skill_id=self.last_set_skill_slot5_skill_id, + property1_property_id=self.property1_property_id, + property1_value1=self.property1_value1, + property1_value2=self.property1_value2, + property2_property_id=self.property2_property_id, + property2_value1=self.property2_value1, + property2_value2=self.property2_value2, + property3_property_id=self.property3_property_id, + property3_value1=self.property3_value1, + property3_value2=self.property3_value2, + property4_property_id=self.property4_property_id, + property4_value1=self.property4_value1, + property4_value2=self.property4_value2, + converted_card_num=self.converted_card_num, + shop_purchase_flag=self.shop_purchase_flag, + protect_flag=self.protect_flag, + get_date_size=len(self.get_date) * 2, + get_date=[ord(x) for x in self.get_date], + + ) + + resp_data.hero_log_user_data_list.append(hero_data) + + 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 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] + + 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 + + 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) + +class SaoSynthesizeEnhancementEquipmentResponse(SaoBaseResponse): + def __init__(self, cmd, synthesize_equipment_data) -> None: + super().__init__(cmd) + self.result = 1 + equipment_level = 0 + + # 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: + + 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)], + ) + + # 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), + ) + + resp_data = resp_struct.parse(resp_struct.build(dict( + result=self.result, + after_equipment_user_data_size=0, + after_equipment_user_data=[], + ))) + + 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], + + ) + + resp_data.after_equipment_user_data.append(synthesize_equipment_data) + + resp_data["after_equipment_user_data_size"] = len(resp_data.after_equipment_user_data) + + # finally, rebuild the resp_data + resp_data = resp_struct.build(resp_data) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetDefragMatchBasicDataRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + +class SaoGetDefragMatchBasicDataResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + 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 + + 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 + +class SaoGetDefragMatchRankingUserDataRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + +class SaoGetDefragMatchRankingUserDataResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.ranking_user_data_size = 1 # number of arrays + + 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 + +class SaoGetDefragMatchLeaguePointRankingListRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + +class SaoGetDefragMatchLeaguePointRankingListResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + 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.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 + +class SaoGetDefragMatchLeagueScoreRankingListRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + +class SaoGetDefragMatchLeagueScoreRankingListResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + 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 + + 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) + return super().make() + resp_data + +class SaoBnidSerialCodeCheckRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + +class SaoBnidSerialCodeCheckResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + + self.result = 1 + self.bnid_item_id = "130050" + 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 + ) + + 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, + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoScanQrQuestProfileCardRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + +class SaoScanQrQuestProfileCardResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + 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 + + def make(self) -> bytes: + #new stuff + + 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, + ) + + resp_data.read_profile_card_data.append(hero_data) + + resp_data["read_profile_card_data_size"] = len(resp_data.read_profile_card_data) + + # finally, rebuild the resp_data + resp_data = resp_struct.build(resp_data) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoConsumeCreditGuestRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + shop_id = decode_str(data, off) + self.shop_id = shop_id[0] + off += shop_id[1] + + serial_num = decode_str(data, off) + self.serial_num = serial_num[0] + off += serial_num[1] + + self.cab_type = decode_byte(data, off) + off += BYTE_OFF + + self.act_type = decode_byte(data, off) + off += BYTE_OFF + + self.consume_num = decode_byte(data, off) + off += BYTE_OFF + +class SaoChangePartyRequest(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] + + self.act_type = decode_byte(data, off) + off += BYTE_OFF + + self.party_data_count = decode_int(data, off) + off += INT_OFF + + self.party_data_list: List[PartyData] = [] + + for _ in range(self.party_data_count): + tmp = PartyData(data, off) + self.party_data_list.append(tmp) + off += tmp.get_size() + +class TrialTowerPlayEndUnanalyzedLogFixed(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] + + self.trial_tower_id = decode_int(data, off) + off += INT_OFF + + 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 GetShopResourceSalesDataListResponse(SaoBaseResponse): + def __init__(self, cmd_id: int) -> None: + super().__init__(cmd_id) + self.result = 1 # byte + self.shop_resource_sales_data: List[ShopResourceSalesData] = [] + + def make(self) -> bytes: + ret = encode_byte(self.result) + ret += encode_arr_cls(self.shop_resource_sales_data) + + self.header.length = len(ret) + return super().make() + ret + +class GetYuiMedalShopUserDataListRequest(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 GetYuiMedalShopUserDataListResponse(SaoBaseResponse): + def __init__(self, cmd_id: int) -> None: + super().__init__(cmd_id) + self.result = 1 # byte + self.user_data_list: List[YuiMedalShopUserData] = [] + + def make(self) -> bytes: + ret = encode_byte(self.result) + ret += encode_arr_cls(self.user_data_list) + + self.header.length = len(ret) + return super().make() + ret + +class GetGashaMedalShopUserDataListRequest(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 GetGashaMedalShopUserDataListResponse(SaoBaseResponse): + def __init__(self, cmd_id: int) -> None: + super().__init__(cmd_id) + self.result = 1 # byte + self.data_list: List[GashaMedalShopUserData] = [] + + 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 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 diff --git a/titles/sao/handlers/helpers.py b/titles/sao/handlers/helpers.py new file mode 100644 index 0000000..6e11146 --- /dev/null +++ b/titles/sao/handlers/helpers.py @@ -0,0 +1,917 @@ +from typing import Tuple, List, Optional +import struct +import logging +from datetime import datetime + +BIGINT_OFF = 16 +LONG_OFF = 8 +INT_OFF = 4 +SHORT_OFF = 2 +BYTE_OFF = 1 + +DT_FMT = "%Y%m%d%H%M%S" + +def fmt_dt(d: Optional[datetime] = None) -> str: + if d is None: + d = datetime.fromtimestamp(0) + return d.strftime(DT_FMT) + +def prs_dt(s: Optional[str] = None) -> datetime: + if not s: + s = "19691231190000" + return datetime.strptime(s, DT_FMT) + +def decode_num(data: bytes, offset: int, size: int) -> int: + try: + return int.from_bytes(data[offset:offset + size], 'big') + except: + logging.getLogger('sao').error(f"Failed to parse {data[offset:offset + size]} as BE number of width {size}") + return 0 + +def decode_byte(data: bytes, offset: int) -> int: + return decode_num(data, offset, BYTE_OFF) + +def decode_short(data: bytes, offset: int) -> int: + return decode_num(data, offset, SHORT_OFF) + +def decode_int(data: bytes, offset: int) -> int: + return decode_num(data, offset, INT_OFF) + +def decode_long(data: bytes, offset: int) -> int: + return decode_num(data, offset, LONG_OFF) + +def decode_bigint(data: bytes, offset: int) -> int: + return decode_num(data, offset, BIGINT_OFF) + +def decode_str(data: bytes, offset: int) -> Tuple[str, int]: + try: + str_len = decode_int(data, offset) + num_bytes_decoded = INT_OFF + str_len + str_out = data[offset + INT_OFF:offset + num_bytes_decoded].decode("utf-16-le", errors="replace") + return (str_out, num_bytes_decoded) + except: + logging.getLogger('sao').error(f"Failed to parse {data[offset:]} as string!") + return ("", 0) + +def decode_arr_num(data: bytes, offset:int, element_size: int) -> Tuple[List[int], int]: + size = 0 + num_obj = decode_int(data, offset + size) + size += INT_OFF + + ret: List[int] = [] + for _ in range(num_obj): + ret.append(decode_num(data, offset + size, element_size)) + size += element_size + + return (ret, size) + +def decode_arr_str(data: bytes, offset: int) -> Tuple[List[str], int]: + size = 0 + num_obj = decode_int(data, offset + size) + size += INT_OFF + + ret: List[str] = [] + for _ in range(num_obj): + tmp = decode_str(data, offset + size) + ret.append(tmp[0]) + size += tmp[1] + + return (ret, size) + +def encode_byte(data: int) -> bytes: + return struct.pack("!b", data) + +def encode_short(data: int) -> bytes: + return struct.pack("!h", data) + +def encode_int(data: int) -> bytes: + return struct.pack("!i", data) + +def encode_long(data: int) -> bytes: + return struct.pack("!l", data) + +def encode_bigint(data: int) -> bytes: + return struct.pack("!q", data) + +def encode_str(s: str) -> bytes: + try: + str_bytes = 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!") + return b"" + +def encode_arr_num(data: List[int], element_size: int) -> bytes: + ret = encode_int(len(data)) + + if element_size == BYTE_OFF: + for x in data: + ret += encode_byte(x) + elif element_size == SHORT_OFF: + for x in data: + ret += encode_short(x) + elif element_size == INT_OFF: + for x in data: + ret += encode_int(x) + elif element_size == LONG_OFF: + for x in data: + ret += encode_long(x) + elif element_size == BIGINT_OFF: + for x in data: + ret += encode_bigint(x) + else: + logging.getLogger('sao').error(f"Unknown element size {element_size}") + return b"\x00" * INT_OFF + + return ret + +class BaseHelper: + def __init__(self, data: bytes, offset: int) -> None: + self._sz = 0 + + @classmethod + def from_args(cls) -> "BaseHelper": + return cls(b"", 0) + + def get_size(self) -> int: + return self._sz + + def make(self) -> bytes: + return b"" + +def decode_arr_cls(data: bytes, offset: int, cls: BaseHelper): + size = 0 + num_cls = decode_int(data, offset + size) + cls_type = type(cls) + + ret: List[cls_type] = [] + for _ in range(num_cls): + tmp = cls(data, offset + size) + size += tmp.get_size() + ret.append(tmp) + + return (ret, size) + +def encode_arr_cls(data: List[BaseHelper]) -> bytes: + ret = encode_int(len(data)) + + for x in data: + ret += x.make() + + return ret + +class MaterialCommonRewardUserData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + self.common_reward_type = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + + user_common_reward_id = decode_str(data, offset + self._sz) + self.user_common_reward_id = user_common_reward_id[0] + self._sz += user_common_reward_id[1] + +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] + + self.arrangement_num = decode_byte(data, offset + sz) + sz += 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] + + 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] + + 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.skill_slot1_skill_id = decode_int(data, offset + sz) + sz += INT_OFF + + self.skill_slot2_skill_id = decode_int(data, offset + sz) + sz += INT_OFF + + self.skill_slot3_skill_id = decode_int(data, offset + sz) + sz += INT_OFF + + self.skill_slot4_skill_id = decode_int(data, offset + sz) + sz += INT_OFF + + self.skill_slot5_skill_id = decode_int(data, offset + sz) + sz += INT_OFF + + self._sz = sz + +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] + + 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.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._sz = sz + +class PlayStartRequestData(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] + + appoint_leader_resource_card_code = decode_str(data, offset + sz) + self.appoint_leader_resource_card_code = appoint_leader_resource_card_code[0] + sz += appoint_leader_resource_card_code[1] + + use_profile_card_code = decode_str(data, offset + sz) + self.use_profile_card_code = use_profile_card_code[0] + sz += use_profile_card_code[1] + + self.quest_drop_boost_apply_flag = decode_byte(data, offset + sz) + sz += BYTE_OFF + + self._sz = sz + +class GetPlayerTraceData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + user_quest_scene_player_trace_id = decode_str(data, offset) + self.user_quest_scene_player_trace_id = user_quest_scene_player_trace_id[0] + self._sz = user_quest_scene_player_trace_id[1] + +class BaseGetData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + self.get_hero_log_exp = decode_int(data, offset) + offset += INT_OFF + + self.get_col = decode_int(data, offset) + + self._sz = INT_OFF + INT_OFF + +class RareDropData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + self.quest_rare_drop_id = decode_int(data, offset) + + self._sz = INT_OFF + +class UnanalyzedLogTmpRewardData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + self.unanalyzed_log_grade_id = decode_int(data, offset) + + self._sz = INT_OFF + +class SpecialRareDropData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + self.quest_special_rare_drop_id = decode_int(data, offset) + + self._sz = INT_OFF + +class EventItemData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + self.event_item_id = decode_int(data, offset) + offset += INT_OFF + self.get_num = decode_short(data, offset) + + self._sz = INT_OFF + SHORT_OFF + +class DiscoveryEnemyData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + self.enemy_kind_id = decode_int(data, offset) + offset += INT_OFF + self.destroy_num = decode_short(data, offset) + + self._sz = INT_OFF + SHORT_OFF + +class DestroyBossData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + self.boss_type = decode_byte(data, offset) + offset += BYTE_OFF + self.enemy_kind_id = decode_int(data, offset) + offset += INT_OFF + self.mission_difficulty_id = decode_short(data, offset) + + self._sz = INT_OFF + SHORT_OFF + BYTE_OFF + +class MissionData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + self.mission_id = decode_int(data, offset) + offset += INT_OFF + self.clear_flag = decode_byte(data, offset) + offset += BYTE_OFF + self.destroy_num = decode_short(data, offset) + + self._sz = INT_OFF + SHORT_OFF + BYTE_OFF + +class ScoreData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + self.clear_time = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + self.combo_num = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + total_damage = decode_str(data, offset + self._sz) + self.total_damage = total_damage[0] + self._sz += total_damage[1] + + self.concurrent_destroying_num = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + + self.reaching_skill_level = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + + self.ko_chara_num = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF + + self.acceleration_invocation_num = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + + self.boss_destroying_num = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + + self.synchro_skill_used_flag = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF + + self.used_friend_skill_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + self.friend_skill_used_flag = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF + + self.continue_cnt = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + + self.total_loss_num = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + +class PlayEndRequestData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + sz = 0 + self.play_result_flag = decode_byte(data, offset + sz) + sz += BYTE_OFF + + self.base_get_data_count = decode_int(data, offset + sz) + sz += INT_OFF + + self.base_get_data_list: List[BaseGetData] = [] + for _ in range(self.base_get_data_count): + tmp = BaseGetData(data, offset + sz) + sz += tmp.get_size() + self.base_get_data_list.append(tmp) + + self.get_player_trace_data_count = decode_int(data, offset + sz) + sz += INT_OFF + + self.get_player_trace_data: List[GetPlayerTraceData] = [] + for _ in range(self.get_player_trace_data_count): + tmp = GetPlayerTraceData(data, offset + sz) + sz += tmp.get_size() + self.get_player_trace_data.append(tmp) + + self.get_rare_drop_data_count = decode_int(data, offset + sz) + sz += INT_OFF + + self.get_rare_drop_data_list: List[RareDropData] = [] + for _ in range(self.get_rare_drop_data_count): + tmp = RareDropData(data, offset + sz) + sz += tmp.get_size() + self.get_rare_drop_data_list.append(tmp) + + self.get_special_rare_drop_data_count = decode_int(data, offset + sz) + sz += INT_OFF + + self.get_special_rare_drop_data_list: List[SpecialRareDropData] = [] + for _ in range(self.get_special_rare_drop_data_count): + tmp = SpecialRareDropData(data, offset + sz) + sz += tmp.get_size() + self.get_special_rare_drop_data_list.append(tmp) + + self.get_unanalyzed_log_tmp_reward_data_count = decode_int(data, offset + sz) + sz += INT_OFF + + self.get_unanalyzed_log_tmp_reward_data_list: List[UnanalyzedLogTmpRewardData] = [] + for _ in range(self.get_unanalyzed_log_tmp_reward_data_count): + tmp = UnanalyzedLogTmpRewardData(data, offset + sz) + sz += tmp.get_size() + self.get_unanalyzed_log_tmp_reward_data_list.append(tmp) + + self.get_event_item_data_count = decode_int(data, offset + sz) + sz += INT_OFF + + self.get_event_item_data_list: List[EventItemData] = [] + for _ in range(self.get_event_item_data_count): + tmp = EventItemData(data, offset + sz) + sz += tmp.get_size() + self.get_event_item_data_list.append(tmp) + + self.discovery_enemy_data_count = decode_int(data, offset + sz) + sz += INT_OFF + + self.discovery_enemy_data_list: List[DiscoveryEnemyData] = [] + for _ in range(self.discovery_enemy_data_count): + tmp = DiscoveryEnemyData(data, offset + sz) + sz += tmp.get_size() + self.discovery_enemy_data_list.append(tmp) + + self.destroy_boss_data_count = decode_int(data, offset + sz) + sz += INT_OFF + + self.destroy_boss_data_list: List[DestroyBossData] = [] + for _ in range(self.destroy_boss_data_count): + tmp = DestroyBossData(data, offset + sz) + sz += tmp.get_size() + self.destroy_boss_data_list.append(tmp) + + self.mission_data_count = decode_int(data, offset + sz) + sz += INT_OFF + + self.mission_data_list: List[MissionData] = [] + for _ in range(self.mission_data_count): + tmp = MissionData(data, offset + sz) + sz += tmp.get_size() + self.mission_data_list.append(tmp) + + self.score_data_count = decode_int(data, offset + sz) + sz += INT_OFF + + self.score_data_list: List[ScoreData] = [] + for _ in range(self.score_data_count): + tmp = ScoreData(data, offset + sz) + sz += tmp.get_size() + self.score_data_list.append(tmp) + + self._sz = sz + +class EntryUserData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + store_id = decode_str(data, offset + self._sz) + self.store_id = store_id[0] + self._sz += store_id[1] + + user_id = decode_str(data, offset + self._sz) + self.user_id = user_id[0] + self._sz += user_id[1] + + self.host_flag = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF + +class MultiPlayStartRequestData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + room_id = decode_str(data, offset + self._sz) + self.room_id = room_id[0] + self._sz += room_id[1] + + self.matching_mode = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF + + self.entry_user_data_count = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + self.entry_user_data_list: List[EntryUserData] = [] + for _ in range(self.entry_user_data_count): + tmp = EntryUserData(data, offset + self._sz) + self._sz += tmp.get_size() + self.entry_user_data_list.append(tmp) + +class MultiPlayEndRequestData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + self.dummy_1 = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF + self.dummy_2 = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF + self.dummy_3 = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF + +class SalesResourceData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + self.common_reward_type = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + self.common_reward_id = 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, reward_type: int = 0, reward_id: int = 0) -> "SalesResourceData": + ret = cls(b"\x00" * 54, 0) + ret.common_reward_type = reward_type # short + ret.common_reward_id = reward_id # int + + return ret + + def make(self) -> bytes: + ret = b"" + ret += encode_short(self.common_reward_type) + ret += encode_int(self.common_reward_id) + + 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) + +class ShopResourceSalesData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + user_shop_resource_id = decode_str(data, offset + self._sz) + self.user_shop_resource_id = user_shop_resource_id[0] + self._sz = user_shop_resource_id[1] + + discharge_user_id = decode_str(data, offset + self._sz) + self.discharge_user_id = discharge_user_id[0] + self._sz = discharge_user_id[1] + + self.remaining_num = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + self.purchase_num = 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_resource_data_list = decode_arr_cls(data, offset + self._sz, SalesResourceData) + self.sales_resource_data_list: List[SalesResourceData] = sales_resource_data_list[0] + self._sz += sales_resource_data_list[1] + + @classmethod + def from_args(cls, resource_id: str = "0", discharge_id: str = "0", remaining: int = 0, purchased: int = 0) -> "ShopResourceSalesData": + ret = cls(b"\x00" * 20, 0) + ret.user_shop_resource_id = resource_id + ret.discharge_user_id = discharge_id + ret.remaining_num = remaining # short + ret.purchase_num = purchased # short + ret.sales_start_date = prs_dt() + + def make(self) -> bytes: + ret = encode_str(self.user_shop_resource_id) + 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_arr_cls(self.sales_resource_data_list) + return ret + +class YuiMedalShopUserData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + self.yui_medal_shop_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + self.purchase_num = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + last_purchase_date = decode_str(data, offset + self._sz) + self.last_purchase_date = last_purchase_date[0] + self._sz += last_purchase_date[1] + + @classmethod + def from_args(cls, yui_medal_shop_id: int = 0, purchase_num: int = 0, last_purchase_date: datetime = datetime.fromtimestamp(0)) -> "YuiMedalShopUserData": + ret = cls(b"\x00" * 20, 0) + ret.yui_medal_shop_id = yui_medal_shop_id + ret.purchase_num = purchase_num + ret.last_purchase_date = last_purchase_date + return ret + + 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)) + return ret + +class GashaMedalShopUserData(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 + + self.purchase_num = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + @classmethod + def from_args(cls, gasha_medal_shop_id: int = 0, purchase_num: int = 0) -> "GashaMedalShopUserData": + ret = cls(b"\x00" * 20, 0) + ret.gasha_medal_shop_id = gasha_medal_shop_id + ret.purchase_num = purchase_num + return ret + + def make(self) -> bytes: + ret = encode_int(self.gasha_medal_shop_id) + 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 diff --git a/titles/sao/index.py b/titles/sao/index.py new file mode 100644 index 0000000..78d641e --- /dev/null +++ b/titles/sao/index.py @@ -0,0 +1,157 @@ +from typing import Tuple, Dict, List +from twisted.web.http import Request +import yaml +import logging, coloredlogs +from logging.handlers import TimedRotatingFileHandler +from os import path +from Crypto.Cipher import Blowfish +from hashlib import md5 +import secrets + +from core import CoreConfig, Utils +from core.title import BaseServlet +from titles.sao.config import SaoConfig +from titles.sao.const import SaoConstants +from titles.sao.base import SaoBase +from titles.sao.handlers.base import * + + +class SaoServlet(BaseServlet): + def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: + super().__init__(core_cfg, cfg_dir) + self.config_dir = cfg_dir + self.game_cfg = SaoConfig() + if path.exists(f"{cfg_dir}/sao.yaml"): + self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/sao.yaml"))) + + self.logger = logging.getLogger("sao") + if not hasattr(self.logger, "inited"): + log_fmt_str = "[%(asctime)s] SAO | %(levelname)s | %(message)s" + log_fmt = logging.Formatter(log_fmt_str) + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.core_cfg.server.log_dir, "sao"), + encoding="utf8", + when="d", + backupCount=10, + ) + + fileHandler.setFormatter(log_fmt) + + consoleHandler = logging.StreamHandler() + consoleHandler.setFormatter(log_fmt) + + self.logger.addHandler(fileHandler) + self.logger.addHandler(consoleHandler) + + self.logger.setLevel(self.game_cfg.server.loglevel) + coloredlogs.install( + level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str + ) + self.logger.inited = True + + self.base = SaoBase(core_cfg, self.game_cfg) + 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 + + def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: + return ( + [], + [("render_POST", "/{datecode}/proto/if/{category}/{endpoint}", {})] + ) + + @classmethod + def is_game_enabled(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> bool: + game_cfg = SaoConfig() + + if path.exists(f"{cfg_dir}/{SaoConstants.CONFIG_NAME}"): + game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{SaoConstants.CONFIG_NAME}")) + ) + + if not game_cfg.server.enable: + return False + + return True + + 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 not self.core_cfg.server.is_using_proxy and port_normal != 80 else "" + + if self.game_cfg.server.use_https: + proto = "https" + port = f":{port_ssl}" if not self.core_cfg.server.is_using_proxy and port_ssl != 443 else "" + + return (f"{proto}://{self.core_cfg.title.hostname}{port}/", "") + + def get_mucha_info(self, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str]: + if not self.game_cfg.server.enable: + return (False, "") + + return (True, "SAO1") + + def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes: + endpoint = matchers.get('endpoint', '') + request.responseHeaders.addRawHeader(b"content-type", b"text/html; charset=utf-8") + iv = b"" + + req_raw = request.content.read() + if len(req_raw) < 40: + self.logger.warn(f"Malformed request to {endpoint} - {req_raw.hex()}") + return b"" + req_header = SaoRequestHeader(req_raw) + + cmd_str = f"{req_header.cmd:04x}" + + if self.game_cfg.hash.verify_hash and self.static_hash != req_header.hash: + self.logger.error(f"Hash mismatch! Expecting {self.static_hash} but recieved {req_header.hash}") + return b"" + + if self.game_cfg.crypt.enable: + iv = req_raw[40:48] + cipher = Blowfish.new(self.game_cfg.crypt.key.encode(), Blowfish.MODE_CBC, iv) + crypt_data = req_raw[48:] + req_data = cipher.decrypt(crypt_data) + self.logger.debug(f"Decrypted {req_data.hex()} with IV {iv.hex()}") + + 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 = handler(req_header, req_data) + + if resp is None: + resp = SaoNoopResponse(req_header.cmd + 1).make() + + if type(resp) == bytes: + pass + + elif issubclass(resp, SaoBaseResponse): + resp = resp.make() + + else: + self.logger.error(f"Unknown response type {type(resp)}") + return b"" + + self.logger.debug(f"Response: {resp.hex()}") + + if self.game_cfg.crypt.enable: + iv = secrets.token_bytes(8) + data_to_crypt = resp[24:] + while len(data_to_crypt) % 8 != 0: + data_to_crypt += b"\x00" + + cipher = Blowfish.new(self.game_cfg.crypt.key.encode(), Blowfish.MODE_CBC, iv) + data_crypt = cipher.encrypt(data_to_crypt) + crypt_data_len = len(data_crypt) + len(iv) + 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 resp \ No newline at end of file diff --git a/titles/sao/read.py b/titles/sao/read.py new file mode 100644 index 0000000..dafb450 --- /dev/null +++ b/titles/sao/read.py @@ -0,0 +1,254 @@ +from typing import Optional, Dict, List +from os import walk, path +import urllib +import csv + +from read import BaseReader +from core.config import CoreConfig +from titles.sao.database import SaoData +from titles.sao.const import SaoConstants + + +class SaoReader(BaseReader): + def __init__( + self, + config: CoreConfig, + version: int, + bin_arg: Optional[str], + opt_arg: Optional[str], + extra: Optional[str], + ) -> None: + super().__init__(config, version, bin_arg, opt_arg, extra) + self.data = SaoData(config) + + try: + self.logger.info( + f"Start importer for {SaoConstants.game_ver_to_string(version)}" + ) + except IndexError: + self.logger.error(f"Invalid project SAO version {version}") + exit(1) + + def read(self) -> None: + pull_bin_ram = True + + if not path.exists(f"{self.bin_dir}"): + self.logger.warning(f"Couldn't find csv file in {self.bin_dir}, skipping") + pull_bin_ram = False + + if pull_bin_ram: + self.read_csv(f"{self.bin_dir}") + + def read_csv(self, bin_dir: str) -> None: + self.logger.info(f"Read csv from {bin_dir}") + + self.logger.info("Now reading QuestScene.csv") + try: + fullPath = bin_dir + "/QuestScene.csv" + with open(fullPath, encoding="UTF-8") as fp: + reader = csv.DictReader(fp) + for row in reader: + questSceneId = row["QuestSceneId"] + sortNo = row["SortNo"] + name = row["Name"] + enabled = True + + self.logger.info(f"Added quest {questSceneId} | Name: {name}") + + try: + self.data.static.put_quest( + questSceneId, + 0, + sortNo, + name, + enabled + ) + except Exception as err: + 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(f"Added hero {heroLogId} | Name: {name}") + + try: + self.data.static.put_hero( + 0, + heroLogId, + name, + nickname, + rarity, + skillTableSubId, + awakeningExp, + flavorText, + enabled + ) + except Exception as err: + 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 + + self.logger.info(f"Added equipment {equipmentId} | Name: {name}") + + try: + self.data.static.put_equipment( + 0, + equipmentId, + name, + equipmentType, + weaponTypeId, + rarity, + flavorText, + enabled + ) + except Exception as err: + 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 Item.csv") + try: + fullPath = bin_dir + "/Item.csv" + with open(fullPath, encoding="UTF-8") as fp: + reader = csv.DictReader(fp) + for row in reader: + itemId = row["ItemId"] + itemTypeId = row["ItemTypeId"] + name = row["Name"] + rarity = row["Rarity"] + flavorText = row["FlavorText"] + enabled = True + + self.logger.info(f"Added item {itemId} | Name: {name}") + + try: + self.data.static.put_item( + 0, + itemId, + name, + itemTypeId, + rarity, + flavorText, + enabled + ) + except Exception as err: + 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 + + self.logger.info(f"Added support log {supportLogId} | Name: {name}") + + try: + self.data.static.put_support_log( + 0, + supportLogId, + charaId, + name, + rarity, + salePrice, + skillName, + enabled + ) + except Exception as err: + 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 + + self.logger.info(f"Added title {titleId} | Name: {displayName}") + + if len(titleId) > 5: + try: + self.data.static.put_title( + 0, + titleId, + displayName, + requirement, + rank, + imageFilePath, + enabled + ) + except Exception as err: + 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("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(f"Added rare drop {questRareDropId} | Reward: {commonRewardId}") + + try: + 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") diff --git a/titles/sao/schema/__init__.py b/titles/sao/schema/__init__.py new file mode 100644 index 0000000..3e75fc0 --- /dev/null +++ b/titles/sao/schema/__init__.py @@ -0,0 +1,3 @@ +from .profile import SaoProfileData +from .static import SaoStaticData +from .item import SaoItemData \ No newline at end of file diff --git a/titles/sao/schema/item.py b/titles/sao/schema/item.py new file mode 100644 index 0000000..11adf27 --- /dev/null +++ b/titles/sao/schema/item.py @@ -0,0 +1,506 @@ +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON +from sqlalchemy.schema import ForeignKey +from sqlalchemy.sql import func, select, update, delete +from sqlalchemy.engine import Row +from sqlalchemy.dialects.mysql import insert + +from core.data.schema import BaseData, metadata + +equipment_data = Table( + "sao_equipment_data", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("equipment_id", Integer, 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("get_date", TIMESTAMP, nullable=False, server_default=func.now()), + UniqueConstraint("user", "equipment_id", name="sao_equipment_data_uk"), + mysql_charset="utf8mb4", +) + +item_data = Table( + "sao_item_data", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("item_id", Integer, nullable=False), + Column("get_date", TIMESTAMP, nullable=False, server_default=func.now()), + UniqueConstraint("user", "item_id", name="sao_item_data_uk"), + mysql_charset="utf8mb4", +) + +hero_log_data = Table( + "sao_hero_log_data", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("user_hero_log_id", Integer, nullable=False), + Column("log_level", Integer, nullable=False), + Column("log_exp", Integer, nullable=False), + Column("main_weapon", Integer, nullable=False), + Column("sub_equipment", Integer, nullable=False), + Column("skill_slot1_skill_id", Integer, nullable=False), + Column("skill_slot2_skill_id", Integer, nullable=False), + Column("skill_slot3_skill_id", Integer, nullable=False), + Column("skill_slot4_skill_id", Integer, nullable=False), + Column("skill_slot5_skill_id", Integer, nullable=False), + Column("get_date", TIMESTAMP, nullable=False, server_default=func.now()), + UniqueConstraint("user", "user_hero_log_id", name="sao_hero_log_data_uk"), + mysql_charset="utf8mb4", +) + +hero_party = Table( + "sao_hero_party", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("user_party_team_id", Integer, nullable=False), + Column("user_hero_log_id_1", Integer, nullable=False), + Column("user_hero_log_id_2", Integer, nullable=False), + Column("user_hero_log_id_3", Integer, nullable=False), + UniqueConstraint("user", "user_party_team_id", name="sao_hero_party_uk"), + mysql_charset="utf8mb4", +) + +quest = Table( + "sao_player_quest", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("episode_id", Integer, 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", +) + +sessions = Table( + "sao_play_sessions", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("user_party_team_id", Integer, nullable=False), + Column("episode_id", Integer, nullable=False), + Column("play_mode", Integer, nullable=False), + Column("quest_drop_boost_apply_flag", Integer, nullable=False), + Column("play_date", TIMESTAMP, nullable=False, server_default=func.now()), + UniqueConstraint("user", "user_party_team_id", "play_date", name="sao_play_sessions_uk"), + mysql_charset="utf8mb4", +) + +end_sessions = Table( + "sao_end_sessions", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("quest_id", Integer, nullable=False), + Column("play_result_flag", Boolean, nullable=False), + Column("reward_data", JSON, nullable=True), + Column("play_date", TIMESTAMP, nullable=False, server_default=func.now()), + mysql_charset="utf8mb4", +) + +class SaoItemData(BaseData): + def create_session(self, user_id: int, user_party_team_id: int, episode_id: int, play_mode: int, quest_drop_boost_apply_flag: int) -> Optional[int]: + sql = insert(sessions).values( + user=user_id, + user_party_team_id=user_party_team_id, + episode_id=episode_id, + play_mode=play_mode, + quest_drop_boost_apply_flag=quest_drop_boost_apply_flag + ) + + conflict = sql.on_duplicate_key_update(user=user_id) + + result = self.execute(conflict) + if result is None: + self.logger.error(f"Failed to create SAO session for user {user_id}!") + return None + return result.lastrowid + + def create_end_session(self, user_id: int, quest_id: int, play_result_flag: bool, reward_data: JSON) -> Optional[int]: + sql = insert(end_sessions).values( + user=user_id, + quest_id=quest_id, + play_result_flag=play_result_flag, + reward_data=reward_data, + ) + + conflict = sql.on_duplicate_key_update(user=user_id) + + result = self.execute(conflict) + if result is None: + self.logger.error(f"Failed to create SAO end session for user {user_id}!") + return None + return result.lastrowid + + def put_item(self, user_id: int, item_id: int) -> Optional[int]: + sql = insert(item_data).values( + user=user_id, + item_id=item_id, + ) + + conflict = sql.on_duplicate_key_update( + item_id=item_id, + ) + + result = self.execute(conflict) + if result is None: + self.logger.error( + f"{__name__} failed to insert item! user: {user_id}, item_id: {item_id}" + ) + return None + + return result.lastrowid + + def put_equipment_data(self, user_id: int, equipment_id: int, enhancement_value: int, enhancement_exp: int, awakening_exp: int, awakening_stage: int, possible_awakening_flag: int) -> Optional[int]: + sql = insert(equipment_data).values( + 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, + ) + + 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, + ) + + result = 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 + + return result.lastrowid + + def put_hero_log(self, user_id: int, user_hero_log_id: int, log_level: int, log_exp: int, main_weapon: int, sub_equipment: int, skill_slot1_skill_id: int, skill_slot2_skill_id: int, skill_slot3_skill_id: int, skill_slot4_skill_id: int, skill_slot5_skill_id: int) -> Optional[int]: + sql = insert(hero_log_data).values( + user=user_id, + user_hero_log_id=user_hero_log_id, + log_level=log_level, + log_exp=log_exp, + main_weapon=main_weapon, + sub_equipment=sub_equipment, + skill_slot1_skill_id=skill_slot1_skill_id, + skill_slot2_skill_id=skill_slot2_skill_id, + skill_slot3_skill_id=skill_slot3_skill_id, + skill_slot4_skill_id=skill_slot4_skill_id, + skill_slot5_skill_id=skill_slot5_skill_id, + ) + + conflict = sql.on_duplicate_key_update( + log_level=log_level, + log_exp=log_exp, + main_weapon=main_weapon, + sub_equipment=sub_equipment, + skill_slot1_skill_id=skill_slot1_skill_id, + skill_slot2_skill_id=skill_slot2_skill_id, + skill_slot3_skill_id=skill_slot3_skill_id, + skill_slot4_skill_id=skill_slot4_skill_id, + skill_slot5_skill_id=skill_slot5_skill_id, + ) + + result = self.execute(conflict) + if result is None: + self.logger.error( + f"{__name__} failed to insert hero! user: {user_id}, user_hero_log_id: {user_hero_log_id}" + ) + return None + + return result.lastrowid + + def put_hero_party(self, user_id: int, user_party_team_id: int, user_hero_log_id_1: int, user_hero_log_id_2: int, user_hero_log_id_3: int) -> Optional[int]: + sql = insert(hero_party).values( + user=user_id, + user_party_team_id=user_party_team_id, + user_hero_log_id_1=user_hero_log_id_1, + user_hero_log_id_2=user_hero_log_id_2, + user_hero_log_id_3=user_hero_log_id_3, + ) + + conflict = sql.on_duplicate_key_update( + user_hero_log_id_1=user_hero_log_id_1, + user_hero_log_id_2=user_hero_log_id_2, + user_hero_log_id_3=user_hero_log_id_3, + ) + + result = self.execute(conflict) + if result is None: + self.logger.error( + f"{__name__} failed to insert hero party! user: {user_id}, user_party_team_id: {user_party_team_id}" + ) + return None + + return result.lastrowid + + def 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]: + sql = insert(quest).values( + user=user_id, + episode_id=episode_id, + quest_clear_flag=quest_clear_flag, + clear_time=clear_time, + combo_num=combo_num, + total_damage=total_damage, + concurrent_destroying_num=concurrent_destroying_num + ) + + conflict = sql.on_duplicate_key_update( + quest_clear_flag=quest_clear_flag, + clear_time=clear_time, + combo_num=combo_num, + total_damage=total_damage, + concurrent_destroying_num=concurrent_destroying_num + ) + + result = 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 + + 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) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_user_equipments( + self, user_id: int + ) -> Optional[List[Row]]: + """ + A catch-all equipments lookup given a profile + """ + sql = equipment_data.select( + and_( + equipment_data.c.user == user_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_user_items( + self, user_id: int + ) -> Optional[List[Row]]: + """ + A catch-all items lookup given a profile + """ + sql = item_data.select( + and_( + item_data.c.user == user_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_hero_log( + self, user_id: int, user_hero_log_id: int = None + ) -> Optional[List[Row]]: + """ + A catch-all hero lookup given a profile and user_party_team_id and ID specifiers + """ + sql = hero_log_data.select( + and_( + hero_log_data.c.user == user_id, + hero_log_data.c.user_hero_log_id == user_hero_log_id if user_hero_log_id is not None else True, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_hero_logs( + self, user_id: int + ) -> Optional[List[Row]]: + """ + A catch-all hero lookup given a profile + """ + sql = hero_log_data.select( + and_( + hero_log_data.c.user == user_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_hero_party( + self, user_id: int, user_party_team_id: int = None + ) -> Optional[List[Row]]: + sql = hero_party.select( + and_( + hero_party.c.user == user_id, + hero_party.c.user_party_team_id == user_party_team_id if user_party_team_id is not None else True, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_quest_log( + self, user_id: int, episode_id: int = None + ) -> Optional[List[Row]]: + """ + A catch-all quest lookup given a profile and episode_id + """ + sql = quest.select( + and_( + quest.c.user == user_id, + quest.c.episode_id == episode_id if episode_id is not None else True, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + 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 = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_session( + self, user_id: int = None + ) -> Optional[List[Row]]: + sql = sessions.select( + and_( + sessions.c.user == user_id, + ) + ).order_by( + sessions.c.play_date.asc() + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_end_session( + self, user_id: int = None + ) -> Optional[List[Row]]: + sql = end_sessions.select( + and_( + end_sessions.c.user == user_id, + ) + ).order_by( + end_sessions.c.play_date.asc() + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def remove_hero_log(self, user_id: int, user_hero_log_id: int) -> None: + sql = hero_log_data.delete( + and_( + hero_log_data.c.user == user_id, + hero_log_data.c.user_hero_log_id == user_hero_log_id, + ) + ) + + result = 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 + + 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) + ) + + result = 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 + + def remove_item(self, user_id: int, item_id: int) -> None: + sql = item_data.delete( + and_(item_data.c.user == user_id, item_data.c.item_id == item_id) + ) + + result = self.execute(sql) + if result is None: + self.logger.error( + f"{__name__} failed to remove item! profile: {user_id}, item_id: {item_id}" + ) + return None \ No newline at end of file diff --git a/titles/sao/schema/profile.py b/titles/sao/schema/profile.py new file mode 100644 index 0000000..d7320cc --- /dev/null +++ b/titles/sao/schema/profile.py @@ -0,0 +1,79 @@ +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON +from sqlalchemy.schema import ForeignKey +from sqlalchemy.sql import func, select, update, delete +from sqlalchemy.engine import Row +from sqlalchemy.dialects.mysql import insert + +from core.data.schema import BaseData, metadata +from ..const import SaoConstants + +profile = Table( + "sao_profile", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + unique=True, + ), + Column("user_type", Integer, server_default="1"), + Column("nick_name", String(16), server_default="PLAYER"), + Column("rank_num", Integer, server_default="1"), + Column("rank_exp", Integer, server_default="0"), + Column("own_col", Integer, server_default="0"), + Column("own_vp", Integer, server_default="0"), + Column("own_yui_medal", Integer, server_default="0"), + Column("setting_title_id", Integer, server_default="20005"), +) + +class SaoProfileData(BaseData): + def create_profile(self, user_id: int) -> Optional[int]: + sql = insert(profile).values(user=user_id) + conflict = sql.on_duplicate_key_update(user=user_id) + + result = self.execute(conflict) + if result is None: + self.logger.error(f"Failed to create SAO profile for user {user_id}!") + return None + return result.lastrowid + + def put_profile(self, user_id: int, user_type: int, nick_name: str, rank_num: int, rank_exp: int, own_col: int, own_vp: int, own_yui_medal: int, setting_title_id: int) -> Optional[int]: + sql = insert(profile).values( + user=user_id, + user_type=user_type, + nick_name=nick_name, + rank_num=rank_num, + rank_exp=rank_exp, + own_col=own_col, + own_vp=own_vp, + own_yui_medal=own_yui_medal, + setting_title_id=setting_title_id + ) + + conflict = sql.on_duplicate_key_update( + rank_num=rank_num, + rank_exp=rank_exp, + own_col=own_col, + own_vp=own_vp, + own_yui_medal=own_yui_medal, + setting_title_id=setting_title_id + ) + + result = self.execute(conflict) + if result is None: + self.logger.error( + f"{__name__} failed to insert profile! user: {user_id}" + ) + return None + + return result.lastrowid + + def get_profile(self, user_id: int) -> Optional[Row]: + sql = profile.select(profile.c.user == user_id) + result = self.execute(sql) + if result is None: + return None + return result.fetchone() \ No newline at end of file diff --git a/titles/sao/schema/static.py b/titles/sao/schema/static.py new file mode 100644 index 0000000..ce9a6a9 --- /dev/null +++ b/titles/sao/schema/static.py @@ -0,0 +1,368 @@ +from typing import Dict, List, Optional +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, Float +from sqlalchemy.engine.base import Connection +from sqlalchemy.engine import Row +from sqlalchemy.schema import ForeignKey +from sqlalchemy.sql import func, select +from sqlalchemy.dialects.mysql import insert + +from core.data.schema import BaseData, metadata + +quest = Table( + "sao_static_quest", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer), + Column("questSceneId", Integer), + Column("sortNo", Integer), + Column("name", String(255)), + Column("enabled", Boolean), + UniqueConstraint( + "version", "questSceneId", name="sao_static_quest_uk" + ), + mysql_charset="utf8mb4", +) + +hero = Table( + "sao_static_hero_list", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer), + Column("heroLogId", Integer), + Column("name", String(255)), + Column("nickname", String(255)), + Column("rarity", Integer), + Column("skillTableSubId", Integer), + Column("awakeningExp", Integer), + Column("flavorText", String(255)), + Column("enabled", Boolean), + UniqueConstraint( + "version", "heroLogId", name="sao_static_hero_list_uk" + ), + mysql_charset="utf8mb4", +) + +equipment = Table( + "sao_static_equipment_list", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer), + Column("equipmentId", Integer), + Column("equipmentType", Integer), + Column("weaponTypeId", Integer), + Column("name", String(255)), + Column("rarity", Integer), + Column("flavorText", String(255)), + Column("enabled", Boolean), + UniqueConstraint( + "version", "equipmentId", name="sao_static_equipment_list_uk" + ), + mysql_charset="utf8mb4", +) + +item = Table( + "sao_static_item_list", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer), + Column("itemId", Integer), + Column("itemTypeId", Integer), + Column("name", String(255)), + Column("rarity", Integer), + Column("flavorText", String(255)), + Column("enabled", Boolean), + UniqueConstraint( + "version", "itemId", name="sao_static_item_list_uk" + ), + mysql_charset="utf8mb4", +) + +support = Table( + "sao_static_support_log_list", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer), + Column("supportLogId", Integer), + Column("charaId", Integer), + Column("name", String(255)), + Column("rarity", Integer), + Column("salePrice", Integer), + Column("skillName", String(255)), + Column("enabled", Boolean), + UniqueConstraint( + "version", "supportLogId", name="sao_static_support_log_list_uk" + ), + mysql_charset="utf8mb4", +) + +rare_drop = Table( + "sao_static_rare_drop_list", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer), + Column("questRareDropId", Integer), + Column("commonRewardId", Integer), + Column("enabled", Boolean), + UniqueConstraint( + "version", "questRareDropId", "commonRewardId", name="sao_static_rare_drop_list_uk" + ), + mysql_charset="utf8mb4", +) + +title = Table( + "sao_static_title_list", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer), + Column("titleId", Integer), + Column("displayName", String(255)), + Column("requirement", Integer), + Column("rank", Integer), + Column("imageFilePath", String(255)), + Column("enabled", Boolean), + UniqueConstraint( + "version", "titleId", name="sao_static_title_list_uk" + ), + mysql_charset="utf8mb4", +) + +class SaoStaticData(BaseData): + def put_quest( self, questSceneId: int, version: int, sortNo: int, name: str, enabled: bool ) -> Optional[int]: + sql = insert(quest).values( + questSceneId=questSceneId, + version=version, + sortNo=sortNo, + name=name, + tutorial=tutorial, + ) + + conflict = sql.on_duplicate_key_update( + name=name, questSceneId=questSceneId, version=version + ) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid + + def put_hero( self, version: int, heroLogId: int, name: str, nickname: str, rarity: int, skillTableSubId: int, awakeningExp: int, flavorText: str, enabled: bool ) -> Optional[int]: + sql = insert(hero).values( + version=version, + heroLogId=heroLogId, + name=name, + nickname=nickname, + rarity=rarity, + skillTableSubId=skillTableSubId, + awakeningExp=awakeningExp, + flavorText=flavorText, + enabled=enabled + ) + + conflict = sql.on_duplicate_key_update( + name=name, heroLogId=heroLogId + ) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid + + def put_equipment( self, version: int, equipmentId: int, name: str, equipmentType: int, weaponTypeId:int, rarity: int, flavorText: str, enabled: bool ) -> Optional[int]: + sql = insert(equipment).values( + version=version, + equipmentId=equipmentId, + name=name, + equipmentType=equipmentType, + weaponTypeId=weaponTypeId, + rarity=rarity, + flavorText=flavorText, + enabled=enabled + ) + + conflict = sql.on_duplicate_key_update( + name=name, equipmentId=equipmentId + ) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid + + def put_item( self, version: int, itemId: int, name: str, itemTypeId: int, rarity: int, flavorText: str, enabled: bool ) -> Optional[int]: + sql = insert(item).values( + version=version, + itemId=itemId, + name=name, + itemTypeId=itemTypeId, + rarity=rarity, + flavorText=flavorText, + enabled=enabled + ) + + conflict = sql.on_duplicate_key_update( + name=name, itemId=itemId + ) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid + + def put_support_log( self, version: int, supportLogId: int, charaId: int, name: str, rarity: int, salePrice: int, skillName: str, enabled: bool ) -> Optional[int]: + sql = insert(support).values( + version=version, + supportLogId=supportLogId, + charaId=charaId, + name=name, + rarity=rarity, + salePrice=salePrice, + skillName=skillName, + enabled=enabled + ) + + conflict = sql.on_duplicate_key_update( + name=name, supportLogId=supportLogId + ) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid + + def put_rare_drop( self, version: int, questRareDropId: int, commonRewardId: int, enabled: bool ) -> Optional[int]: + sql = insert(rare_drop).values( + version=version, + questRareDropId=questRareDropId, + commonRewardId=commonRewardId, + enabled=enabled, + ) + + conflict = sql.on_duplicate_key_update( + questRareDropId=questRareDropId, commonRewardId=commonRewardId, version=version + ) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid + + def put_title( self, version: int, titleId: int, displayName: str, requirement: int, rank: int, imageFilePath: str, enabled: bool ) -> Optional[int]: + sql = insert(title).values( + version=version, + titleId=titleId, + displayName=displayName, + requirement=requirement, + rank=rank, + imageFilePath=imageFilePath, + enabled=enabled + ) + + conflict = sql.on_duplicate_key_update( + displayName=displayName, titleId=titleId + ) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid + + def get_quests_id(self, sortNo: int) -> Optional[Dict]: + sql = quest.select(quest.c.sortNo == sortNo) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_quests_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]: + sql = quest.select(quest.c.version == version and quest.c.enabled == enabled).order_by( + quest.c.questSceneId.asc() + ) + + result = self.execute(sql) + if result is None: + return None + return [list[2] for list in result.fetchall()] + + def get_hero_id(self, heroLogId: int) -> Optional[Dict]: + sql = hero.select(hero.c.heroLogId == heroLogId) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_hero_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]: + sql = hero.select(hero.c.version == version and hero.c.enabled == enabled).order_by( + hero.c.heroLogId.asc() + ) + + result = self.execute(sql) + if result is None: + return None + return [list[2] for list in result.fetchall()] + + def get_equipment_id(self, equipmentId: int) -> Optional[Dict]: + sql = equipment.select(equipment.c.equipmentId == equipmentId) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_equipment_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]: + sql = equipment.select(equipment.c.version == version and equipment.c.enabled == enabled).order_by( + equipment.c.equipmentId.asc() + ) + + result = self.execute(sql) + if result is None: + return None + return [list[2] for list in result.fetchall()] + + def get_item_id(self, itemId: int) -> Optional[Dict]: + sql = item.select(item.c.itemId == itemId) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_rare_drop_id(self, questRareDropId: int) -> Optional[Dict]: + sql = rare_drop.select(rare_drop.c.questRareDropId == questRareDropId) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_item_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]: + sql = item.select(item.c.version == version and item.c.enabled == enabled).order_by( + item.c.itemId.asc() + ) + + result = self.execute(sql) + if result is None: + return None + return [list[2] for list in result.fetchall()] + + def get_support_log_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]: + sql = support.select(support.c.version == version and support.c.enabled == enabled).order_by( + support.c.supportLogId.asc() + ) + + result = self.execute(sql) + if result is None: + return None + return [list[2] for list in result.fetchall()] + + def get_title_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]: + sql = title.select(title.c.version == version and title.c.enabled == enabled).order_by( + title.c.titleId.asc() + ) + + result = self.execute(sql) + if result is None: + return None + return [list[2] for list in result.fetchall()] \ No newline at end of file diff --git a/titles/wacca/__init__.py b/titles/wacca/__init__.py index c0351da..a3bf96b 100644 --- a/titles/wacca/__init__.py +++ b/titles/wacca/__init__.py @@ -2,17 +2,11 @@ from titles.wacca.const import WaccaConstants from titles.wacca.index import WaccaServlet from titles.wacca.read import WaccaReader from titles.wacca.database import WaccaData +from titles.wacca.frontend import WaccaFrontend index = WaccaServlet database = WaccaData reader = WaccaReader - -use_default_title = True -include_protocol = True -title_secure = False +frontend = WaccaFrontend game_codes = [WaccaConstants.GAME_CODE] -trailing_slash = False -use_default_host = False -host = "" - -current_schema_version = 3 \ No newline at end of file +current_schema_version = 5 diff --git a/titles/wacca/base.py b/titles/wacca/base.py index 598f1fc..dd3fe24 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -1,76 +1,88 @@ from typing import Any, List, Dict import logging +import inflection from math import floor - from datetime import datetime, timedelta - from core.config import CoreConfig from titles.wacca.config import WaccaConfig from titles.wacca.const import WaccaConstants from titles.wacca.database import WaccaData from titles.wacca.handlers import * +from core.const import AllnetCountryCode -class WaccaBase(): + +class WaccaBase: def __init__(self, cfg: CoreConfig, game_cfg: WaccaConfig) -> None: - self.config = cfg # Config file - self.game_config = game_cfg # Game Config file - self.game = WaccaConstants.GAME_CODE # Game code - self.version = WaccaConstants.VER_WACCA # Game version - self.data = WaccaData(cfg) # Database + self.config = cfg # Config file + self.game_config = game_cfg # Game Config file + self.game = WaccaConstants.GAME_CODE # Game code + self.version = WaccaConstants.VER_WACCA # Game version + self.data = WaccaData(cfg) # Database self.logger = logging.getLogger("wacca") self.srvtime = datetime.now() self.season = 1 self.OPTIONS_DEFAULTS: Dict[str, Any] = { - "note_speed": 5, - "field_mask": 0, - "note_sound": 105001, - "note_color": 203001, - "bgm_volume": 10, - "bg_video": 0, - - "mirror": 0, - "judge_display_pos": 0, - "judge_detail_display": 0, - "measure_guidelines": 1, - "guideline_mask": 1, - "judge_line_timing_adjust": 10, - "note_design": 3, - "bonus_effect": 1, - "chara_voice": 1, - "score_display_method": 0, - "give_up": 0, - "guideline_spacing": 1, - "center_display": 1, - "ranking_display": 1, - "stage_up_icon_display": 1, - "rating_display": 1, - "player_level_display": 1, - "touch_effect": 1, - "guide_sound_vol": 3, - "touch_note_vol": 8, - "hold_note_vol": 8, - "slide_note_vol": 8, - "snap_note_vol": 8, - "chain_note_vol": 8, - "bonus_note_vol": 8, - "gate_skip": 0, - "key_beam_display": 1, - - "left_slide_note_color": 4, - "right_slide_note_color": 3, - "forward_slide_note_color": 1, - "back_slide_note_color": 2, - - "master_vol": 3, - "set_title_id": 104001, - "set_icon_id": 102001, - "set_nav_id": 210001, - "set_plate_id": 211001 - } + "note_speed": 5, + "field_mask": 0, + "note_sound": 105001, + "note_color": 203001, + "bgm_volume": 10, + "bg_video": 0, + "mirror": 0, + "judge_display_pos": 0, + "judge_detail_display": 0, + "measure_guidelines": 1, + "guideline_mask": 1, + "judge_line_timing_adjust": 10, + "note_design": 3, + "bonus_effect": 1, + "chara_voice": 1, + "score_display_method": 0, + "give_up": 0, + "guideline_spacing": 1, + "center_display": 1, + "ranking_display": 1, + "stage_up_icon_display": 1, + "rating_display": 1, + "player_level_display": 1, + "touch_effect": 1, + "guide_sound_vol": 3, + "touch_note_vol": 8, + "hold_note_vol": 8, + "slide_note_vol": 8, + "snap_note_vol": 8, + "chain_note_vol": 8, + "bonus_note_vol": 8, + "gate_skip": 0, + "key_beam_display": 1, + "left_slide_note_color": 4, + "right_slide_note_color": 3, + "forward_slide_note_color": 1, + "back_slide_note_color": 2, + "master_vol": 3, + "set_title_id": 104001, + "set_icon_id": 102001, + "set_nav_id": 210001, + "set_plate_id": 211001, + } self.allowed_stages = [] - + + prefecture_name = ( + inflection.underscore(game_cfg.server.prefecture_name) + .replace(" ", "_") + .upper() + ) + if prefecture_name not in [region.name for region in WaccaConstants.Region]: + self.logger.warning( + f"Invalid prefecture name {game_cfg.server.prefecture_name} in config file" + ) + self.region_id = WaccaConstants.Region.HOKKAIDO + + else: + self.region_id = WaccaConstants.Region[prefecture_name] + def handle_housing_get_request(self, data: Dict) -> Dict: req = BaseRequest(data) housing_id = 1337 @@ -78,22 +90,42 @@ class WaccaBase(): resp = HousingGetResponse(housing_id) return resp.make() - def handle_housing_start_request(self, data: Dict) -> Dict: - req = HousingStartRequest(data) + def handle_advertise_GetRanking_request(self, data: Dict) -> Dict: + req = AdvertiseGetRankingRequest(data) + return AdvertiseGetRankingResponse().make() - resp = HousingStartResponseV1( - 1, - [ # Recomended songs - 1269,1007,1270,1002,1020,1003,1008,1211,1018,1092,1056,32, - 1260,1230,1258,1251,2212,1264,1125,1037,2001,1272,1126,1119, - 1104,1070,1047,1044,1027,1004,1001,24,2068,2062,2021,1275, - 1249,1207,1203,1107,1021,1009,9,4,3,23,22,2014,13,1276,1247, - 1240,1237,1128,1114,1110,1109,1102,1045,1043,1036,1035,1030, - 1023,1015 - ] - ) + def handle_housing_start_request(self, data: Dict) -> Dict: + req = HousingStartRequestV1(data) + allnet_region_id = None + + machine = self.data.arcade.get_machine(req.chipId) + if machine is not None: + arcade = self.data.arcade.get_arcade(machine["arcade"]) + allnet_region_id = arcade["region_id"] + + if req.appVersion.country == AllnetCountryCode.JAPAN.value: + if allnet_region_id is not None: + region = WaccaConstants.allnet_region_id_to_wacca_region( + allnet_region_id + ) + + if region is None: + region_id = self.region_id + else: + region_id = region + + else: + region_id = self.region_id + + elif req.appVersion.country in WaccaConstants.VALID_COUNTRIES: + region_id = WaccaConstants.Region[req.appVersion.country] + + else: + region_id = WaccaConstants.Region.NONE + + resp = HousingStartResponseV1(region_id) return resp.make() - + def handle_advertise_GetNews_request(self, data: Dict) -> Dict: resp = GetNewsResponseV1() return resp.make() @@ -103,62 +135,22 @@ class WaccaBase(): self.logger.info(f"Log out user {req.userId} from {req.chipId}") return BaseResponse().make() - def handle_user_status_login_request(self, data: Dict) -> List[Any]: - req = UserStatusLoginRequest(data) - resp = UserStatusLoginResponseV1() - is_new_day = False - is_consec_day = False - is_consec_day = True - - if req.userId == 0: - self.logger.info(f"Guest login on {req.chipId}") - resp.lastLoginDate = 0 - - else: - profile = self.data.profile.get_profile(req.userId) - if profile is None: - self.logger.warn(f"Unknown user id {req.userId} attempted login from {req.chipId}") - return resp.make() - - self.logger.info(f"User {req.userId} login on {req.chipId}") - last_login_time = int(profile["last_login_date"].timestamp()) - resp.lastLoginDate = last_login_time - - # If somebodies login timestamp < midnight of current day, then they are logging in for the first time today - if last_login_time < int(datetime.now().replace(hour=0,minute=0,second=0,microsecond=0).timestamp()): - is_new_day = True - is_consec_day = True - - # If somebodies login timestamp > midnight of current day + 1 day, then they broke their daily login streak - elif last_login_time > int((datetime.now().replace(hour=0,minute=0,second=0,microsecond=0) + timedelta(days=1)).timestamp()): - is_consec_day = False - # else, they are simply logging in again on the same day, and we don't need to do anything for that - - self.data.profile.session_login(req.userId, is_new_day, is_consec_day) - - resp.firstLoginDaily = int(is_new_day) - - return resp.make() - - def handle_user_status_get_request(self, data: Dict) -> List[Any]: + def handle_user_status_get_request(self, data: Dict) -> Dict: req = UserStatusGetRequest(data) resp = UserStatusGetV1Response() - ver_split = req.appVersion.split(".") profile = self.data.profile.get_profile(aime_id=req.aimeId) if profile is None: self.logger.info(f"No user exists for aime id {req.aimeId}") + resp.profileStatus = ProfileStatus.ProfileRegister return resp.make() - - + self.logger.info(f"User preview for {req.aimeId} from {req.chipId}") if profile["last_game_ver"] is None: - profile_ver_split = ver_split - resp.lastGameVersion = req.appVersion + resp.lastGameVersion = ShortVersion(str(req.appVersion)) else: - profile_ver_split = profile["last_game_ver"].split(".") - resp.lastGameVersion = profile["last_game_ver"] - + resp.lastGameVersion = ShortVersion(profile["last_game_ver"]) + resp.userStatus.userId = profile["id"] resp.userStatus.username = profile["username"] resp.userStatus.xp = profile["xp"] @@ -166,131 +158,123 @@ class WaccaBase(): resp.userStatus.danType = profile["dan_type"] resp.userStatus.wp = profile["wp"] resp.userStatus.useCount = profile["login_count"] - resp.userStatus.loginDays = profile["login_count_days"] - resp.userStatus.loginConsecutiveDays = profile["login_count_days_consec"] - set_title_id = self.data.profile.get_options(WaccaConstants.OPTIONS["set_title_id"], profile["user"]) + set_title_id = self.data.profile.get_options( + WaccaConstants.OPTIONS["set_title_id"], profile["user"] + ) if set_title_id is None: set_title_id = self.OPTIONS_DEFAULTS["set_title_id"] resp.setTitleId = set_title_id - set_icon_id = self.data.profile.get_options(WaccaConstants.OPTIONS["set_title_id"], profile["user"]) + set_icon_id = self.data.profile.get_options( + WaccaConstants.OPTIONS["set_title_id"], profile["user"] + ) if set_icon_id is None: set_icon_id = self.OPTIONS_DEFAULTS["set_icon_id"] - resp.setIconId = set_icon_id - - if profile["last_login_date"].timestamp() < int((datetime.now().replace(hour=0,minute=0,second=0,microsecond=0) - timedelta(days=1)).timestamp()): - resp.userStatus.loginConsecutiveDays = 0 + resp.setIconId = set_icon_id - if int(ver_split[0]) > int(profile_ver_split[0]): + if req.appVersion > resp.lastGameVersion: resp.versionStatus = PlayVersionStatus.VersionUpgrade - elif int(ver_split[0]) < int(profile_ver_split[0]): + elif req.appVersion < resp.lastGameVersion: resp.versionStatus = PlayVersionStatus.VersionTooNew - - else: - if int(ver_split[1]) > int(profile_ver_split[1]): - resp.versionStatus = PlayVersionStatus.VersionUpgrade - - elif int(ver_split[1]) < int(profile_ver_split[1]): - resp.versionStatus = PlayVersionStatus.VersionTooNew - - else: - if int(ver_split[2]) > int(profile_ver_split[2]): - resp.versionStatus = PlayVersionStatus.VersionUpgrade - - - elif int(ver_split[2]) < int(profile_ver_split[2]): - resp.versionStatus = PlayVersionStatus.VersionTooNew - if profile["always_vip"]: - resp.userStatus.vipExpireTime = int((datetime.now() + timedelta(days=30)).timestamp()) - - elif profile["vip_expire_time"] is not None: - resp.userStatus.vipExpireTime = int(profile["vip_expire_time"].timestamp()) - return resp.make() - def handle_user_status_login_request(self, data: Dict) -> List[Any]: + def handle_user_status_login_request(self, data: Dict) -> Dict: req = UserStatusLoginRequest(data) - resp = UserStatusLoginResponseV2() - is_new_day = False - is_consec_day = False + resp = UserStatusLoginResponseV1() is_consec_day = True if req.userId == 0: self.logger.info(f"Guest login on {req.chipId}") resp.lastLoginDate = 0 - + else: profile = self.data.profile.get_profile(req.userId) if profile is None: - self.logger.warn(f"Unknown user id {req.userId} attempted login from {req.chipId}") + self.logger.warning( + f"Unknown user id {req.userId} attempted login from {req.chipId}" + ) return resp.make() self.logger.info(f"User {req.userId} login on {req.chipId}") last_login_time = int(profile["last_login_date"].timestamp()) resp.lastLoginDate = last_login_time - - # If somebodies login timestamp < midnight of current day, then they are logging in for the first time today - if last_login_time < int(datetime.now().replace(hour=0,minute=0,second=0,microsecond=0).timestamp()): - is_new_day = True - is_consec_day = True + midnight_today_ts = int( + datetime.now() + .replace(hour=0, minute=0, second=0, microsecond=0) + .timestamp() + ) - # If somebodies login timestamp > midnight of current day + 1 day, then they broke their daily login streak - elif last_login_time > int((datetime.now().replace(hour=0,minute=0,second=0,microsecond=0) + timedelta(days=1)).timestamp()): + # If somebodies login timestamp < midnight of current day, then they are logging in for the first time today + if last_login_time < midnight_today_ts: + resp.firstLoginDaily = True + + # If the difference between midnight today and their last login is greater then 1 day (86400 seconds) they've broken their streak + if midnight_today_ts - last_login_time > 86400: is_consec_day = False - # else, they are simply logging in again on the same day, and we don't need to do anything for that - - self.data.profile.session_login(req.userId, is_new_day, is_consec_day) - resp.vipInfo.pageYear = datetime.now().year - resp.vipInfo.pageMonth = datetime.now().month - resp.vipInfo.pageDay = datetime.now().day - resp.vipInfo.numItem = 1 - resp.firstLoginDaily = int(is_new_day) - + self.data.profile.session_login( + req.userId, resp.firstLoginDaily, is_consec_day + ) + + if resp.firstLoginDaily: + # TODO: Daily bonus + pass + + # TODO: VIP dialy/monthly rewards + return resp.make() - - def handle_user_status_create_request(self, data: Dict) -> List[Any]: + + def handle_user_status_create_request(self, data: Dict) -> Dict: req = UserStatusCreateRequest(data) - profileId = self.data.profile.create_profile(req.aimeId, req.username, self.version) + profileId = self.data.profile.create_profile( + req.aimeId, req.username, self.version + ) - if profileId is None: return BaseResponse().make() + if profileId is None: + return BaseResponse().make() + + if profileId == 0: + # We've already made this profile, just return success + new_user = self.data.profile.get_profile(aime_id=req.aimeId) + profileId = new_user['id'] # Insert starting items self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["title"], 104001) self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["title"], 104002) self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["title"], 104003) self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["title"], 104005) - + self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["icon"], 102001) self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["icon"], 102002) - - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["note_color"], 103001) - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["note_color"], 203001) - - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["note_sound"], 105001) - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["note_sound"], 205005) # Added lily - - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210001) - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["user_plate"], 211001) # Added lily + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["note_color"], 103001 + ) + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["note_color"], 203001 + ) + + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["note_sound"], 105001 + ) + + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210001 + ) - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["touch_effect"], 312000) # Added reverse - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["touch_effect"], 312001) # Added reverse - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["touch_effect"], 312002) # Added reverse - return UserStatusCreateResponseV2(profileId, req.username).make() - def handle_user_status_getDetail_request(self, data: Dict) -> List[Any]: + def handle_user_status_getDetail_request(self, data: Dict) -> Dict: req = UserStatusGetDetailRequest(data) resp = UserStatusGetDetailResponseV1() profile = self.data.profile.get_profile(req.userId) if profile is None: - self.logger.warn(f"Unknown profile {req.userId}") + self.logger.warning(f"Unknown profile {req.userId}") return resp.make() self.logger.info(f"Get detail for profile {req.userId}") @@ -303,15 +287,6 @@ class WaccaBase(): profile_trophies = self.data.item.get_trophies(user_id) profile_tickets = self.data.item.get_tickets(user_id) - if profile["vip_expire_time"] is None: - resp.userStatus.vipExpireTime = 0 - - else: - resp.userStatus.vipExpireTime = int(profile["vip_expire_time"].timestamp()) - - if profile["always_vip"] or self.game_config.mods.always_vip: - resp.userStatus.vipExpireTime = int((self.srvtime + timedelta(days=31)).timestamp()) - resp.songUpdateTime = int(profile["last_login_date"].timestamp()) resp.songPlayStatus = [profile["last_song_id"], 1] @@ -322,38 +297,44 @@ class WaccaBase(): resp.userStatus.danType = profile["dan_type"] resp.userStatus.wp = profile["wp"] resp.userStatus.useCount = profile["login_count"] - resp.userStatus.loginDays = profile["login_count_days"] - resp.userStatus.loginConsecutiveDays = profile["login_count_days_consec"] if self.game_config.mods.infinite_wp: resp.userStatus.wp = 999999 - + if profile["friend_view_1"] is not None: pass if profile["friend_view_2"] is not None: pass if profile["friend_view_3"] is not None: pass - - resp.seasonalPlayModeCounts.append(PlayModeCounts(self.season, 1, profile["playcount_single"])) - resp.seasonalPlayModeCounts.append(PlayModeCounts(self.season, 2, profile["playcount_multi_vs"])) - resp.seasonalPlayModeCounts.append(PlayModeCounts(self.season, 3, profile["playcount_multi_coop"])) - resp.seasonalPlayModeCounts.append(PlayModeCounts(self.season, 4, profile["playcount_stageup"])) - + + resp.seasonalPlayModeCounts.append( + PlayModeCounts(self.season, 1, profile["playcount_single"]) + ) + resp.seasonalPlayModeCounts.append( + PlayModeCounts(self.season, 2, profile["playcount_multi_vs"]) + ) + resp.seasonalPlayModeCounts.append( + PlayModeCounts(self.season, 3, profile["playcount_multi_coop"]) + ) + resp.seasonalPlayModeCounts.append( + PlayModeCounts(self.season, 4, profile["playcount_stageup"]) + ) + for opt in profile_options: resp.options.append(UserOption(opt["opt_id"], opt["value"])) - + for unlock in profile_song_unlocks: for x in range(1, unlock["highest_difficulty"] + 1): - resp.userItems.songUnlocks.append(SongUnlock(unlock["song_id"], x, 0, int(unlock["acquire_date"].timestamp()))) - if x > 2: - resp.scores.append(BestScoreDetailV1(unlock["song_id"], x)) - - empty_scores = len(resp.scores) + resp.userItems.songUnlocks.append( + SongUnlock( + unlock["song_id"], x, 0, int(unlock["acquire_date"].timestamp()) + ) + ) + for song in profile_scores: resp.seasonInfo.cumulativeScore += song["score"] - empty_score_idx = resp.find_score_idx(song["song_id"], song["chart_id"], 0, empty_scores) - + clear_cts = SongDetailClearCounts( song["play_ct"], song["clear_ct"], @@ -363,32 +344,38 @@ class WaccaBase(): ) grade_cts = SongDetailGradeCountsV1( - song["grade_d_ct"], song["grade_c_ct"], song["grade_b_ct"], song["grade_a_ct"], song["grade_aa_ct"], - song["grade_aaa_ct"], song["grade_s_ct"], song["grade_ss_ct"], song["grade_sss_ct"], - song["grade_master_ct"] + song["grade_d_ct"], + song["grade_c_ct"], + song["grade_b_ct"], + song["grade_a_ct"], + song["grade_aa_ct"], + song["grade_aaa_ct"], + song["grade_s_ct"], + song["grade_ss_ct"], + song["grade_sss_ct"], + song["grade_master_ct"], ) - if empty_score_idx is not None: - resp.scores[empty_score_idx].clearCounts = clear_cts - resp.scores[empty_score_idx].clearCountsSeason = clear_cts - resp.scores[empty_score_idx].gradeCounts = grade_cts - resp.scores[empty_score_idx].score = song["score"] - resp.scores[empty_score_idx].bestCombo = song["best_combo"] - resp.scores[empty_score_idx].lowestMissCtMaybe = song["lowest_miss_ct"] - resp.scores[empty_score_idx].rating = song["rating"] - - else: - deets = BestScoreDetailV1(song["song_id"], song["chart_id"]) - deets.clearCounts = clear_cts - deets.clearCountsSeason = clear_cts - deets.gradeCounts = grade_cts - deets.score = song["score"] - deets.bestCombo = song["best_combo"] - deets.lowestMissCtMaybe = song["lowest_miss_ct"] - deets.rating = song["rating"] - + deets = BestScoreDetailV1(song["song_id"], song["chart_id"]) + deets.clearCounts = clear_cts + deets.clearCountsSeason = clear_cts + deets.gradeCounts = grade_cts + deets.score = song["score"] + deets.bestCombo = song["best_combo"] + deets.lowestMissCtMaybe = song["lowest_miss_ct"] + deets.rating = song["rating"] + + resp.scores.append(deets) + for trophy in profile_trophies: - resp.userItems.trophies.append(TrophyItem(trophy["trophy_id"], trophy["season"], trophy["progress"], trophy["badge_type"])) + resp.userItems.trophies.append( + TrophyItem( + trophy["trophy_id"], + trophy["season"], + trophy["progress"], + trophy["badge_type"], + ) + ) if self.game_config.mods.infinite_tickets: for x in range(5): @@ -400,29 +387,41 @@ class WaccaBase(): else: expire = int(ticket["expire_date"].timestamp()) - resp.userItems.tickets.append(TicketItem(ticket["id"], ticket["ticket_id"], expire)) + resp.userItems.tickets.append( + TicketItem(ticket["id"], ticket["ticket_id"], expire) + ) if profile_items: for item in profile_items: try: - if item["type"] == WaccaConstants.ITEM_TYPES["icon"]: - resp.userItems.icons.append(IconItem(item["item_id"], 1, item["use_count"], int(item["acquire_date"].timestamp()))) + resp.userItems.icons.append( + IconItem( + item["item_id"], + 1, + item["use_count"], + int(item["acquire_date"].timestamp()), + ) + ) else: - itm_send = GenericItemSend(item["item_id"], 1, int(item["acquire_date"].timestamp())) + itm_send = GenericItemSend( + item["item_id"], 1, int(item["acquire_date"].timestamp()) + ) if item["type"] == WaccaConstants.ITEM_TYPES["title"]: resp.userItems.titles.append(itm_send) - + elif item["type"] == WaccaConstants.ITEM_TYPES["note_color"]: resp.userItems.noteColors.append(itm_send) elif item["type"] == WaccaConstants.ITEM_TYPES["note_sound"]: resp.userItems.noteSounds.append(itm_send) - except: - self.logger.error(f"{__name__} Failed to load item {item['item_id']} for user {profile['user']}") + except Exception: + self.logger.error( + f"{__name__} Failed to load item {item['item_id']} for user {profile['user']}" + ) resp.seasonInfo.level = profile["xp"] resp.seasonInfo.wpObtained = profile["wp_total"] @@ -433,30 +432,28 @@ class WaccaBase(): resp.seasonInfo.noteSoundsObtained = len(resp.userItems.noteSounds) return resp.make() - - def handle_user_trial_get_request(self, data: Dict) -> List[Any]: + + def handle_user_trial_get_request(self, data: Dict) -> Dict: req = UserTrialGetRequest(data) resp = UserTrialGetResponse() - + user_id = self.data.profile.profile_to_aime_user(req.profileId) if user_id is None: - self.logger.error(f"handle_user_trial_get_request: No profile with id {req.profileId}") + self.logger.error( + f"handle_user_trial_get_request: No profile with id {req.profileId}" + ) return resp.make() self.logger.info(f"Get trial info for user {req.profileId}") - - for d in self.allowed_stages: - if d[1] > 0 and d[1] < 10: - resp.stageList.append(StageInfo(d[0], d[1])) - stages = self.data.score.get_stageup(user_id, self.version) if stages is None: stages = [] - - add_next = True + + tmp: List[StageInfo] = [] + for d in self.allowed_stages: stage_info = StageInfo(d[0], d[1]) - + for score in stages: if score["stage_id"] == stage_info.danId: stage_info.clearStatus = score["clear_status"] @@ -465,56 +462,94 @@ class WaccaBase(): stage_info.song2BestScore = score["song2_score"] stage_info.song3BestScore = score["song3_score"] break - - if add_next or stage_info.danLevel < 9: - resp.stageList.append(stage_info) - if stage_info.danLevel >= 9 and stage_info.clearStatus < 1: - add_next = False + tmp.append(stage_info) + + for x in range(len(tmp)): + if tmp[x].danLevel >= 10 and ( + tmp[x + 1].clearStatus >= 1 or tmp[x].clearStatus >= 1 + ): + resp.stageList.append(tmp[x]) + elif tmp[x].danLevel < 10: + resp.stageList.append(tmp[x]) return resp.make() - def handle_user_trial_update_request(self, data: Dict) -> List[Any]: + def handle_user_trial_update_request(self, data: Dict) -> Dict: req = UserTrialUpdateRequest(data) total_score = 0 for score in req.songScores: total_score += score - + while len(req.songScores) < 3: req.songScores.append(0) profile = self.data.profile.get_profile(req.profileId) - - user_id = profile["user"] - old_stage = self.data.score.get_stageup_stage(user_id, self.version, req.stageId) - if old_stage is None: - self.data.score.put_stageup(user_id, self.version, req.stageId, req.clearType.value, req.numSongsCleared, req.songScores[0], req.songScores[1], req.songScores[2]) - + user_id = profile["user"] + old_stage = self.data.score.get_stageup_stage( + user_id, self.version, req.stageId + ) + + if old_stage is None: + self.data.score.put_stageup( + user_id, + self.version, + req.stageId, + req.clearType.value, + req.numSongsCleared, + req.songScores[0], + req.songScores[1], + req.songScores[2], + ) + else: # We only care about total score for best of, even if one score happens to be lower (I think) - if total_score > (old_stage["song1_score"] + old_stage["song2_score"] + old_stage["song3_score"]): + if total_score > ( + old_stage["song1_score"] + + old_stage["song2_score"] + + old_stage["song3_score"] + ): best_score1 = req.songScores[0] - best_score2 = req.songScores[2] - best_score3 = req.songScores[3] + best_score2 = req.songScores[1] + best_score3 = req.songScores[2] else: best_score1 = old_stage["song1_score"] best_score2 = old_stage["song2_score"] best_score3 = old_stage["song3_score"] - self.data.score.put_stageup(user_id, self.version, req.stageId, req.clearType.value, req.numSongsCleared, - best_score1, best_score2, best_score3) - - if req.stageLevel > 0 and req.stageLevel <= 14 and req.clearType.value > 0: # For some reason, special stages send dan level 1001... - if req.stageLevel > profile["dan_level"] or (req.stageLevel == profile["dan_level"] and req.clearType.value > profile["dan_type"]): - self.data.profile.update_profile_dan(req.profileId, req.stageLevel, req.clearType.value) + self.data.score.put_stageup( + user_id, + self.version, + req.stageId, + req.clearType.value, + req.numSongsCleared, + best_score1, + best_score2, + best_score3, + ) + + if ( + req.stageLevel > 0 and req.stageLevel <= 14 and req.clearType.value > 0 + ): # For some reason, special stages send dan level 1001... + if req.stageLevel > profile["dan_level"] or ( + req.stageLevel == profile["dan_level"] + and req.clearType.value > profile["dan_type"] + ): + self.data.profile.update_profile_dan( + req.profileId, req.stageLevel, req.clearType.value + ) self.util_put_items(req.profileId, user_id, req.itemsObtained) # user/status/update isn't called after stageup so we have to do some things now - current_icon = self.data.profile.get_options(user_id, WaccaConstants.OPTIONS["set_icon_id"]) - current_nav = self.data.profile.get_options(user_id, WaccaConstants.OPTIONS["set_nav_id"]) + current_icon = self.data.profile.get_options( + user_id, WaccaConstants.OPTIONS["set_icon_id"] + ) + current_nav = self.data.profile.get_options( + user_id, WaccaConstants.OPTIONS["set_nav_id"] + ) if current_icon is None: current_icon = self.OPTIONS_DEFAULTS["set_icon_id"] @@ -525,122 +560,182 @@ class WaccaBase(): else: current_nav = current_nav["value"] - self.data.item.put_item(user_id, WaccaConstants.ITEM_TYPES["icon"], current_icon) - self.data.item.put_item(user_id, WaccaConstants.ITEM_TYPES["navigator"], current_nav) - self.data.profile.update_profile_playtype(req.profileId, 4, data["appVersion"][:7]) - return BaseResponse.make() - - def handle_user_sugoroku_update_request(self, data: Dict) -> List[Any]: + self.data.item.put_item( + user_id, WaccaConstants.ITEM_TYPES["icon"], current_icon + ) + self.data.item.put_item( + user_id, WaccaConstants.ITEM_TYPES["navigator"], current_nav + ) + self.data.profile.update_profile_playtype( + req.profileId, 4, data["appVersion"][:7] + ) + return BaseResponse().make() + + def handle_user_sugoroku_update_request(self, data: Dict) -> Dict: ver_split = data["appVersion"].split(".") resp = BaseResponse() if int(ver_split[0]) <= 2 and int(ver_split[1]) < 53: req = UserSugarokuUpdateRequestV1(data) mission_flg = 0 - + else: req = UserSugarokuUpdateRequestV2(data) mission_flg = req.mission_flag user_id = self.data.profile.profile_to_aime_user(req.profileId) - if user_id is None: - self.logger.info(f"handle_user_sugoroku_update_request unknwon profile ID {req.profileId}") + if user_id is None: + self.logger.info( + f"handle_user_sugoroku_update_request unknwon profile ID {req.profileId}" + ) return resp.make() self.util_put_items(req.profileId, user_id, req.itemsObtainted) - self.data.profile.update_gate(user_id, req.gateId, req.page, req.progress, req.loops, mission_flg, req.totalPts) + self.data.profile.update_gate( + user_id, + req.gateId, + req.page, + req.progress, + req.loops, + mission_flg, + req.totalPts, + ) return resp.make() - - def handle_user_info_getMyroom_request(self, data: Dict) -> List[Any]: - return UserInfogetMyroomResponse().make() - - def handle_user_music_unlock_request(self, data: Dict) -> List[Any]: + + def handle_user_info_getMyroom_request(self, data: Dict) -> Dict: + return UserInfogetMyroomResponseV1().make() + + def handle_user_music_unlock_request(self, data: Dict) -> Dict: req = UserMusicUnlockRequest(data) profile = self.data.profile.get_profile(req.profileId) - if profile is None: return BaseResponse().make() + if profile is None: + return BaseResponse().make() user_id = profile["user"] current_wp = profile["wp"] tickets = self.data.item.get_tickets(user_id) - new_tickets = [] - + new_tickets: List[TicketItem] = [] + for ticket in tickets: - new_tickets.append([ticket["id"], ticket["ticket_id"], 9999999999]) - + new_tickets.append(TicketItem(ticket["id"], ticket["ticket_id"], 9999999999)) + for item in req.itemsUsed: - if item.itemType == WaccaConstants.ITEM_TYPES["wp"]: + if ( + item.itemType == WaccaConstants.ITEM_TYPES["wp"] + and not self.game_config.mods.infinite_wp + ): if current_wp >= item.quantity: current_wp -= item.quantity self.data.profile.spend_wp(req.profileId, item.quantity) - else: return BaseResponse().make() + else: + return BaseResponse().make() - elif item.itemType == WaccaConstants.ITEM_TYPES["ticket"]: + elif ( + item.itemType == WaccaConstants.ITEM_TYPES["ticket"] + and not self.game_config.mods.infinite_tickets + ): for x in range(len(new_tickets)): - if new_tickets[x][1] == item.itemId: - self.data.item.spend_ticket(new_tickets[x][0]) + if new_tickets[x].ticketId == item.itemId: + self.logger.debug( + f"Remove ticket ID {new_tickets[x].userTicketId} type {new_tickets[x].ticketId} from {user_id}" + ) + self.data.item.spend_ticket(new_tickets[x].userTicketId) new_tickets.pop(x) break # wp, ticket info if req.difficulty > WaccaConstants.Difficulty.HARD.value: - old_score = self.data.score.get_best_score(user_id, req.songId, req.difficulty) + old_score = self.data.score.get_best_score( + user_id, req.songId, req.difficulty + ) if not old_score: - self.data.score.put_best_score(user_id, req.songId, req.difficulty, 0, [0] * 5, [0] * 13, 0, 0) - - self.data.item.unlock_song(user_id, req.songId, req.difficulty if req.difficulty > WaccaConstants.Difficulty.HARD.value else WaccaConstants.Difficulty.HARD.value) - + self.data.score.put_best_score( + user_id, req.songId, req.difficulty, 0, [0] * 5, [0] * 13, 0, 0 + ) + + self.data.item.unlock_song( + user_id, + req.songId, + req.difficulty + if req.difficulty > WaccaConstants.Difficulty.HARD.value + else WaccaConstants.Difficulty.HARD.value, + ) + if self.game_config.mods.infinite_tickets: - new_tickets = [ - [0, 106002, 0], - [1, 106002, 0], - [2, 106002, 0], - [3, 106002, 0], - [4, 106002, 0], - ] - + for x in range(5): + new_tickets.append(TicketItem(x, 106002, 0)) + if self.game_config.mods.infinite_wp: current_wp = 999999 return UserMusicUnlockResponse(current_wp, new_tickets).make() - - def handle_user_info_getRanking_request(self, data: Dict) -> List[Any]: + + def handle_user_info_getRanking_request(self, data: Dict) -> Dict: # total score, high score by song, cumulative socre, stage up score, other score, WP ranking # This likely requies calculating standings at regular intervals and caching the results return UserInfogetRankingResponse().make() - - def handle_user_music_update_request(self, data: Dict) -> List[Any]: - req = UserMusicUpdateRequest(data) - ver_split = req.appVersion.split(".") + + def handle_user_music_update_request(self, data: Dict) -> Dict: + ver_split = data["appVersion"].split(".") if int(ver_split[0]) >= 3: resp = UserMusicUpdateResponseV3() + req = UserMusicUpdateRequestV2(data) elif int(ver_split[0]) >= 2: resp = UserMusicUpdateResponseV2() + req = UserMusicUpdateRequestV2(data) else: resp = UserMusicUpdateResponseV1() + req = UserMusicUpdateRequestV1(data) resp.songDetail.songId = req.songDetail.songId resp.songDetail.difficulty = req.songDetail.difficulty - + + if req.profileId == 0: + self.logger.info( + f"Guest score for song {req.songDetail.songId} difficulty {req.songDetail.difficulty}" + ) + return resp.make() + profile = self.data.profile.get_profile(req.profileId) - + if profile is None: - self.logger.warn(f"handle_user_music_update_request: No profile for game_id {req.profileId}") - return BaseResponse().make() - + self.logger.warning( + f"handle_user_music_update_request: No profile for game_id {req.profileId}" + ) + return resp.make() + user_id = profile["user"] self.util_put_items(req.profileId, user_id, req.itemsObtained) - playlog_clear_status = req.songDetail.flagCleared + req.songDetail.flagMissless + req.songDetail.flagFullcombo + \ - req.songDetail.flagAllMarvelous - - self.data.score.put_playlog(user_id, req.songDetail.songId, req.songDetail.difficulty, req.songDetail.score, - playlog_clear_status, req.songDetail.grade.value, req.songDetail.maxCombo, req.songDetail.judgements.marvCt, - req.songDetail.judgements.greatCt, req.songDetail.judgements.goodCt, req.songDetail.judgements.missCt, - req.songDetail.fastCt, req.songDetail.slowCt, self.season) + playlog_clear_status = ( + req.songDetail.flagCleared + + req.songDetail.flagMissless + + req.songDetail.flagFullcombo + + req.songDetail.flagAllMarvelous + ) - old_score = self.data.score.get_best_score(user_id, req.songDetail.songId, req.songDetail.difficulty) + self.data.score.put_playlog( + user_id, + req.songDetail.songId, + req.songDetail.difficulty, + req.songDetail.score, + playlog_clear_status, + req.songDetail.grade.value, + req.songDetail.maxCombo, + req.songDetail.judgements.marvCt, + req.songDetail.judgements.greatCt, + req.songDetail.judgements.goodCt, + req.songDetail.judgements.missCt, + req.songDetail.fastCt, + req.songDetail.slowCt, + self.season, + ) + + old_score = self.data.score.get_best_score( + user_id, req.songDetail.songId, req.songDetail.difficulty + ) if not old_score: grades = [0] * 13 @@ -654,9 +749,17 @@ class WaccaBase(): grades[req.songDetail.grade.value - 1] = 1 - self.data.score.put_best_score(user_id, req.songDetail.songId, req.songDetail.difficulty, req.songDetail.score, - clears, grades, req.songDetail.maxCombo, req.songDetail.judgements.missCt) - + self.data.score.put_best_score( + user_id, + req.songDetail.songId, + req.songDetail.difficulty, + req.songDetail.score, + clears, + grades, + req.songDetail.maxCombo, + req.songDetail.judgements.missCt, + ) + resp.songDetail.score = req.songDetail.score resp.songDetail.lowestMissCount = req.songDetail.judgements.missCt @@ -694,124 +797,161 @@ class WaccaBase(): best_score = max(req.songDetail.score, old_score["score"]) best_max_combo = max(req.songDetail.maxCombo, old_score["best_combo"]) - lowest_miss_ct = min(req.songDetail.judgements.missCt, old_score["lowest_miss_ct"]) - best_rating = max(self.util_calc_song_rating(req.songDetail.score, req.songDetail.level), old_score["rating"]) - - self.data.score.put_best_score(user_id, req.songDetail.songId, req.songDetail.difficulty, best_score, clears, - grades, best_max_combo, lowest_miss_ct) - + lowest_miss_ct = min( + req.songDetail.judgements.missCt, old_score["lowest_miss_ct"] + ) + best_rating = max( + self.util_calc_song_rating(req.songDetail.score, req.songDetail.level), + old_score["rating"], + ) + + self.data.score.put_best_score( + user_id, + req.songDetail.songId, + req.songDetail.difficulty, + best_score, + clears, + grades, + best_max_combo, + lowest_miss_ct, + ) + resp.songDetail.score = best_score resp.songDetail.lowestMissCount = lowest_miss_ct resp.songDetail.rating = best_rating resp.songDetail.clearCounts = SongDetailClearCounts(counts=clears) resp.songDetail.clearCountsSeason = SongDetailClearCounts(counts=clears) - + if int(ver_split[0]) >= 3: resp.songDetail.grades = SongDetailGradeCountsV2(counts=grades) else: resp.songDetail.grades = SongDetailGradeCountsV1(counts=grades) - + resp.songDetail.lockState = 1 return resp.make() - - #TODO: Coop and vs data - def handle_user_music_updateCoop_request(self, data: Dict) -> List[Any]: + + # TODO: Coop and vs data + def handle_user_music_updateCoop_request(self, data: Dict) -> Dict: coop_info = data["params"][4] return self.handle_user_music_update_request(data) - def handle_user_music_updateVersus_request(self, data: Dict) -> List[Any]: + def handle_user_music_updateVersus_request(self, data: Dict) -> Dict: vs_info = data["params"][4] return self.handle_user_music_update_request(data) - - def handle_user_music_updateTrial_request(self, data: Dict) -> List[Any]: + + def handle_user_music_updateTrial_request(self, data: Dict) -> Dict: return self.handle_user_music_update_request(data) - def handle_user_mission_update_request(self, data: Dict) -> List[Any]: + def handle_user_mission_update_request(self, data: Dict) -> Dict: req = UserMissionUpdateRequest(data) page_status = req.params[1][1] profile = self.data.profile.get_profile(req.profileId) if profile is None: return BaseResponse().make() - + if len(req.itemsObtained) > 0: self.util_put_items(req.profileId, profile["user"], req.itemsObtained) - - self.data.profile.update_bingo(profile["user"], req.bingoDetail.pageNumber, page_status) + + self.data.profile.update_bingo( + profile["user"], req.bingoDetail.pageNumber, page_status + ) self.data.profile.update_tutorial_flags(req.profileId, req.params[3]) return BaseResponse().make() - def handle_user_goods_purchase_request(self, data: Dict) -> List[Any]: + def handle_user_goods_purchase_request(self, data: Dict) -> Dict: req = UserGoodsPurchaseRequest(data) resp = UserGoodsPurchaseResponse() profile = self.data.profile.get_profile(req.profileId) - if profile is None: return BaseResponse().make() + if profile is None: + return BaseResponse().make() user_id = profile["user"] resp.currentWp = profile["wp"] - if req.purchaseType == PurchaseType.PurchaseTypeWP: + if ( + req.purchaseType == PurchaseType.PurchaseTypeWP + and not self.game_config.mods.infinite_wp + ): resp.currentWp -= req.cost self.data.profile.spend_wp(req.profileId, req.cost) - - elif req.purchaseType == PurchaseType.PurchaseTypeCredit: - self.logger.info(f"User {req.profileId} Purchased item {req.itemObtained.itemType} id {req.itemObtained.itemId} for {req.cost} credits on machine {req.chipId}") - self.util_put_items(req.profileId ,user_id, [req.itemObtained]) + elif req.purchaseType == PurchaseType.PurchaseTypeCredit: + self.logger.info( + f"User {req.profileId} Purchased item {req.itemObtained.itemType} id {req.itemObtained.itemId} for {req.cost} credits on machine {req.chipId}" + ) + + self.util_put_items(req.profileId, user_id, [req.itemObtained]) if self.game_config.mods.infinite_tickets: for x in range(5): resp.tickets.append(TicketItem(x, 106002, 0)) else: tickets = self.data.item.get_tickets(user_id) - + for ticket in tickets: - resp.tickets.append(TicketItem(ticket["id"], ticket["ticket_id"], int((self.srvtime + timedelta(days=30)).timestamp()))) - + resp.tickets.append( + TicketItem( + ticket["id"], + ticket["ticket_id"], + int((self.srvtime + timedelta(days=30)).timestamp()), + ) + ) + if self.game_config.mods.infinite_wp: resp.currentWp = 999999 return resp.make() - def handle_competition_status_login_request(self, data: Dict) -> List[Any]: + def handle_competition_status_login_request(self, data: Dict) -> Dict: return BaseResponse().make() - def handle_competition_status_update_request(self, data: Dict) -> List[Any]: + def handle_competition_status_update_request(self, data: Dict) -> Dict: return BaseResponse().make() - def handle_user_rating_update_request(self, data: Dict) -> List[Any]: + def handle_user_rating_update_request(self, data: Dict) -> Dict: req = UserRatingUpdateRequest(data) user_id = self.data.profile.profile_to_aime_user(req.profileId) if user_id is None: - self.logger.error(f"handle_user_rating_update_request: No profild with ID {req.profileId}") + self.logger.error( + f"handle_user_rating_update_request: No profild with ID {req.profileId}" + ) return BaseResponse().make() for song in req.songs: - self.data.score.update_song_rating(user_id, song.songId, song.difficulty, song.rating) - + self.data.score.update_song_rating( + user_id, song.songId, song.difficulty, song.rating + ) + self.data.profile.update_user_rating(req.profileId, req.totalRating) return BaseResponse().make() - - def handle_user_status_update_request(self, data: Dict) -> List[Any]: - req = UserStatusUpdateRequestV2(data) + + def handle_user_status_update_request(self, data: Dict) -> Dict: + req = UserStatusUpdateRequestV1(data) user_id = self.data.profile.profile_to_aime_user(req.profileId) if user_id is None: - self.logger.info(f"handle_user_status_update_request: No profile with ID {req.profileId}") + self.logger.info( + f"handle_user_status_update_request: No profile with ID {req.profileId}" + ) return BaseResponse().make() - + self.util_put_items(req.profileId, user_id, req.itemsRecieved) - self.data.profile.update_profile_playtype(req.profileId, req.playType.value, data["appVersion"][:7]) - self.data.profile.update_profile_lastplayed(req.profileId, req.lastSongInfo.lastSongId, req.lastSongInfo.lastSongDiff, - req.lastSongInfo.lastFolderOrd, req.lastSongInfo.lastFolderId, req.lastSongInfo.lastSongOrd) - - current_icon = self.data.profile.get_options(user_id, WaccaConstants.OPTIONS["set_icon_id"]) - current_nav = self.data.profile.get_options(user_id, WaccaConstants.OPTIONS["set_nav_id"]) + self.data.profile.update_profile_playtype( + req.profileId, req.playType.value, data["appVersion"][:7] + ) + + current_icon = self.data.profile.get_options( + user_id, WaccaConstants.OPTIONS["set_icon_id"] + ) + current_nav = self.data.profile.get_options( + user_id, WaccaConstants.OPTIONS["set_nav_id"] + ) if current_icon is None: current_icon = self.OPTIONS_DEFAULTS["set_icon_id"] @@ -821,21 +961,25 @@ class WaccaBase(): current_nav = self.OPTIONS_DEFAULTS["set_nav_id"] else: current_nav = current_nav["value"] - - self.data.item.put_item(user_id, WaccaConstants.ITEM_TYPES["icon"], current_icon) - self.data.item.put_item(user_id, WaccaConstants.ITEM_TYPES["navigator"], current_nav) + + self.data.item.put_item( + user_id, WaccaConstants.ITEM_TYPES["icon"], current_icon + ) + self.data.item.put_item( + user_id, WaccaConstants.ITEM_TYPES["navigator"], current_nav + ) return BaseResponse().make() - def handle_user_info_update_request(self, data: Dict) -> List[Any]: + def handle_user_info_update_request(self, data: Dict) -> Dict: req = UserInfoUpdateRequest(data) user_id = self.data.profile.profile_to_aime_user(req.profileId) for opt in req.optsUpdated: - self.data.profile.update_option(user_id, opt.id, opt.val) + self.data.profile.update_option(user_id, opt.optId, opt.optVal) for update in req.datesUpdated: - pass + pass for fav in req.favoritesAdded: self.data.profile.add_favorite_song(user_id, fav) @@ -844,64 +988,96 @@ class WaccaBase(): self.data.profile.remove_favorite_song(user_id, unfav) return BaseResponse().make() - - def handle_user_vip_get_request(self, data: Dict) -> List[Any]: + + def handle_user_vip_get_request(self, data: Dict) -> Dict: req = UserVipGetRequest(data) resp = UserVipGetResponse() profile = self.data.profile.get_profile(req.profileId) if profile is None: - self.logger.warn(f"handle_user_vip_get_request no profile with ID {req.profileId}") + self.logger.warning( + f"handle_user_vip_get_request no profile with ID {req.profileId}" + ) return BaseResponse().make() - - if "vip_expire_time" in profile and profile["vip_expire_time"] is not None and profile["vip_expire_time"].timestamp() > int(self.srvtime.timestamp()): - resp.vipDays = int((profile["vip_expire_time"].timestamp() - int(self.srvtime.timestamp())) / 86400) - + + if ( + "vip_expire_time" in profile + and profile["vip_expire_time"] is not None + and profile["vip_expire_time"].timestamp() > int(self.srvtime.timestamp()) + ): + resp.vipDays = int( + (profile["vip_expire_time"].timestamp() - int(self.srvtime.timestamp())) + / 86400 + ) + resp.vipDays += 30 - - resp.presents.append(VipLoginBonus(1,0,16,211025,1)) - resp.presents.append(VipLoginBonus(2,0,6,202086,1)) - resp.presents.append(VipLoginBonus(3,0,11,205008,1)) - resp.presents.append(VipLoginBonus(4,0,10,203009,1)) - resp.presents.append(VipLoginBonus(5,0,16,211026,1)) - resp.presents.append(VipLoginBonus(6,0,9,206001,1)) - + + resp.presents.append(VipLoginBonus(1, 0, 16, 211025, 1)) + resp.presents.append(VipLoginBonus(2, 0, 6, 202086, 1)) + resp.presents.append(VipLoginBonus(3, 0, 11, 205008, 1)) + resp.presents.append(VipLoginBonus(4, 0, 10, 203009, 1)) + resp.presents.append(VipLoginBonus(5, 0, 16, 211026, 1)) + resp.presents.append(VipLoginBonus(6, 0, 9, 206001, 1)) + return resp.make() - - def handle_user_vip_start_request(self, data: Dict) -> List[Any]: + + def handle_user_vip_start_request(self, data: Dict) -> Dict: req = UserVipStartRequest(data) profile = self.data.profile.get_profile(req.profileId) - if profile is None: return BaseResponse().make() + if profile is None: + return BaseResponse().make() # This should never happen because wacca stops you from buying VIP # if you have more then 10 days remaining, but this IS wacca we're dealing with... - if "always_vip" in profile and profile["always_vip"] or self.game_config.mods.always_vip: - return UserVipStartResponse(int((self.srvtime + timedelta(days=req.days)).timestamp())).make() + if ( + "always_vip" in profile + and profile["always_vip"] + or self.game_config.mods.always_vip + ): + return UserVipStartResponse( + int((self.srvtime + timedelta(days=req.days)).timestamp()) + ).make() - profile["vip_expire_time"] = int((self.srvtime + timedelta(days=req.days)).timestamp()) - self.data.profile.update_vip_time(req.profileId, self.srvtime + timedelta(days=req.days)) - return UserVipStartResponse(profile["vip_expire_time"]).make() + vip_exp_time = self.srvtime + timedelta(days=req.days) + self.data.profile.update_vip_time(req.profileId, vip_exp_time) + return UserVipStartResponse(int(vip_exp_time.timestamp())).make() - def util_put_items(self, profile_id: int, user_id: int, items_obtained: List[GenericItemRecv]) -> None: + def util_put_items( + self, profile_id: int, user_id: int, items_obtained: List[GenericItemRecv] + ) -> None: if user_id is None or profile_id <= 0: return None if items_obtained: for item in items_obtained: - if item.itemType == WaccaConstants.ITEM_TYPES["xp"]: self.data.profile.add_xp(profile_id, item.quantity) elif item.itemType == WaccaConstants.ITEM_TYPES["wp"]: self.data.profile.add_wp(profile_id, item.quantity) - - elif item.itemType == WaccaConstants.ITEM_TYPES["music_difficulty_unlock"] or item.itemType == WaccaConstants.ITEM_TYPES["music_unlock"]: + + elif ( + item.itemType + == WaccaConstants.ITEM_TYPES["music_difficulty_unlock"] + or item.itemType == WaccaConstants.ITEM_TYPES["music_unlock"] + ): if item.quantity > WaccaConstants.Difficulty.HARD.value: - old_score = self.data.score.get_best_score(user_id, item.itemId, item.quantity) + old_score = self.data.score.get_best_score( + user_id, item.itemId, item.quantity + ) if not old_score: - self.data.score.put_best_score(user_id, item.itemId, item.quantity, 0, [0] * 5, [0] * 13, 0, 0) - + self.data.score.put_best_score( + user_id, + item.itemId, + item.quantity, + 0, + [0] * 5, + [0] * 13, + 0, + 0, + ) + if item.quantity == 0: item.quantity = WaccaConstants.Difficulty.HARD.value self.data.item.unlock_song(user_id, item.itemId, item.quantity) @@ -910,7 +1086,9 @@ class WaccaBase(): self.data.item.add_ticket(user_id, item.itemId) elif item.itemType == WaccaConstants.ITEM_TYPES["trophy"]: - self.data.item.update_trophy(user_id, item.itemId, self.season, item.quantity, 0) + self.data.item.update_trophy( + user_id, item.itemId, self.season, item.quantity, 0 + ) else: self.data.item.put_item(user_id, item.itemType, item.itemId) @@ -936,6 +1114,7 @@ class WaccaBase(): const = 2.00 elif score >= 900000 and score < 910000: const = 1.00 - else: const = 0.00 + else: + const = 0.00 return floor((difficulty * const) * 10) diff --git a/titles/wacca/config.py b/titles/wacca/config.py index f5bc235..e96c3f4 100644 --- a/titles/wacca/config.py +++ b/titles/wacca/config.py @@ -1,41 +1,65 @@ from typing import Dict, List from core.config import CoreConfig -class WaccaServerConfig(): + +class WaccaServerConfig: def __init__(self, parent_config: "WaccaConfig") -> None: self.__config = parent_config - + @property def enable(self) -> bool: - return CoreConfig.get_config_field(self.__config, 'wacca', 'server', 'enable', default=True) - + return CoreConfig.get_config_field( + self.__config, "wacca", "server", "enable", default=True + ) + @property def loglevel(self) -> int: - return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'wacca', 'server', 'loglevel', default="info")) + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "wacca", "server", "loglevel", default="info" + ) + ) -class WaccaModsConfig(): + @property + def prefecture_name(self) -> str: + return CoreConfig.get_config_field( + self.__config, "wacca", "server", "prefecture_name", default="Hokkaido" + ) + + +class WaccaModsConfig: def __init__(self, parent_config: "WaccaConfig") -> None: self.__config = parent_config - + @property def always_vip(self) -> bool: - return CoreConfig.get_config_field(self.__config, 'wacca', 'mods', 'always_vip', default=True) - + return CoreConfig.get_config_field( + self.__config, "wacca", "mods", "always_vip", default=True + ) + @property def infinite_tickets(self) -> bool: - return CoreConfig.get_config_field(self.__config, 'wacca', 'mods', 'infinite_tickets', default=True) + return CoreConfig.get_config_field( + self.__config, "wacca", "mods", "infinite_tickets", default=True + ) @property def infinite_wp(self) -> bool: - return CoreConfig.get_config_field(self.__config, 'wacca', 'mods', 'infinite_wp', default=True) + return CoreConfig.get_config_field( + self.__config, "wacca", "mods", "infinite_wp", default=True + ) -class WaccaGateConfig(): + +class WaccaGateConfig: def __init__(self, parent_config: "WaccaConfig") -> None: self.__config = parent_config @property def enabled_gates(self) -> List[int]: - return CoreConfig.get_config_field(self.__config, 'wacca', 'gates', 'enabled_gates', default=[]) + return CoreConfig.get_config_field( + self.__config, "wacca", "gates", "enabled_gates", default=[] + ) + class WaccaConfig(dict): def __init__(self) -> None: diff --git a/titles/wacca/const.py b/titles/wacca/const.py index 910f313..b25d3ac 100644 --- a/titles/wacca/const.py +++ b/titles/wacca/const.py @@ -1,6 +1,10 @@ from enum import Enum +from typing import Optional -class WaccaConstants(): +from core.const import AllnetJapanRegionId + + +class WaccaConstants: CONFIG_NAME = "wacca.yaml" GAME_CODE = "SDFE" @@ -48,58 +52,48 @@ class WaccaConstants(): } OPTIONS = { - "note_speed": 1, # 1.0 - 6.0 - "field_mask": 2, # 0-4 - "note_sound": 3, # ID - "note_color": 4, # ID - "bgm_volume": 5, # 0-100 incremements of 10 - "bg_video": 7, # ask, on, or off - - "mirror": 101, # none or left+right swap - "judge_display_pos": 102, # center, under, over, top or off - "judge_detail_display": 103, # on or off - "measure_guidelines": 105, # on or off - "guideline_mask": 106, # 0 - 5 - "judge_line_timing_adjust": 108, # -10 - 10 - "note_design": 110, # 1 - 5 - "bonus_effect": 114, # on or off - "chara_voice": 115, # "usually" or none - "score_display_method": 116, # add or subtract - "give_up": 117, # off, no touch, can't achieve s, ss, sss, pb - "guideline_spacing": 118, # none, or a-g type - "center_display": 119, # none, combo, score add, score sub, s ss sss pb boarder - "ranking_display": 120, # on or off - "stage_up_icon_display": 121, # on or off - "rating_display": 122, # on or off - "player_level_display": 123, # on or off - "touch_effect": 124, # on or off - "guide_sound_vol": 125, # 0-100 incremements of 10 - "touch_note_vol": 126, # 0-100 incremements of 10 - "hold_note_vol": 127, # 0-100 incremements of 10 - "slide_note_vol": 128, # 0-100 incremements of 10 - "snap_note_vol": 129, # 0-100 incremements of 10 - "chain_note_vol": 130, # 0-100 incremements of 10 - "bonus_note_vol": 131, # 0-100 incremements of 10 - "gate_skip": 132, # on or off - "key_beam_display": 133, # on or off - - "left_slide_note_color": 201, # red blue green or orange - "right_slide_note_color": 202, # red blue green or orange - "forward_slide_note_color": 203, # red blue green or orange - "back_slide_note_color": 204, # red blue green or orange - - "master_vol": 1001, # 0-100 incremements of 10 - "set_title_id": 1002, # ID - "set_icon_id": 1003, # ID - "set_nav_id": 1004, # ID - "set_plate_id": 1005, # ID - } - - DIFFICULTIES = { - "Normal": 1, - "Hard": 2, - "Expert": 3, - "Inferno": 4, + "note_speed": 1, # 1.0 - 6.0 + "field_mask": 2, # 0-4 + "note_sound": 3, # ID + "note_color": 4, # ID + "bgm_volume": 5, # 0-100 incremements of 10 + "bg_video": 7, # ask, on, or off + "mirror": 101, # none or left+right swap + "judge_display_pos": 102, # center, under, over, top or off + "judge_detail_display": 103, # on or off + "measure_guidelines": 105, # on or off + "guideline_mask": 106, # 0 - 5 + "judge_line_timing_adjust": 108, # -10 - 10 + "note_design": 110, # 1 - 5 + "bonus_effect": 114, # on or off + "chara_voice": 115, # "usually" or none + "score_display_method": 116, # add or subtract + "give_up": 117, # off, no touch, can't achieve s, ss, sss, pb + "guideline_spacing": 118, # none, or a-g type + "center_display": 119, # none, combo, score add, score sub, s ss sss pb boarder + "ranking_display": 120, # on or off + "stage_up_icon_display": 121, # on or off + "rating_display": 122, # on or off + "player_level_display": 123, # on or off + "touch_effect": 124, # on or off + "guide_sound_vol": 125, # 0-100 incremements of 10 + "touch_note_vol": 126, # 0-100 incremements of 10 + "hold_note_vol": 127, # 0-100 incremements of 10 + "slide_note_vol": 128, # 0-100 incremements of 10 + "snap_note_vol": 129, # 0-100 incremements of 10 + "chain_note_vol": 130, # 0-100 incremements of 10 + "bonus_note_vol": 131, # 0-100 incremements of 10 + "gate_skip": 132, # on or off + "key_beam_display": 133, # on or off + "left_slide_note_color": 201, # red blue green or orange + "right_slide_note_color": 202, # red blue green or orange + "forward_slide_note_color": 203, # red blue green or orange + "back_slide_note_color": 204, # red blue green or orange + "master_vol": 1001, # 0-100 incremements of 10 + "set_title_id": 1002, # ID + "set_icon_id": 1003, # ID + "set_nav_id": 1004, # ID + "set_plate_id": 1005, # ID } class Difficulty(Enum): @@ -108,6 +102,124 @@ class WaccaConstants(): EXPERT = 3 INFERNO = 4 + class Region(Enum): + NONE = 0 + HOKKAIDO = 1 + AOMORI = 2 + IWATE = 3 + MIYAGI = 4 + AKITA = 5 + YAMAGATA = 6 + FUKUSHIMA = 7 + IBARAKI = 8 + TOCHIGI = 9 + GUNMA = 10 + SAITAMA = 11 + CHIBA = 12 + TOKYO = 13 + KANAGAWA = 14 + NIIGATA = 15 + TOYAMA = 16 + ISHIKAWA = 17 + FUKUI = 18 + YAMANASHI = 19 + NAGANO = 20 + GIFU = 21 + SHIZUOKA = 22 + AICHI = 23 + MIE = 24 + SHIGA = 25 + KYOTO = 26 + OSAKA = 27 + HYOGO = 28 + NARA = 29 + WAKAYAMA = 30 + TOTTORI = 31 + SHIMANE = 32 + OKAYAMA = 33 + HIROSHIMA = 34 + YAMAGUCHI = 35 + TOKUSHIMA = 36 + KAGAWA = 37 + EHIME = 38 + KOCHI = 39 + FUKUOKA = 40 + SAGA = 41 + NAGASAKI = 42 + KUMAMOTO = 43 + OITA = 44 + MIYAZAKI = 45 + KAGOSHIMA = 46 + OKINAWA = 47 + UNITED_STATES = 48 + USA = 48 + TAIWAN = 49 + TWN = 49 + HONG_KONG = 50 + HKG = 50 + SINGAPORE = 51 + SGP = 51 + KOREA = 52 + KOR = 52 + + VALID_COUNTRIES = set(["JPN", "USA", "KOR", "HKG", "SGP"]) + @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] + + @classmethod + def allnet_region_id_to_wacca_region(cls, region: int) -> Optional[Region]: + try: + return [ + cls.Region.NONE, + cls.Region.AICHI, + cls.Region.AOMORI, + cls.Region.AKITA, + cls.Region.ISHIKAWA, + cls.Region.IBARAKI, + cls.Region.IWATE, + cls.Region.EHIME, + cls.Region.OITA, + cls.Region.OSAKA, + cls.Region.OKAYAMA, + cls.Region.OKINAWA, + cls.Region.KAGAWA, + cls.Region.KAGOSHIMA, + cls.Region.KANAGAWA, + cls.Region.GIFU, + cls.Region.KYOTO, + cls.Region.KUMAMOTO, + cls.Region.GUNMA, + cls.Region.KOCHI, + cls.Region.SAITAMA, + cls.Region.SAGA, + cls.Region.SHIGA, + cls.Region.SHIZUOKA, + cls.Region.SHIMANE, + cls.Region.CHIBA, + cls.Region.TOKYO, + cls.Region.TOKUSHIMA, + cls.Region.TOCHIGI, + cls.Region.TOTTORI, + cls.Region.TOYAMA, + cls.Region.NAGASAKI, + cls.Region.NAGANO, + cls.Region.NARA, + cls.Region.NIIGATA, + cls.Region.HYOGO, + cls.Region.HIROSHIMA, + cls.Region.FUKUI, + cls.Region.FUKUOKA, + cls.Region.FUKUSHIMA, + cls.Region.HOKKAIDO, + cls.Region.MIE, + cls.Region.MIYAGI, + cls.Region.MIYAZAKI, + cls.Region.YAMAGATA, + cls.Region.YAMAGUCHI, + cls.Region.YAMANASHI, + cls.Region.WAKAYAMA, + ][region] + except Exception: + return None diff --git a/titles/wacca/database.py b/titles/wacca/database.py index 8d4c8a5..133e22f 100644 --- a/titles/wacca/database.py +++ b/titles/wacca/database.py @@ -2,6 +2,7 @@ from core.data import Data from core.config import CoreConfig from titles.wacca.schema import * + class WaccaData(Data): def __init__(self, cfg: CoreConfig) -> None: super().__init__(cfg) @@ -9,4 +10,4 @@ class WaccaData(Data): self.profile = WaccaProfileData(self.config, self.session) self.score = WaccaScoreData(self.config, self.session) self.item = WaccaItemData(self.config, self.session) - self.static = WaccaStaticData(self.config, self.session) \ No newline at end of file + self.static = WaccaStaticData(self.config, self.session) diff --git a/titles/wacca/frontend.py b/titles/wacca/frontend.py new file mode 100644 index 0000000..cc40644 --- /dev/null +++ b/titles/wacca/frontend.py @@ -0,0 +1,38 @@ +import yaml +import jinja2 +from twisted.web.http import Request +from os import path +from twisted.web.server import Session + +from core.frontend import FE_Base, IUserSession +from core.config import CoreConfig +from titles.wacca.database import WaccaData +from titles.wacca.config import WaccaConfig +from titles.wacca.const import WaccaConstants + + +class WaccaFrontend(FE_Base): + def __init__( + self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str + ) -> None: + super().__init__(cfg, environment) + self.data = WaccaData(cfg) + self.game_cfg = WaccaConfig() + if path.exists(f"{cfg_dir}/{WaccaConstants.CONFIG_NAME}"): + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{WaccaConstants.CONFIG_NAME}")) + ) + self.nav_name = "Wacca" + + def render_GET(self, request: Request) -> bytes: + template = self.environment.get_template( + "titles/wacca/frontend/wacca_index.jinja" + ) + sesh: Session = request.getSession() + usr_sesh = IUserSession(sesh) + + return template.render( + title=f"{self.core_config.server.name} | {self.nav_name}", + game_list=self.environment.globals["game_list"], + sesh=vars(usr_sesh) + ).encode("utf-16") diff --git a/titles/wacca/frontend/wacca_index.jinja b/titles/wacca/frontend/wacca_index.jinja new file mode 100644 index 0000000..6a5f046 --- /dev/null +++ b/titles/wacca/frontend/wacca_index.jinja @@ -0,0 +1,4 @@ +{% extends "core/frontend/index.jinja" %} +{% block content %} +

Wacca

+{% endblock content %} \ No newline at end of file diff --git a/titles/wacca/handlers/__init__.py b/titles/wacca/handlers/__init__.py index a59c7c1..f084682 100644 --- a/titles/wacca/handlers/__init__.py +++ b/titles/wacca/handlers/__init__.py @@ -6,4 +6,4 @@ from titles.wacca.handlers.user_misc import * from titles.wacca.handlers.user_music import * from titles.wacca.handlers.user_status import * from titles.wacca.handlers.user_trial import * -from titles.wacca.handlers.user_vip import * \ No newline at end of file +from titles.wacca.handlers.user_vip import * diff --git a/titles/wacca/handlers/advertise.py b/titles/wacca/handlers/advertise.py index cf41359..47c8406 100644 --- a/titles/wacca/handlers/advertise.py +++ b/titles/wacca/handlers/advertise.py @@ -1,45 +1,70 @@ from typing import List, Dict -from titles.wacca.handlers.base import BaseResponse +from titles.wacca.handlers.base import BaseResponse, BaseRequest from titles.wacca.handlers.helpers import Notice + # ---advertise/GetNews--- class GetNewsResponseV1(BaseResponse): def __init__(self) -> None: super().__init__() - self.notices: list[Notice] = [] - self.copywrightListings: list[str] = [] - self.stoppedSongs: list[int] = [] - self.stoppedJackets: list[int] = [] - self.stoppedMovies: list[int] = [] - self.stoppedIcons: list[int] = [] + self.notices: List[Notice] = [] + self.copywrightListings: List[str] = [] + self.stoppedSongs: List[int] = [] + self.stoppedJackets: List[int] = [] + self.stoppedMovies: List[int] = [] + self.stoppedIcons: List[int] = [] def make(self) -> Dict: note = [] for notice in self.notices: note.append(notice.make()) - - self.params = [ - note, - self.copywrightListings, - self.stoppedSongs, - self.stoppedJackets, - self.stoppedMovies, - self.stoppedIcons + + self.params = [ + note, + self.copywrightListings, + self.stoppedSongs, + self.stoppedJackets, + self.stoppedMovies, + self.stoppedIcons, ] return super().make() -class GetNewsResponseV2(GetNewsResponseV1): - stoppedProducts: list[int] = [] - stoppedNavs: list[int] = [] - stoppedNavVoices: list[int] = [] + +class GetNewsResponseV2(GetNewsResponseV1): + stoppedProducts: List[int] = [] def make(self) -> Dict: super().make() self.params.append(self.stoppedProducts) + + return super(GetNewsResponseV1, self).make() + + +class GetNewsResponseV3(GetNewsResponseV2): + stoppedNavs: List[int] = [] + stoppedNavVoices: List[int] = [] + + def make(self) -> Dict: + super().make() self.params.append(self.stoppedNavs) self.params.append(self.stoppedNavVoices) - + return super(GetNewsResponseV1, self).make() + + +# ---advertise/GetRanking--- +class AdvertiseGetRankingRequest(BaseRequest): + def __init__(self, data: Dict) -> None: + super().__init__(data) + self.resourceVer: int = self.params[0] + + +class AdvertiseGetRankingResponse(BaseResponse): + def __init__(self) -> None: + super().__init__() + + def make(self) -> Dict: + return super().make() diff --git a/titles/wacca/handlers/base.py b/titles/wacca/handlers/base.py index 1e1197b..abfed5f 100644 --- a/titles/wacca/handlers/base.py +++ b/titles/wacca/handlers/base.py @@ -1,15 +1,18 @@ from typing import Dict, List +from titles.wacca.handlers.helpers import Version from datetime import datetime -class BaseRequest(): + +class BaseRequest: def __init__(self, data: Dict) -> None: self.requestNo: int = data["requestNo"] - self.appVersion: str = data["appVersion"] + self.appVersion: Version = Version(data["appVersion"]) self.boardId: str = data["boardId"] self.chipId: str = data["chipId"] self.params: List = data["params"] -class BaseResponse(): + +class BaseResponse: def __init__(self) -> None: self.status: int = 0 self.message: str = "" @@ -27,5 +30,5 @@ class BaseResponse(): "maintNoticeTime": self.maintNoticeTime, "maintNotPlayableTime": self.maintNotPlayableTime, "maintStartTime": self.maintStartTime, - "params": self.params + "params": self.params, } diff --git a/titles/wacca/handlers/helpers.py b/titles/wacca/handlers/helpers.py index 19991e5..354bdaf 100644 --- a/titles/wacca/handlers/helpers.py +++ b/titles/wacca/handlers/helpers.py @@ -1,59 +1,173 @@ -from typing import List, Dict, Any +from typing import List, Optional, Any from enum import Enum from titles.wacca.const import WaccaConstants -class HousingInfo(): + +class ShortVersion: + def __init__(self, version: str = "", major=1, minor=0, patch=0) -> None: + split = version.split(".") + if len(split) >= 3: + self.major = int(split[0]) + self.minor = int(split[1]) + self.patch = int(split[2]) + + else: + self.major = major + self.minor = minor + self.patch = patch + + def __str__(self) -> str: + return f"{self.major}.{self.minor}.{self.patch}" + + def __int__(self) -> int: + return (self.major * 10000) + (self.minor * 100) + self.patch + + def __eq__(self, other: "ShortVersion"): + return ( + self.major == other.major + and self.minor == other.minor + and self.patch == other.patch + ) + + def __gt__(self, other: "ShortVersion"): + if self.major > other.major: + return True + elif self.major == other.major: + if self.minor > other.minor: + return True + elif self.minor == other.minor: + if self.patch > other.patch: + return True + + return False + + def __ge__(self, other: "ShortVersion"): + if self.major > other.major: + return True + elif self.major == other.major: + if self.minor > other.minor: + return True + elif self.minor == other.minor: + if self.patch > other.patch or self.patch == other.patch: + return True + + return False + + def __lt__(self, other: "ShortVersion"): + if self.major < other.major: + return True + elif self.major == other.major: + if self.minor < other.minor: + return True + elif self.minor == other.minor: + if self.patch < other.patch: + return True + + return False + + def __le__(self, other: "ShortVersion"): + if self.major < other.major: + return True + elif self.major == other.major: + if self.minor < other.minor: + return True + elif self.minor == other.minor: + if self.patch < other.patch or self.patch == other.patch: + return True + + return False + + +class Version(ShortVersion): + def __init__( + self, version="", major=1, minor=0, patch=0, country="JPN", build=0, role="C" + ) -> None: + super().__init__(version, major, minor, patch) + split = version.split(".") + if len(split) >= 6: + self.country: str = split[3] + self.build = int(split[4]) + self.role: str = split[5] + + else: + self.country = country + self.build = build + self.role = role + + def __str__(self) -> str: + return f"{self.major}.{self.minor}.{self.patch}.{self.country}.{self.role}.{self.build}" + + +class HousingInfo: """ 1 is lan install role, 2 is country """ + id: int = 0 val: str = "" def __init__(self, id: int = 0, val: str = "") -> None: self.id = id self.val = val - - def make(self) -> List: - return [ self.id, self.val ] -class Notice(): - name: str = "" + def make(self) -> List: + return [self.id, self.val] + + +class Notice: title: str = "" message: str = "" - unknown3: str = "" - unknown4: str = "" + dialog: str = "" + fullImage: str = "" + messageImage: str = "" showTitleScreen: bool = True showWelcomeScreen: bool = True startTime: int = 0 endTime: int = 0 - voiceline: int = 0 + voiceLine: int = 0 - def __init__(self, name: str = "", title: str = "", message: str = "", start: int = 0, end: int = 0) -> None: - self.name = name + def __init__( + self, + title: str = "", + message: str = "", + dialog: str = "", + start: int = 0, + end: int = 0, + ) -> None: self.title = title self.message = message + self.dialog = dialog self.startTime = start self.endTime = end - + def make(self) -> List: - return [ self.name, self.title, self.message, self.unknown3, self.unknown4, int(self.showTitleScreen), - int(self.showWelcomeScreen), self.startTime, self.endTime, self.voiceline] + return [ + self.title, + self.message, + self.dialog, + self.fullImage, + self.messageImage, + int(self.showTitleScreen), + int(self.showWelcomeScreen), + self.startTime, + self.endTime, + self.voiceLine, + ] -class UserOption(): - opt_id: int - opt_val: Any +class UserOption: def __init__(self, opt_id: int = 0, opt_val: Any = 0) -> None: - self.opt_id = opt_id - self.opt_val = opt_val - - def make(self) -> List: - return [self.opt_id, self.opt_val] + self.optId = opt_id + self.optVal = opt_val -class UserStatusV1(): + def make(self) -> List: + return [self.optId, self.optVal] + + +class UserStatusV1: def __init__(self) -> None: - self.userId: int = -1 + self.userId: int = 0 self.username: str = "" self.userType: int = 1 self.xp: int = 0 @@ -62,10 +176,6 @@ class UserStatusV1(): self.wp: int = 0 self.titlePartIds: List[int] = [0, 0, 0] self.useCount: int = 0 - self.loginDays: int = 0 - self.loginConsecutive: int = 0 - self.loginConsecutiveDays: int = 0 - self.vipExpireTime: int = 0 def make(self) -> List: return [ @@ -78,38 +188,46 @@ class UserStatusV1(): self.wp, self.titlePartIds, self.useCount, - self.loginDays, - self.loginConsecutive, - self.loginConsecutiveDays, - self.vipExpireTime ] + class UserStatusV2(UserStatusV1): def __init__(self) -> None: super().__init__() + self.loginDays: int = 0 + self.loginConsecutive: int = 0 + self.loginConsecutiveDays: int = 0 self.loginsToday: int = 0 self.rating: int = 0 + self.vipExpireTime: int = 0 def make(self) -> List: ret = super().make() + ret.append(self.loginDays) + ret.append(self.loginConsecutive) + ret.append(self.loginConsecutiveDays) + ret.append(self.vipExpireTime) ret.append(self.loginsToday) ret.append(self.rating) return ret + class ProfileStatus(Enum): ProfileGood = 0 ProfileRegister = 1 ProfileInUse = 2 ProfileWrongRegion = 3 + class PlayVersionStatus(Enum): VersionGood = 0 VersionTooNew = 1 VersionUpgrade = 2 -class PlayModeCounts(): + +class PlayModeCounts: seasonId: int = 0 modeId: int = 0 playNum: int = 0 @@ -118,92 +236,85 @@ class PlayModeCounts(): self.seasonId = seasonId self.modeId = modeId self.playNum = playNum - - def make(self) -> List: - return [ - self.seasonId, - self.modeId, - self.playNum - ] -class SongUnlock(): + def make(self) -> List: + return [self.seasonId, self.modeId, self.playNum] + + +class SongUnlock: songId: int = 0 difficulty: int = 0 whenAppeared: int = 0 whenUnlocked: int = 0 - def __init__(self, song_id: int = 0, difficulty: int = 1, whenAppered: int = 0, whenUnlocked: int = 0) -> None: + def __init__( + self, + song_id: int = 0, + difficulty: int = 1, + whenAppered: int = 0, + whenUnlocked: int = 0, + ) -> None: self.songId = song_id self.difficulty = difficulty self.whenAppeared = whenAppered self.whenUnlocked = whenUnlocked def make(self) -> List: - return [ - self.songId, - self.difficulty, - self.whenAppeared, - self.whenUnlocked - ] + return [self.songId, self.difficulty, self.whenAppeared, self.whenUnlocked] -class GenericItemRecv(): + +class GenericItemRecv: def __init__(self, item_type: int = 1, item_id: int = 1, quantity: int = 1) -> None: self.itemId = item_id self.itemType = item_type self.quantity = quantity - - def make(self) -> List: - return [ self.itemType, self.itemId, self.quantity ] -class GenericItemSend(): + def make(self) -> List: + return [self.itemType, self.itemId, self.quantity] + + +class GenericItemSend: def __init__(self, itemId: int, itemType: int, whenAcquired: int) -> None: self.itemId = itemId self.itemType = itemType self.whenAcquired = whenAcquired - + def make(self) -> List: - return [ - self.itemId, - self.itemType, - self.whenAcquired - ] + return [self.itemId, self.itemType, self.whenAcquired] + class IconItem(GenericItemSend): uses: int = 0 - def __init__(self, itemId: int, itemType: int, uses: int, whenAcquired: int) -> None: + def __init__( + self, itemId: int, itemType: int, uses: int, whenAcquired: int + ) -> None: super().__init__(itemId, itemType, whenAcquired) self.uses = uses - - def make(self) -> List: - return [ - self.itemId, - self.itemType, - self.uses, - self.whenAcquired - ] -class TrophyItem(): - trophyId: int = 0 + def make(self) -> List: + return [self.itemId, self.itemType, self.uses, self.whenAcquired] + + +class TrophyItem: + trophyId: int = 0 season: int = 1 progress: int = 0 badgeType: int = 0 - def __init__(self, trophyId: int, season: int, progress: int, badgeType: int) -> None: + def __init__( + self, trophyId: int, season: int, progress: int, badgeType: int + ) -> None: self.trophyId = trophyId - self.season = season + self.season = season self.progress = progress self.badgeType = badgeType - - def make(self) -> List: - return [ - self.trophyId, - self.season, - self.progress, - self.badgeType - ] -class TicketItem(): + def make(self) -> List: + return [self.trophyId, self.season, self.progress, self.badgeType] + + +class TicketItem: userTicketId: int = 0 ticketId: int = 0 whenExpires: int = 0 @@ -212,18 +323,17 @@ class TicketItem(): self.userTicketId = userTicketId self.ticketId = ticketId self.whenExpires = whenExpires - + def make(self) -> List: - return [ - self.userTicketId, - self.ticketId, - self.whenExpires - ] + return [self.userTicketId, self.ticketId, self.whenExpires] + class NavigatorItem(IconItem): usesToday: int = 0 - def __init__(self, itemId: int, itemType: int, whenAcquired: int, uses: int, usesToday: int) -> None: + def __init__( + self, itemId: int, itemType: int, whenAcquired: int, uses: int, usesToday: int + ) -> None: super().__init__(itemId, itemType, uses, whenAcquired) self.usesToday = usesToday @@ -233,24 +343,43 @@ class NavigatorItem(IconItem): self.itemType, self.whenAcquired, self.uses, - self.usesToday + self.usesToday, ] -class SkillItem(): - skill_type: int + +class SkillItem: + skillType: int level: int flag: int badge: int def make(self) -> List: - return [ - self.skill_type, - self.level, - self.flag, - self.badge - ] + return [self.skillType, self.level, self.flag, self.badge] -class UserItemInfoV1(): + +class UserEventInfo: + def __init__(self) -> None: + self.eventId = 0 + self.conditionInfo: List[UserEventConditionInfo] = [] + + def make(self) -> List: + conditions = [] + for x in self.conditionInfo: + conditions.append(x.make()) + + return [self.eventId, conditions] + + +class UserEventConditionInfo: + def __init__(self) -> None: + self.achievementCondition = 0 + self.progress = 0 + + def make(self) -> List: + return [self.achievementCondition, self.progress] + + +class UserItemInfoV1: def __init__(self) -> None: self.songUnlocks: List[SongUnlock] = [] self.titles: List[GenericItemSend] = [] @@ -299,6 +428,7 @@ class UserItemInfoV1(): sounds, ] + class UserItemInfoV2(UserItemInfoV1): def __init__(self) -> None: super().__init__() @@ -307,18 +437,19 @@ class UserItemInfoV2(UserItemInfoV1): def make(self) -> List: ret = super().make() - plates = [] + plates = [] navs = [] - + for x in self.navigators: navs.append(x.make()) for x in self.plates: plates.append(x.make()) - + ret.append(navs) ret.append(plates) return ret + class UserItemInfoV3(UserItemInfoV2): def __init__(self) -> None: super().__init__() @@ -330,31 +461,46 @@ class UserItemInfoV3(UserItemInfoV2): for x in self.touchEffect: effect.append(x.make()) - + ret.append(effect) return ret -class SongDetailClearCounts(): - def __init__(self, play_ct: int = 0, clear_ct: int = 0, ml_ct: int = 0, fc_ct: int = 0, - am_ct: int = 0, counts: List[int] = None) -> None: + +class SongDetailClearCounts: + def __init__( + self, + playCt: int = 0, + clearCt: int = 0, + mlCt: int = 0, + fcCt: int = 0, + amCt: int = 0, + counts: Optional[List[int]] = None, + ) -> None: if counts is None: - self.playCt = play_ct - self.clearCt = clear_ct - self.misslessCt = ml_ct - self.fullComboCt = fc_ct - self.allMarvelousCt = am_ct - + self.playCt = playCt + self.clearCt = clearCt + self.misslessCt = mlCt + self.fullComboCt = fcCt + self.allMarvelousCt = amCt + else: self.playCt = counts[0] self.clearCt = counts[1] self.misslessCt = counts[2] self.fullComboCt = counts[3] self.allMarvelousCt = counts[4] - - def make(self) -> List: - return [self.playCt, self.clearCt, self.misslessCt, self.fullComboCt, self.allMarvelousCt] -class SongDetailGradeCountsV1(): + def make(self) -> List: + return [ + self.playCt, + self.clearCt, + self.misslessCt, + self.fullComboCt, + self.allMarvelousCt, + ] + + +class SongDetailGradeCountsV1: dCt: int cCt: int bCt: int @@ -366,8 +512,20 @@ class SongDetailGradeCountsV1(): sssCt: int masterCt: int - def __init__(self, d: int = 0, c: int = 0, b: int = 0, a: int = 0, aa: int = 0, aaa: int = 0, s: int = 0, - ss: int = 0, sss: int = 0, master: int = 0, counts: List[int] = None) -> None: + def __init__( + self, + d: int = 0, + c: int = 0, + b: int = 0, + a: int = 0, + aa: int = 0, + aaa: int = 0, + s: int = 0, + ss: int = 0, + sss: int = 0, + master: int = 0, + counts: Optional[List[int]] = None, + ) -> None: if counts is None: self.dCt = d self.cCt = c @@ -379,7 +537,7 @@ class SongDetailGradeCountsV1(): self.ssCt = ss self.sssCt = sss self.masterCt = master - + else: self.dCt = counts[0] self.cCt = counts[1] @@ -390,33 +548,75 @@ class SongDetailGradeCountsV1(): self.sCt = counts[6] self.ssCt = counts[7] self.sssCt = counts[8] - self.masterCt =counts[9] + self.masterCt = counts[9] def make(self) -> List: - return [self.dCt, self.cCt, self.bCt, self.aCt, self.aaCt, self.aaaCt, self.sCt, self.ssCt, self.sssCt, self.masterCt] + return [ + self.dCt, + self.cCt, + self.bCt, + self.aCt, + self.aaCt, + self.aaaCt, + self.sCt, + self.ssCt, + self.sssCt, + self.masterCt, + ] + class SongDetailGradeCountsV2(SongDetailGradeCountsV1): spCt: int sspCt: int ssspCt: int - def __init__(self, d: int = 0, c: int = 0, b: int = 0, a: int = 0, aa: int = 0, aaa: int = 0, s: int = 0, - ss: int = 0, sss: int = 0, master: int = 0, sp: int = 0, ssp: int = 0, sssp: int = 0, counts: List[int] = None, ) -> None: + def __init__( + self, + d: int = 0, + c: int = 0, + b: int = 0, + a: int = 0, + aa: int = 0, + aaa: int = 0, + s: int = 0, + ss: int = 0, + sss: int = 0, + master: int = 0, + sp: int = 0, + ssp: int = 0, + sssp: int = 0, + counts: Optional[List[int]] = None, + ) -> None: super().__init__(d, c, b, a, aa, aaa, s, ss, sss, master, counts) if counts is None: self.spCt = sp self.sspCt = ssp self.ssspCt = sssp - + else: self.spCt = counts[10] self.sspCt = counts[11] self.ssspCt = counts[12] def make(self) -> List: - return super().make() + [self.spCt, self.sspCt, self.ssspCt] + return [ + self.dCt, + self.cCt, + self.bCt, + self.aCt, + self.aaCt, + self.aaaCt, + self.sCt, + self.spCt, + self.ssCt, + self.sspCt, + self.sssCt, + self.ssspCt, + self.masterCt, + ] -class BestScoreDetailV1(): + +class BestScoreDetailV1: songId: int = 0 difficulty: int = 1 clearCounts: SongDetailClearCounts = SongDetailClearCounts() @@ -443,68 +643,69 @@ class BestScoreDetailV1(): self.bestCombo, self.lowestMissCtMaybe, self.isUnlock, - self.rating + self.rating, ] + class BestScoreDetailV2(BestScoreDetailV1): gradeCounts: SongDetailGradeCountsV2 = SongDetailGradeCountsV2() -class SongUpdateJudgementCounts(): + +class SongUpdateJudgementCounts: marvCt: int greatCt: int goodCt: int missCt: int - def __init__(self, marvs: int = 0, greats: int = 0, goods: int = 0, misses: int = 0) -> None: + def __init__( + self, marvs: int = 0, greats: int = 0, goods: int = 0, misses: int = 0 + ) -> None: self.marvCt = marvs self.greatCt = greats self.goodCt = goods self.missCt = misses - + def make(self) -> List: return [self.marvCt, self.greatCt, self.goodCt, self.missCt] -class SongUpdateDetail(): - songId: int - difficulty: int - level: float - score: int - judgements: SongUpdateJudgementCounts - maxCombo: int - grade: WaccaConstants.GRADES - flagCleared: bool - flagMissless: bool - flagFullcombo: bool - flagAllMarvelous: bool - flagGiveUp: bool - skillPt: int - fastCt: int - slowCt: int - flagNewRecord: bool - def __init__(self, data: List = None) -> None: +class SongUpdateDetailV1: + def __init__(self, data: List) -> None: if data is not None: self.songId = data[0] self.difficulty = data[1] self.level = data[2] self.score = data[3] - - self.judgements = SongUpdateJudgementCounts(data[4][0], data[4][1], data[4][2], data[4][3]) + + self.judgements = SongUpdateJudgementCounts( + data[4][0], data[4][1], data[4][2], data[4][3] + ) self.maxCombo = data[5] - self.grade = WaccaConstants.GRADES(data[6]) # .value to get number, .name to get letter + self.grade = WaccaConstants.GRADES( + data[6] + ) # .value to get number, .name to get letter self.flagCleared = False if data[7] == 0 else True self.flagMissless = False if data[8] == 0 else True self.flagFullcombo = False if data[9] == 0 else True self.flagAllMarvelous = False if data[10] == 0 else True self.flagGiveUp = False if data[11] == 0 else True - self.skillPt = data[12] + self.fastCt = 0 + self.slowCt = 0 + self.flagNewRecord = False + + +class SongUpdateDetailV2(SongUpdateDetailV1): + def __init__(self, data: List) -> None: + super().__init__(data) + if data is not None: self.fastCt = data[13] self.slowCt = data[14] self.flagNewRecord = False if data[15] == 0 else True -class SeasonalInfoV1(): + +class SeasonalInfoV1: def __init__(self) -> None: self.level: int = 0 self.wpObtained: int = 0 @@ -512,7 +713,7 @@ class SeasonalInfoV1(): self.cumulativeScore: int = 0 self.titlesObtained: int = 0 self.iconsObtained: int = 0 - self.skillPts: int = 0 + self.skillPts: int = 0 self.noteColorsObtained: int = 0 self.noteSoundsObtained: int = 0 @@ -526,9 +727,10 @@ class SeasonalInfoV1(): self.iconsObtained, self.skillPts, self.noteColorsObtained, - self.noteSoundsObtained + self.noteSoundsObtained, ] + class SeasonalInfoV2(SeasonalInfoV1): def __init__(self) -> None: super().__init__() @@ -538,7 +740,8 @@ class SeasonalInfoV2(SeasonalInfoV1): def make(self) -> List: return super().make() + [self.platesObtained, self.cumulativeGatePts] -class BingoPageStatus(): + +class BingoPageStatus: id = 0 location = 1 progress = 0 @@ -551,23 +754,30 @@ class BingoPageStatus(): def make(self) -> List: return [self.id, self.location, self.progress] -class BingoDetail(): + +class BingoDetail: def __init__(self, pageNumber: int) -> None: self.pageNumber = pageNumber self.pageStatus: List[BingoPageStatus] = [] - + def make(self) -> List: status = [] for x in self.pageStatus: status.append(x.make()) - return [ - self.pageNumber, - status - ] + return [self.pageNumber, status] -class GateDetailV1(): - def __init__(self, gate_id: int = 1, page: int = 1, progress: int = 0, loops: int = 0, last_used: int = 0, mission_flg = 0) -> None: + +class GateDetailV1: + def __init__( + self, + gate_id: int = 1, + page: int = 1, + progress: int = 0, + loops: int = 0, + last_used: int = 0, + mission_flg=0, + ) -> None: self.id = gate_id self.page = page self.progress = progress @@ -578,23 +788,36 @@ class GateDetailV1(): def make(self) -> List: return [self.id, 1, self.page, self.progress, self.loops, self.lastUsed] + class GateDetailV2(GateDetailV1): def make(self) -> List: return super().make() + [self.missionFlg] -class GachaInfo(): - def make() -> List: - return [] -class LastSongDetail(): +class GachaInfo: + def __init__(self, gacha_id: int = 0, gacha_roll_ct: int = 0) -> None: + self.gachaId = gacha_id + self.rollCt = gacha_roll_ct + + def make(self) -> List: + return [self.gachaId, self.rollCt] + + +class LastSongDetail: lastSongId = 90 lastSongDiff = 1 lastFolderOrd = 1 lastFolderId = 1 lastSongOrd = 1 - def __init__(self, last_song: int = 90, last_diff: int = 1, last_folder_ord: int = 1, - last_folder_id: int = 1, last_song_ord: int = 1) -> None: + def __init__( + self, + last_song: int = 90, + last_diff: int = 1, + last_folder_ord: int = 1, + last_folder_id: int = 1, + last_song_ord: int = 1, + ) -> None: self.lastSongId = last_song self.lastSongDiff = last_diff self.lastFolderOrd = last_folder_ord @@ -602,25 +825,44 @@ class LastSongDetail(): self.lastSongOrd = last_song_ord def make(self) -> List: - return [self.lastSongId, self.lastSongDiff, self.lastFolderOrd, self.lastFolderId, - self.lastSongOrd] + return [ + self.lastSongId, + self.lastSongDiff, + self.lastFolderOrd, + self.lastFolderId, + self.lastSongOrd, + ] + + +class FriendScoreDetail: + def __init__( + self, song_id: int = 0, difficulty: int = 1, best_score: int = 0 + ) -> None: + self.songId = song_id + self.difficulty = difficulty + self.bestScore = best_score -class FriendDetail(): def make(self) -> List: - return [] + return [self.songId, self.difficulty, self.bestScore] -class UserOption(): - id = 1 - val = 1 - def __init__(self, id: int = 1, val: int = val) -> None: - self.id = id - self.val = val - +class FriendDetail: + def __init__(self, user_id: int = 0, username: str = "") -> None: + self.friendId = user_id + self.friendUsername = username + self.friendUserType = 1 + self.friendScoreDetail: List[FriendScoreDetail] = [] + def make(self) -> List: - return [self.id, self.val] + scores = [] -class LoginBonusInfo(): + for x in self.friendScoreDetail: + scores.append(x.make()) + + return [self.friendId, self.friendUsername, self.friendUserType, scores] + + +class LoginBonusInfo: def __init__(self) -> None: self.tickets: List[TicketItem] = [] self.items: List[GenericItemRecv] = [] @@ -632,27 +874,38 @@ class LoginBonusInfo(): for ticket in self.tickets: tks.append(ticket.make()) - + for item in self.items: itms.append(item.make()) - - return [ tks, itms, self.message ] -class VipLoginBonus(): + return [tks, itms, self.message] + + +class VipLoginBonus: id = 1 unknown = 0 item: GenericItemRecv - def __init__(self, id: int = 1, unk: int = 0, item_type: int = 1, item_id: int = 1, item_qt: int = 1) -> None: + def __init__( + self, + id: int = 1, + unk: int = 0, + item_type: int = 1, + item_id: int = 1, + item_qt: int = 1, + ) -> None: self.id = id self.unknown = unk self.item = GenericItemRecv(item_type, item_id, item_qt) def make(self) -> List: - return [ self.id, self.unknown, self.item.make() ] + return [self.id, self.unknown, self.item.make()] -class VipInfo(): - def __init__(self, year: int = 2019, month: int = 1, day: int = 1, num_item: int = 1) -> None: + +class VipInfo: + def __init__( + self, year: int = 2019, month: int = 1, day: int = 1, num_item: int = 1 + ) -> None: self.pageYear = year self.pageMonth = month self.pageDay = day @@ -666,36 +919,34 @@ class VipInfo(): for present in self.presentInfo: pres.append(present.make()) - + for b in self.vipLoginBonus: vipBonus.append(b.make()) - return [ self.pageYear, self.pageMonth, self.pageDay, self.numItem, pres, vipBonus ] + return [ + self.pageYear, + self.pageMonth, + self.pageDay, + self.numItem, + pres, + vipBonus, + ] + class PurchaseType(Enum): PurchaseTypeCredit = 1 PurchaseTypeWP = 2 + class PlayType(Enum): PlayTypeSingle = 1 PlayTypeVs = 2 PlayTypeCoop = 3 PlayTypeStageup = 4 + PlayTypeTimeFree = 5 -class SongRatingUpdate(): - song_id = 0 - difficulty = 0 - rating = 0 - def __init__(self, song: int = 0, difficulty: int = 0, rating: int = 0) -> None: - self.song_id = song - self.difficulty = difficulty - self.rating = rating - - def make(self) -> List: - return [self.song_id, self.difficulty, self.rating] - -class StageInfo(): +class StageInfo: danId: int = 0 danLevel: int = 0 clearStatus: int = 0 @@ -720,16 +971,18 @@ class StageInfo(): self.song2BestScore, self.song3BestScore, ], - self.unk5 + self.unk5, ] + class StageupClearType(Enum): FAIL = 0 CLEAR_BLUE = 1 CLEAR_SILVER = 2 CLEAR_GOLD = 3 -class MusicUpdateDetailV1(): + +class MusicUpdateDetailV1: def __init__(self) -> None: self.songId = 0 self.difficulty = 1 @@ -739,9 +992,8 @@ class MusicUpdateDetailV1(): self.score = 0 self.lowestMissCount = 0 self.maxSkillPts = 0 - self.locked = 0 - self.rating = 0 - + self.lockState = 0 + def make(self) -> List: return [ self.songId, @@ -752,21 +1004,33 @@ class MusicUpdateDetailV1(): self.score, self.lowestMissCount, self.maxSkillPts, - self.locked, - self.rating + self.lockState, ] + class MusicUpdateDetailV2(MusicUpdateDetailV1): + def __init__(self) -> None: + super().__init__() + self.rating = 0 + + def make(self) -> List: + return super().make() + [self.rating] + + +class MusicUpdateDetailV3(MusicUpdateDetailV2): def __init__(self) -> None: super().__init__() self.grades = SongDetailGradeCountsV2() -class SongRatingUpdate(): - def __init__(self, song_id: int = 0, difficulty: int = 1, new_rating: int = 0) -> None: + +class SongRatingUpdate: + def __init__( + self, song_id: int = 0, difficulty: int = 1, new_rating: int = 0 + ) -> None: self.songId = song_id self.difficulty = difficulty self.rating = new_rating - + def make(self) -> List: return [ self.songId, @@ -774,13 +1038,20 @@ class SongRatingUpdate(): self.rating, ] -class GateTutorialFlag(): + +class GateTutorialFlag: def __init__(self, tutorial_id: int = 1, flg_watched: bool = False) -> None: self.tutorialId = tutorial_id self.flagWatched = flg_watched - + def make(self) -> List: - return [ - self.tutorialId, - int(self.flagWatched) - ] + return [self.tutorialId, int(self.flagWatched)] + + +class DateUpdate: + def __init__(self, date_id: int = 0, timestamp: int = 0) -> None: + self.id = date_id + self.timestamp = timestamp + + def make(self) -> List: + return [self.id, self.timestamp] diff --git a/titles/wacca/handlers/housing.py b/titles/wacca/handlers/housing.py index 7806632..e11c61a 100644 --- a/titles/wacca/handlers/housing.py +++ b/titles/wacca/handlers/housing.py @@ -2,37 +2,125 @@ from typing import List, Dict from titles.wacca.handlers.base import BaseRequest, BaseResponse from titles.wacca.handlers.helpers import HousingInfo +from titles.wacca.const import WaccaConstants + # ---housing/get---- class HousingGetResponse(BaseResponse): def __init__(self, housingId: int) -> None: super().__init__() self.housingId: int = housingId - self.regionId: int = 0 + self.isNewCab: bool = False def make(self) -> Dict: - self.params = [self.housingId, self.regionId] + self.params = [self.housingId, int(self.isNewCab)] return super().make() + # ---housing/start---- -class HousingStartRequest(BaseRequest): +class HousingStartRequestV1(BaseRequest): def __init__(self, data: Dict) -> None: super().__init__(data) self.unknown0: str = self.params[0] self.errorLog: str = self.params[1] - self.unknown2: str = self.params[2] + self.info: List[HousingInfo] = [] + + for info in self.params[2]: + self.info.append(HousingInfo(info[0], info[1])) + + +class HousingStartRequestV2(HousingStartRequestV1): + def __init__(self, data: Dict) -> None: + super(HousingStartRequestV1, self).__init__(data) + self.unknown0: str = self.params[0] + self.errorLog: str = self.params[1] + self.creditLog: str = self.params[2] self.info: List[HousingInfo] = [] for info in self.params[3]: self.info.append(HousingInfo(info[0], info[1])) + class HousingStartResponseV1(BaseResponse): - def __init__(self, regionId: int, songList: List[int]) -> None: + def __init__( + self, + regionId: WaccaConstants.Region = WaccaConstants.Region.HOKKAIDO, + songList: List[int] = [], + ) -> None: super().__init__() self.regionId = regionId - self.songList = songList + self.songList = songList # Recomended songs + + if not self.songList: + self.songList = [ + 1269, + 1007, + 1270, + 1002, + 1020, + 1003, + 1008, + 1211, + 1018, + 1092, + 1056, + 32, + 1260, + 1230, + 1258, + 1251, + 2212, + 1264, + 1125, + 1037, + 2001, + 1272, + 1126, + 1119, + 1104, + 1070, + 1047, + 1044, + 1027, + 1004, + 1001, + 24, + 2068, + 2062, + 2021, + 1275, + 1249, + 1207, + 1203, + 1107, + 1021, + 1009, + 9, + 4, + 3, + 23, + 22, + 2014, + 13, + 1276, + 1247, + 1240, + 1237, + 1128, + 1114, + 1110, + 1109, + 1102, + 1045, + 1043, + 1036, + 1035, + 1030, + 1023, + 1015, + ] def make(self) -> Dict: - self.params = [self.regionId, self.songList] + self.params = [self.regionId.value, self.songList] return super().make() diff --git a/titles/wacca/handlers/user_info.py b/titles/wacca/handlers/user_info.py index c7336d7..b70ac35 100644 --- a/titles/wacca/handlers/user_info.py +++ b/titles/wacca/handlers/user_info.py @@ -1,7 +1,8 @@ from typing import List, Dict from titles.wacca.handlers.base import BaseRequest, BaseResponse -from titles.wacca.handlers.helpers import UserOption +from titles.wacca.handlers.helpers import UserOption, DateUpdate + # ---user/info/update--- class UserInfoUpdateRequest(BaseRequest): @@ -9,35 +10,69 @@ class UserInfoUpdateRequest(BaseRequest): super().__init__(data) self.profileId = int(self.params[0]) self.optsUpdated: List[UserOption] = [] - self.datesUpdated: List = self.params[3] - self.favoritesAdded: List[int] = self.params[4] - self.favoritesRemoved: List[int] = self.params[5] + self.unknown2: List = self.params[2] + self.datesUpdated: List[DateUpdate] = [] + self.favoritesRemoved: List[int] = self.params[4] + self.favoritesAdded: List[int] = self.params[5] - for x in self.params[2]: + for x in self.params[1]: self.optsUpdated.append(UserOption(x[0], x[1])) + for x in self.params[3]: + self.datesUpdated.append(DateUpdate(x[0], x[1])) + + # ---user/info/getMyroom--- TODO: Understand this better class UserInfogetMyroomRequest(BaseRequest): game_id = 0 + def __init__(self, data: Dict) -> None: super().__init__(data) self.game_id = int(self.params[0]) -class UserInfogetMyroomResponse(BaseResponse): + +class UserInfogetMyroomResponseV1(BaseResponse): + def __init__(self) -> None: + super().__init__() + self.titleViewBadge = 0 + self.iconViewBadge = 0 + self.trophyViewBadge = 0 + self.noteColorViewBadge = 0 + self.noteSoundViewBadge = 0 + self.userViewingInfo = [] + def make(self) -> Dict: self.params = [ - 0,0,0,0,0,[],0,0,0 + self.titleViewBadge, + self.iconViewBadge, + self.trophyViewBadge, + self.noteColorViewBadge, + self.noteSoundViewBadge, + self.userViewingInfo, ] return super().make() + +class UserInfogetMyroomResponseV2(UserInfogetMyroomResponseV1): + def __init__(self) -> None: + super().__init__() + + def make(self) -> Dict: + super().make() + self.params += [0, 0, 0] + return super(UserInfogetMyroomResponseV1, self).make() + + # ---user/info/getRanking--- class UserInfogetRankingRequest(BaseRequest): game_id = 0 + def __init__(self, data: Dict) -> None: super().__init__(data) self.game_id = int(self.params[0]) - + + class UserInfogetRankingResponse(BaseResponse): def __init__(self) -> None: super().__init__() @@ -58,4 +93,4 @@ class UserInfogetRankingResponse(BaseResponse): self.wacca_points_ranking, ] - return super().make() \ No newline at end of file + return super().make() diff --git a/titles/wacca/handlers/user_misc.py b/titles/wacca/handlers/user_misc.py index e710a8b..eb03802 100644 --- a/titles/wacca/handlers/user_misc.py +++ b/titles/wacca/handlers/user_misc.py @@ -5,6 +5,7 @@ from titles.wacca.handlers.helpers import PurchaseType, GenericItemRecv from titles.wacca.handlers.helpers import TicketItem, SongRatingUpdate, BingoDetail from titles.wacca.handlers.helpers import BingoPageStatus, GateTutorialFlag + # ---user/goods/purchase--- class UserGoodsPurchaseRequest(BaseRequest): def __init__(self, data: Dict) -> None: @@ -14,18 +15,21 @@ class UserGoodsPurchaseRequest(BaseRequest): self.purchaseCount = int(self.params[2]) self.purchaseType = PurchaseType(self.params[3]) self.cost = int(self.params[4]) - self.itemObtained: GenericItemRecv = GenericItemRecv(self.params[5][0], self.params[5][1], self.params[5][2]) + self.itemObtained: GenericItemRecv = GenericItemRecv( + self.params[5][0], self.params[5][1], self.params[5][2] + ) + class UserGoodsPurchaseResponse(BaseResponse): def __init__(self, wp: int = 0, tickets: List = []) -> None: super().__init__() self.currentWp = wp self.tickets: List[TicketItem] = [] - + for ticket in tickets: self.tickets.append(TicketItem(ticket[0], ticket[1], ticket[2])) - def make(self) -> List: + def make(self) -> Dict: tix = [] for ticket in self.tickets: tix.append(ticket.make()) @@ -34,6 +38,7 @@ class UserGoodsPurchaseResponse(BaseResponse): return super().make() + # ---user/sugaroku/update--- class UserSugarokuUpdateRequestV1(BaseRequest): def __init__(self, data: Dict) -> None: @@ -44,17 +49,19 @@ class UserSugarokuUpdateRequestV1(BaseRequest): self.progress = int(self.params[3]) self.loops = int(self.params[4]) self.boostsUsed = self.params[5] - self.totalPts = int(self.params[7]) + self.totalPts = int(self.params[7]) self.itemsObtainted: List[GenericItemRecv] = [] for item in self.params[6]: self.itemsObtainted.append(GenericItemRecv(item[0], item[1], item[2])) + class UserSugarokuUpdateRequestV2(UserSugarokuUpdateRequestV1): def __init__(self, data: Dict) -> None: super().__init__(data) self.mission_flag = int(self.params[8]) + # ---user/rating/update--- class UserRatingUpdateRequest(BaseRequest): def __init__(self, data: Dict) -> None: @@ -66,8 +73,9 @@ class UserRatingUpdateRequest(BaseRequest): for x in self.params[2]: self.songs.append(SongRatingUpdate(x[0], x[1], x[2])) + # ---user/mission/update--- -class UserMissionUpdateRequest(BaseRequest): +class UserMissionUpdateRequest(BaseRequest): def __init__(self, data: Dict) -> None: super().__init__(data) self.profileId = self.params[0] diff --git a/titles/wacca/handlers/user_music.py b/titles/wacca/handlers/user_music.py index adb11b6..26c2167 100644 --- a/titles/wacca/handlers/user_music.py +++ b/titles/wacca/handlers/user_music.py @@ -1,92 +1,109 @@ from typing import List, Dict from titles.wacca.handlers.base import BaseRequest, BaseResponse -from titles.wacca.handlers.helpers import GenericItemRecv, SongUpdateDetail, TicketItem -from titles.wacca.handlers.helpers import MusicUpdateDetailV1, MusicUpdateDetailV2 -from titles.wacca.handlers.helpers import SeasonalInfoV2, SeasonalInfoV1 +from titles.wacca.handlers.helpers import ( + GenericItemRecv, + SongUpdateDetailV2, + TicketItem, +) +from titles.wacca.handlers.helpers import MusicUpdateDetailV2, MusicUpdateDetailV3 +from titles.wacca.handlers.helpers import ( + SeasonalInfoV2, + SeasonalInfoV1, + SongUpdateDetailV1, +) +from titles.wacca.handlers.helpers import MusicUpdateDetailV1 + # ---user/music/update--- -class UserMusicUpdateRequest(BaseRequest): +class UserMusicUpdateRequestV1(BaseRequest): def __init__(self, data: Dict) -> None: super().__init__(data) self.profileId: int = self.params[0] self.songNumber: int = self.params[1] - self.songDetail = SongUpdateDetail(self.params[2]) + self.songDetail = SongUpdateDetailV1(self.params[2]) self.itemsObtained: List[GenericItemRecv] = [] for itm in data["params"][3]: self.itemsObtained.append(GenericItemRecv(itm[0], itm[1], itm[2])) + +class UserMusicUpdateRequestV2(UserMusicUpdateRequestV1): + def __init__(self, data: Dict) -> None: + super().__init__(data) + self.songDetail = SongUpdateDetailV2(self.params[2]) + + class UserMusicUpdateResponseV1(BaseResponse): def __init__(self) -> None: super().__init__() self.songDetail = MusicUpdateDetailV1() self.seasonInfo = SeasonalInfoV1() self.rankingInfo: List[List[int]] = [] - + def make(self) -> Dict: self.params = [ self.songDetail.make(), [self.songDetail.songId, self.songDetail.clearCounts.playCt], self.seasonInfo.make(), - self.rankingInfo + self.rankingInfo, ] return super().make() + class UserMusicUpdateResponseV2(UserMusicUpdateResponseV1): def __init__(self) -> None: super().__init__() + self.songDetail = MusicUpdateDetailV2() self.seasonInfo = SeasonalInfoV2() + class UserMusicUpdateResponseV3(UserMusicUpdateResponseV2): def __init__(self) -> None: super().__init__() - self.songDetail = MusicUpdateDetailV2() + self.songDetail = MusicUpdateDetailV3() + # ---user/music/updateCoop--- -class UserMusicUpdateCoopRequest(UserMusicUpdateRequest): +class UserMusicUpdateCoopRequest(UserMusicUpdateRequestV2): def __init__(self, data: Dict) -> None: super().__init__(data) self.coopData = self.params[4] + # ---user/music/updateVs--- -class UserMusicUpdateVsRequest(UserMusicUpdateRequest): +class UserMusicUpdateVsRequest(UserMusicUpdateRequestV2): def __init__(self, data: Dict) -> None: super().__init__(data) self.vsData = self.params[4] + # ---user/music/unlock--- class UserMusicUnlockRequest(BaseRequest): def __init__(self, data: Dict) -> None: super().__init__(data) self.profileId = self.params[0] self.songId = self.params[1] - self.difficulty = self.params[2] + self.difficulty = self.params[2] self.itemsUsed: List[GenericItemRecv] = [] for itm in self.params[3]: self.itemsUsed.append(GenericItemRecv(itm[0], itm[1], itm[2])) -class UserMusicUnlockResponse(BaseResponse): - def __init__(self, current_wp: int = 0, tickets_remaining: List = []) -> None: - super().__init__() - self.wp = current_wp - self.tickets: List[TicketItem] = [] - for ticket in tickets_remaining: - self.tickets.append(TicketItem(ticket[0], ticket[1], ticket[2])) - - def make(self) -> List: +class UserMusicUnlockResponse(BaseResponse): + def __init__(self, current_wp: int = 0, tickets_remaining: List[TicketItem] = []) -> None: + super().__init__() + self.wp = current_wp + self.tickets = tickets_remaining + + def make(self) -> Dict: tickets = [] for ticket in self.tickets: tickets.append(ticket.make()) - self.params = [ - self.wp, - tickets - ] + self.params = [self.wp, tickets] return super().make() - diff --git a/titles/wacca/handlers/user_status.py b/titles/wacca/handlers/user_status.py index 66d0bae..6eef16a 100644 --- a/titles/wacca/handlers/user_status.py +++ b/titles/wacca/handlers/user_status.py @@ -3,6 +3,7 @@ from typing import List, Dict, Optional from titles.wacca.handlers.base import BaseRequest, BaseResponse from titles.wacca.handlers.helpers import * + # ---user/status/get---- class UserStatusGetRequest(BaseRequest): aimeId: int = 0 @@ -11,6 +12,7 @@ class UserStatusGetRequest(BaseRequest): super().__init__(data) self.aimeId = int(data["params"][0]) + class UserStatusGetV1Response(BaseResponse): def __init__(self) -> None: super().__init__() @@ -19,7 +21,7 @@ class UserStatusGetV1Response(BaseResponse): self.setIconId: int = 0 self.profileStatus: ProfileStatus = ProfileStatus.ProfileGood self.versionStatus: PlayVersionStatus = PlayVersionStatus.VersionGood - self.lastGameVersion: str = "" + self.lastGameVersion: ShortVersion = ShortVersion() def make(self) -> Dict: self.params = [ @@ -27,27 +29,30 @@ class UserStatusGetV1Response(BaseResponse): self.setTitleId, self.setIconId, self.profileStatus.value, - [ - self.versionStatus.value, - self.lastGameVersion - ] + [self.versionStatus.value, str(self.lastGameVersion)], ] - + return super().make() + class UserStatusGetV2Response(UserStatusGetV1Response): def __init__(self) -> None: super().__init__() self.userStatus: UserStatusV2 = UserStatusV2() - self.unknownArr: List = [] + self.options: List[UserOption] = [] def make(self) -> Dict: super().make() + opts = [] - self.params.append(self.unknownArr) + for x in self.options: + opts.append(x.make()) + + self.params.append(opts) return super(UserStatusGetV1Response, self).make() + # ---user/status/getDetail---- class UserStatusGetDetailRequest(BaseRequest): userId: int = 0 @@ -56,6 +61,7 @@ class UserStatusGetDetailRequest(BaseRequest): super().__init__(data) self.userId = data["params"][0] + class UserStatusGetDetailResponseV1(BaseResponse): def __init__(self) -> None: super().__init__() @@ -64,22 +70,32 @@ class UserStatusGetDetailResponseV1(BaseResponse): self.seasonalPlayModeCounts: List[PlayModeCounts] = [] self.userItems: UserItemInfoV1 = UserItemInfoV1() self.scores: List[BestScoreDetailV1] = [] - self.songPlayStatus: List[int] = [0,0] - self.seasonInfo: SeasonalInfoV1 = [] - self.playAreaList: List = [ [0],[0,0,0,0,0,0],[0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0],[0,0,0,0],[0,0,0,0,0,0,0],[0] ] + self.songPlayStatus: List[int] = [0, 0] + self.seasonInfo: SeasonalInfoV1 = SeasonalInfoV1() + self.playAreaList: List = [ + [0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0], + ] self.songUpdateTime: int = 0 - def make(self) -> List: + def make(self) -> Dict: opts = [] play_modes = [] scores = [] for x in self.seasonalPlayModeCounts: play_modes.append(x.make()) - + for x in self.scores: scores.append(x.make()) - + for x in self.options: opts.append(x.make()) @@ -92,21 +108,31 @@ class UserStatusGetDetailResponseV1(BaseResponse): self.songPlayStatus, self.seasonInfo.make(), self.playAreaList, - self.songUpdateTime + self.songUpdateTime, ] return super().make() - - def find_score_idx(self, song_id: int, difficulty: int = 1, start_idx: int = 0, stop_idx: int = None) -> Optional[int]: + + def find_score_idx( + self, + song_id: int, + difficulty: int = 1, + start_idx: int = 0, + stop_idx: Optional[int] = None, + ) -> Optional[int]: if stop_idx is None or stop_idx > len(self.scores): stop_idx = len(self.scores) for x in range(start_idx, stop_idx): - if self.scores[x].songId == song_id and self.scores[x].difficulty == difficulty: + if ( + self.scores[x].songId == song_id + and self.scores[x].difficulty == difficulty + ): return x - + return None + class UserStatusGetDetailResponseV2(UserStatusGetDetailResponseV1): def __init__(self) -> None: super().__init__() @@ -115,48 +141,58 @@ class UserStatusGetDetailResponseV2(UserStatusGetDetailResponseV1): self.userItems: UserItemInfoV2 = UserItemInfoV2() self.favorites: List[int] = [] self.stoppedSongIds: List[int] = [] - self.eventInfo: List[int] = [] + self.eventInfo: List[UserEventInfo] = [] self.gateInfo: List[GateDetailV1] = [] self.lastSongInfo: LastSongDetail = LastSongDetail() self.gateTutorialFlags: List[GateTutorialFlag] = [] self.gatchaInfo: List[GachaInfo] = [] self.friendList: List[FriendDetail] = [] - def make(self) -> List: + def make(self) -> Dict: super().make() gates = [] friends = [] tut_flg = [] + evts = [] + gacha = [] for x in self.gateInfo: gates.append(x.make()) - + for x in self.friendList: friends.append(x.make()) - + for x in self.gateTutorialFlags: tut_flg.append(x.make()) - + while len(tut_flg) < 5: flag_id = len(tut_flg) + 1 tut_flg.append([flag_id, 0]) + for x in self.eventInfo: + evts.append(x.make()) + + for x in self.gatchaInfo: + gacha.append(x.make()) + self.params.append(self.favorites) self.params.append(self.stoppedSongIds) - self.params.append(self.eventInfo) + self.params.append(evts) self.params.append(gates) self.params.append(self.lastSongInfo.make()) self.params.append(tut_flg) - self.params.append(self.gatchaInfo) + self.params.append(gacha) self.params.append(friends) return super(UserStatusGetDetailResponseV1, self).make() + class UserStatusGetDetailResponseV3(UserStatusGetDetailResponseV2): def __init__(self) -> None: super().__init__() self.gateInfo: List[GateDetailV2] = [] + class UserStatusGetDetailResponseV4(UserStatusGetDetailResponseV3): def __init__(self) -> None: super().__init__() @@ -164,12 +200,13 @@ class UserStatusGetDetailResponseV4(UserStatusGetDetailResponseV3): self.bingoStatus: BingoDetail = BingoDetail(0) self.scores: List[BestScoreDetailV2] = [] - def make(self) -> List: + def make(self) -> Dict: super().make() self.params.append(self.bingoStatus.make()) return super(UserStatusGetDetailResponseV1, self).make() + # ---user/status/login---- class UserStatusLoginRequest(BaseRequest): userId: int = 0 @@ -178,16 +215,20 @@ class UserStatusLoginRequest(BaseRequest): super().__init__(data) self.userId = data["params"][0] + class UserStatusLoginResponseV1(BaseResponse): - def __init__(self, is_first_login_daily: bool = False, last_login_date: int = 0) -> None: + def __init__( + self, is_first_login_daily: bool = False, last_login_date: int = 0 + ) -> None: super().__init__() self.dailyBonus: List[LoginBonusInfo] = [] self.consecBonus: List[LoginBonusInfo] = [] - self.otherBonus: List[LoginBonusInfo] = [] + self.otherBonus: List[LoginBonusInfo] = [] self.firstLoginDaily = is_first_login_daily self.lastLoginDate = last_login_date - def make(self) -> List: + def make(self) -> Dict: + super().make() daily = [] consec = [] other = [] @@ -201,33 +242,41 @@ class UserStatusLoginResponseV1(BaseResponse): for bonus in self.otherBonus: other.append(bonus.make()) - self.params = [ daily, consec, other, int(self.firstLoginDaily)] + self.params = [daily, consec, other, int(self.firstLoginDaily)] return super().make() -class UserStatusLoginResponseV2(UserStatusLoginResponseV1): - vipInfo: VipInfo - lastLoginDate: int = 0 - def __init__(self, is_first_login_daily: bool = False, last_login_date: int = 0) -> None: +class UserStatusLoginResponseV2(UserStatusLoginResponseV1): + def __init__( + self, is_first_login_daily: bool = False, last_login_date: int = 0 + ) -> None: super().__init__(is_first_login_daily) self.lastLoginDate = last_login_date self.vipInfo = VipInfo() - - def make(self) -> List: + + def make(self) -> Dict: super().make() self.params.append(self.vipInfo.make()) self.params.append(self.lastLoginDate) return super(UserStatusLoginResponseV1, self).make() -class UserStatusLoginResponseV3(UserStatusLoginResponseV2): - unk: List = [] - def make(self) -> List: +class UserStatusLoginResponseV3(UserStatusLoginResponseV2): + def __init__( + self, is_first_login_daily: bool = False, last_login_date: int = 0 + ) -> None: + super().__init__(is_first_login_daily, last_login_date) + self.unk: List = ( + [] + ) # Ticket info, item info, message, title, voice name (not sure how they fit...) + + def make(self) -> Dict: super().make() self.params.append(self.unk) return super(UserStatusLoginResponseV1, self).make() + # ---user/status/create--- class UserStatusCreateRequest(BaseRequest): def __init__(self, data: Dict) -> None: @@ -235,26 +284,27 @@ class UserStatusCreateRequest(BaseRequest): self.aimeId = data["params"][0] self.username = data["params"][1] + class UserStatusCreateResponseV1(BaseResponse): def __init__(self, userId: int, username: str) -> None: super().__init__() self.userStatus = UserStatusV1() self.userStatus.userId = userId self.userStatus.username = username - - def make(self) -> List: - self.params = [ - self.userStatus.make() - ] + + def make(self) -> Dict: + self.params = [self.userStatus.make()] return super().make() + class UserStatusCreateResponseV2(UserStatusCreateResponseV1): def __init__(self, userId: int, username: str) -> None: super().__init__(userId, username) - self.userStatus: UserStatusV2 = UserStatusV2() + self.userStatus: UserStatusV2 = UserStatusV2() self.userStatus.userId = userId self.userStatus.username = username + # ---user/status/logout--- class UserStatusLogoutRequest(BaseRequest): userId: int @@ -263,6 +313,7 @@ class UserStatusLogoutRequest(BaseRequest): super().__init__(data) self.userId = data["params"][0] + # ---user/status/update--- class UserStatusUpdateRequestV1(BaseRequest): def __init__(self, data: Dict) -> None: @@ -274,16 +325,17 @@ class UserStatusUpdateRequestV1(BaseRequest): for itm in data["params"][2]: self.itemsRecieved.append(GenericItemRecv(itm[0], itm[1], itm[2])) -class UserStatusUpdateRequestV2(UserStatusUpdateRequestV1): - isContinue = False - isFirstPlayFree = False - itemsUsed = [] - lastSongInfo: LastSongDetail +class UserStatusUpdateRequestV2(UserStatusUpdateRequestV1): def __init__(self, data: Dict) -> None: super().__init__(data) self.isContinue = bool(data["params"][3]) self.isFirstPlayFree = bool(data["params"][4]) self.itemsUsed = data["params"][5] - self.lastSongInfo = LastSongDetail(data["params"][6][0], data["params"][6][1], - data["params"][6][2], data["params"][6][3], data["params"][6][4]) + self.lastSongInfo = LastSongDetail( + data["params"][6][0], + data["params"][6][1], + data["params"][6][2], + data["params"][6][3], + data["params"][6][4], + ) diff --git a/titles/wacca/handlers/user_trial.py b/titles/wacca/handlers/user_trial.py index 84bd44a..ba6ea50 100644 --- a/titles/wacca/handlers/user_trial.py +++ b/titles/wacca/handlers/user_trial.py @@ -1,6 +1,7 @@ from typing import Dict, List from titles.wacca.handlers.base import BaseRequest, BaseResponse -from titles.wacca.handlers.helpers import StageInfo, StageupClearType +from titles.wacca.handlers.helpers import StageInfo, StageupClearType, GenericItemRecv + # --user/trial/get-- class UserTrialGetRequest(BaseRequest): @@ -10,39 +11,45 @@ class UserTrialGetRequest(BaseRequest): super().__init__(data) self.profileId = self.params[0] + class UserTrialGetResponse(BaseResponse): def __init__(self) -> None: super().__init__() - + self.stageList: List[StageInfo] = [] def make(self) -> Dict: dans = [] for x in self.stageList: dans.append(x.make()) - + self.params = [dans] return super().make() + # --user/trial/update-- class UserTrialUpdateRequest(BaseRequest): def __init__(self, data: Dict) -> None: super().__init__(data) - self.profileId = self.params[0] - self.stageId = self.params[1] - self.stageLevel = self.params[2] + self.profileId: int = self.params[0] + self.stageId: int = self.params[1] + self.stageLevel: int = self.params[2] self.clearType = StageupClearType(self.params[3]) - self.songScores = self.params[4] - self.numSongsCleared = self.params[5] - self.itemsObtained = self.params[6] + self.songScores: List[int] = self.params[4] + self.numSongsCleared: int = self.params[5] + self.itemsObtained: List[GenericItemRecv] = [] self.unk7: List = [] + for x in self.params[6]: + self.itemsObtained.append(GenericItemRecv(x[0], x[1], x[2])) + if len(self.params) == 8: self.unk7 = self.params[7] + class UserTrialUpdateResponse(BaseResponse): def __init__(self) -> None: super().__init__() - + def make(self) -> Dict: - return super().make() \ No newline at end of file + return super().make() diff --git a/titles/wacca/handlers/user_vip.py b/titles/wacca/handlers/user_vip.py index c48c9fa..bc418b5 100644 --- a/titles/wacca/handlers/user_vip.py +++ b/titles/wacca/handlers/user_vip.py @@ -2,12 +2,14 @@ from typing import Dict, List from titles.wacca.handlers.base import BaseRequest, BaseResponse from titles.wacca.handlers.helpers import VipLoginBonus + # --user/vip/get-- class UserVipGetRequest(BaseRequest): def __init__(self, data: Dict) -> None: super().__init__(data) self.profileId = self.params[0] + class UserVipGetResponse(BaseResponse): def __init__(self) -> None: super().__init__() @@ -15,22 +17,16 @@ class UserVipGetResponse(BaseResponse): self.unknown1: int = 1 self.unknown2: int = 1 self.presents: List[VipLoginBonus] = [] - + def make(self) -> Dict: pres = [] for x in self.presents: pres.append(x.make()) - self.params = [ - self.vipDays, - [ - self.unknown1, - self.unknown2, - pres - ] - ] + self.params = [self.vipDays, [self.unknown1, self.unknown2, pres]] return super().make() + # --user/vip/start-- class UserVipStartRequest(BaseRequest): def __init__(self, data: Dict) -> None: @@ -39,6 +35,7 @@ class UserVipStartRequest(BaseRequest): self.cost = self.params[1] self.days = self.params[2] + class UserVipStartResponse(BaseResponse): def __init__(self, expires: int = 0) -> None: super().__init__() @@ -46,9 +43,6 @@ class UserVipStartResponse(BaseResponse): self.presents = [] def make(self) -> Dict: - self.params = [ - self.whenExpires, - self.presents - ] + self.params = [self.whenExpires, self.presents] - return super().make() \ No newline at end of file + return super().make() diff --git a/titles/wacca/index.py b/titles/wacca/index.py index 963fa7a..90e4a0b 100644 --- a/titles/wacca/index.py +++ b/titles/wacca/index.py @@ -3,27 +3,34 @@ import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler import logging import json -from datetime import datetime from hashlib import md5 from twisted.web.http import Request -from typing import Dict +from typing import Dict, Tuple, List +from os import path +import traceback +import sys -from core.config import CoreConfig -from titles.wacca.config import WaccaConfig -from titles.wacca.config import WaccaConfig -from titles.wacca.const import WaccaConstants -from titles.wacca.reverse import WaccaReverse -from titles.wacca.lilyr import WaccaLilyR -from titles.wacca.lily import WaccaLily -from titles.wacca.s import WaccaS -from titles.wacca.base import WaccaBase -from titles.wacca.handlers.base import BaseResponse +from core import CoreConfig, Utils +from .config import WaccaConfig +from .config import WaccaConfig +from .const import WaccaConstants +from .reverse import WaccaReverse +from .lilyr import WaccaLilyR +from .lily import WaccaLily +from .s import WaccaS +from .base import WaccaBase +from .handlers.base import BaseResponse, BaseRequest +from .handlers.helpers import Version -class WaccaServlet(): + +class WaccaServlet: def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: self.core_cfg = core_cfg self.game_cfg = WaccaConfig() - self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/wacca.yaml"))) + if path.exists(f"{cfg_dir}/{WaccaConstants.CONFIG_NAME}"): + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{WaccaConstants.CONFIG_NAME}")) + ) self.versions = [ WaccaBase(core_cfg, self.game_cfg), @@ -36,51 +43,104 @@ class WaccaServlet(): self.logger = logging.getLogger("wacca") log_fmt_str = "[%(asctime)s] Wacca | %(levelname)s | %(message)s" log_fmt = logging.Formatter(log_fmt_str) - fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.core_cfg.server.log_dir, "wacca"), encoding='utf8', - when="d", backupCount=10) + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.core_cfg.server.log_dir, "wacca"), + encoding="utf8", + when="d", + backupCount=10, + ) fileHandler.setFormatter(log_fmt) - + consoleHandler = logging.StreamHandler() consoleHandler.setFormatter(log_fmt) self.logger.addHandler(fileHandler) self.logger.addHandler(consoleHandler) - + self.logger.setLevel(self.game_cfg.server.loglevel) - coloredlogs.install(level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str) + coloredlogs.install( + level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str + ) + + def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: + return ( + [], + [ + ("render_POST", "/WaccaServlet/api/{api}/{endpoint}", {}), + ("render_POST", "/WaccaServlet/api/{api}/{branch}/{endpoint}", {}) + ] + ) + + @classmethod + def is_game_enabled(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> bool: + game_cfg = WaccaConfig() + if path.exists(f"{cfg_dir}/{WaccaConstants.CONFIG_NAME}"): + game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{WaccaConstants.CONFIG_NAME}")) + ) + + if not game_cfg.server.enable: + return False + + return True - def render_POST(self, request: Request, version: int, url_path: str) -> bytes: + def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: + if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80: + return ( + f"http://{self.core_cfg.title.hostname}:{Utils.get_title_port(self.core_cfg)}/WaccaServlet", + self.core_cfg.title.hostname, + ) + + return (f"http://{self.core_cfg.title.hostname}/WaccaServlet", self.core_cfg.title.hostname) + + def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes: def end(resp: Dict) -> bytes: hash = md5(json.dumps(resp, ensure_ascii=False).encode()).digest() request.responseHeaders.addRawHeader(b"X-Wacca-Hash", hash.hex().encode()) return json.dumps(resp).encode() - version_full = [] + api = matchers['api'] + branch = matchers.get('branch', '') + endpoint = matchers['endpoint'] + client_ip = Utils.get_ip_addr(request) + if branch: + url_path = f"{api}/{branch}/{endpoint}" + func_to_find = f"handle_{api}_{branch}_{endpoint}_request" + + else: + url_path = f"{api}/{endpoint}" + func_to_find = f"handle_{api}_{endpoint}_request" + try: req_json = json.loads(request.content.getvalue()) - version_full = req_json["appVersion"].split(".") - except: - self.logger.error(f"Failed to parse request toi {request.uri} -> {request.content.getvalue()}") + version_full = Version(req_json["appVersion"]) + req = BaseRequest(req_json) + + except KeyError as e: + self.logger.error( + f"Failed to parse request to {request.content.getvalue()} -> Missing required value {e}" + ) + resp = BaseResponse() + resp.status = 1 + resp.message = "不正なリクエスト エラーです" + return end(resp.make()) + + except Exception as e: + self.logger.error( + f"Failed to parse request to {url_path} -> {request.content.getvalue()} -> {e}" + ) resp = BaseResponse() resp.status = 1 resp.message = "不正なリクエスト エラーです" return end(resp.make()) - url_split = url_path.split("/") - start_req_idx = url_split.index("api") + 1 - - func_to_find = "handle_" - for x in range(len(url_split) - start_req_idx): - func_to_find += f"{url_split[x + start_req_idx]}_" - func_to_find += "request" - - ver_search = (int(version_full[0]) * 10000) + (int(version_full[1]) * 100) + int(version_full[2]) + ver_search = int(version_full) if ver_search < 15000: internal_ver = WaccaConstants.VER_WACCA - + elif ver_search >= 15000 and ver_search < 20000: internal_ver = WaccaConstants.VER_WACCA_S @@ -89,38 +149,49 @@ class WaccaServlet(): elif ver_search >= 25000 and ver_search < 30000: internal_ver = WaccaConstants.VER_WACCA_LILY_R - + elif ver_search >= 30000: internal_ver = WaccaConstants.VER_WACCA_REVERSE else: - self.logger.warning(f"Unsupported version ({req_json['appVersion']}) request {url_path} - {req_json}") + self.logger.warning( + f"Unsupported version ({req.appVersion}) request {url_path} - {req_json}" + ) resp = BaseResponse() resp.status = 1 resp.message = "不正なアプリバージョンエラーです" return end(resp.make()) - - self.logger.info(f"v{req_json['appVersion']} {url_path} request from {request.getClientAddress().host} with chipId {req_json['chipId']}") + + self.logger.info( + f"v{req.appVersion} {url_path} request from {client_ip} with chipId {req.chipId}" + ) self.logger.debug(req_json) + if not hasattr(self.versions[internal_ver], func_to_find): + self.logger.warning( + f"{req_json['appVersion']} has no handler for {func_to_find}" + ) + resp = BaseResponse().make() + return end(resp) + try: handler = getattr(self.versions[internal_ver], func_to_find) - if handler is not None: - resp = handler(req_json) - - else: - self.logger.warn(f"{req_json['appVersion']} has no handler for {func_to_find}") - resp = None - - if resp is None: - resp = BaseResponse().make() - - self.logger.debug(f"{req_json['appVersion']} response {resp}") + resp = handler(req_json) + + self.logger.debug(f"{req.appVersion} response {resp}") return end(resp) except Exception as e: - self.logger.error(f"{req_json['appVersion']} Error handling method {url_path} -> {e}") - if self.game_cfg.server.loglevel <= logging.DEBUG: - raise - resp = BaseResponse().make() - return end(resp) + self.logger.error( + f"{req.appVersion} Error handling method {url_path} -> {e}" + ) + if self.logger.level == logging.DEBUG: + tp, val, tb = sys.exc_info() + traceback.print_exception(tp, val, tb, limit=3) + with open("{0}/{1}.log".format(self.core_cfg.server.log_dir, "wacca"), "a") as f: + traceback.print_exception(tp, val, tb, limit=3, file=f) + + resp = BaseResponse() + resp.status = 1 + resp.message = "A server error occoured." + return end(resp.make()) diff --git a/titles/wacca/lily.py b/titles/wacca/lily.py index 7f9580f..33928ec 100644 --- a/titles/wacca/lily.py +++ b/titles/wacca/lily.py @@ -9,6 +9,7 @@ from titles.wacca.const import WaccaConstants from titles.wacca.handlers import * + class WaccaLily(WaccaS): def __init__(self, cfg: CoreConfig, game_cfg: WaccaConfig) -> None: super().__init__(cfg, game_cfg) @@ -17,29 +18,55 @@ class WaccaLily(WaccaS): self.OPTIONS_DEFAULTS["set_nav_id"] = 210002 self.allowed_stages = [ - (2001, 1), - (2002, 2), - (2003, 3), - (2004, 4), - (2005, 5), - (2006, 6), - (2007, 7), - (2008, 8), - (2009, 9), - (2010, 10), - (2011, 11), - (2012, 12), - (2013, 13), (2014, 14), + (2013, 13), + (2012, 12), + (2011, 11), + (2010, 10), + (2009, 9), + (2008, 8), + (2007, 7), + (2006, 6), + (2005, 5), + (2004, 4), + (2003, 3), + (2002, 2), + (2001, 1), (210001, 0), (210002, 0), (210003, 0), ] - def handle_user_status_get_request(self, data: Dict) -> List[Any]: + def handle_advertise_GetNews_request(self, data: Dict) -> Dict: + resp = GetNewsResponseV3() + return resp.make() + + def handle_user_status_create_request(self, data: Dict) -> Dict: + req = UserStatusCreateRequest(data) + ret = super().handle_user_status_create_request(data) + + new_user = self.data.profile.get_profile(aime_id=req.aimeId) + + if new_user is None: + return BaseResponse().make() + + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["user_plate"], 211001 + ) # Added lily + + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["note_sound"], 205005 + ) # Added lily + + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210002 + ) # Lily, Added Lily + + return ret + + def handle_user_status_get_request(self, data: Dict) -> Dict: req = UserStatusGetRequest(data) resp = UserStatusGetV2Response() - ver_split = req.appVersion.split(".") profile = self.data.profile.get_profile(aime_id=req.aimeId) if profile is None: @@ -47,14 +74,14 @@ class WaccaLily(WaccaS): resp.profileStatus = ProfileStatus.ProfileRegister return resp.make() + opts = self.data.profile.get_options(req.aimeId) + self.logger.info(f"User preview for {req.aimeId} from {req.chipId}") if profile["last_game_ver"] is None: - profile_ver_split = ver_split - resp.lastGameVersion = req.appVersion + resp.lastGameVersion = ShortVersion(str(req.appVersion)) else: - profile_ver_split = profile["last_game_ver"].split(".") - resp.lastGameVersion = profile["last_game_ver"] - + resp.lastGameVersion = ShortVersion(profile["last_game_ver"]) + resp.userStatus.userId = profile["id"] resp.userStatus.username = profile["username"] resp.userStatus.xp = profile["xp"] @@ -67,106 +94,111 @@ class WaccaLily(WaccaS): resp.userStatus.loginsToday = profile["login_count_today"] resp.userStatus.rating = profile["rating"] - set_title_id = self.data.profile.get_options(WaccaConstants.OPTIONS["set_title_id"], profile["user"]) + set_title_id = self.data.profile.get_options( + WaccaConstants.OPTIONS["set_title_id"], profile["user"] + ) if set_title_id is None: set_title_id = self.OPTIONS_DEFAULTS["set_title_id"] resp.setTitleId = set_title_id - set_icon_id = self.data.profile.get_options(WaccaConstants.OPTIONS["set_title_id"], profile["user"]) + set_icon_id = self.data.profile.get_options( + WaccaConstants.OPTIONS["set_title_id"], profile["user"] + ) if set_icon_id is None: set_icon_id = self.OPTIONS_DEFAULTS["set_icon_id"] resp.setIconId = set_icon_id - if profile["last_login_date"].timestamp() < int(datetime.now().replace(hour=0,minute=0,second=0,microsecond=0).timestamp()): + if profile["last_login_date"].timestamp() < int( + datetime.now() + .replace(hour=0, minute=0, second=0, microsecond=0) + .timestamp() + ): resp.userStatus.loginsToday = 0 - if profile["last_login_date"].timestamp() < int((datetime.now().replace(hour=0,minute=0,second=0,microsecond=0) - timedelta(days=1)).timestamp()): + if profile["last_login_date"].timestamp() < int( + ( + datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) + - timedelta(days=1) + ).timestamp() + ): resp.userStatus.loginConsecutiveDays = 0 - if int(ver_split[0]) > int(profile_ver_split[0]): + if req.appVersion > resp.lastGameVersion: resp.versionStatus = PlayVersionStatus.VersionUpgrade - elif int(ver_split[0]) < int(profile_ver_split[0]): + elif req.appVersion < resp.lastGameVersion: resp.versionStatus = PlayVersionStatus.VersionTooNew - - else: - if int(ver_split[1]) > int(profile_ver_split[1]): - resp.versionStatus = PlayVersionStatus.VersionUpgrade - - elif int(ver_split[1]) < int(profile_ver_split[1]): - resp.versionStatus = PlayVersionStatus.VersionTooNew - - else: - if int(ver_split[2]) > int(profile_ver_split[2]): - resp.versionStatus = PlayVersionStatus.VersionUpgrade - - - elif int(ver_split[2]) < int(profile_ver_split[2]): - resp.versionStatus = PlayVersionStatus.VersionTooNew - + if profile["vip_expire_time"] is not None: resp.userStatus.vipExpireTime = int(profile["vip_expire_time"].timestamp()) - + if profile["always_vip"] or self.game_config.mods.always_vip: - resp.userStatus.vipExpireTime = int((datetime.now() + timedelta(days=30)).timestamp()) - + resp.userStatus.vipExpireTime = int( + (datetime.now() + timedelta(days=30)).timestamp() + ) + if self.game_config.mods.infinite_wp: resp.userStatus.wp = 999999 - + + for opt in opts: + resp.options.append(UserOption(opt["opt_id"], opt["value"])) + return resp.make() - def handle_user_status_login_request(self, data: Dict) -> List[Any]: + def handle_user_status_login_request(self, data: Dict) -> Dict: req = UserStatusLoginRequest(data) resp = UserStatusLoginResponseV2() - is_new_day = False - is_consec_day = False is_consec_day = True if req.userId == 0: self.logger.info(f"Guest login on {req.chipId}") resp.lastLoginDate = 0 - + else: profile = self.data.profile.get_profile(req.userId) if profile is None: - self.logger.warn(f"Unknown user id {req.userId} attempted login from {req.chipId}") + self.logger.warning( + f"Unknown user id {req.userId} attempted login from {req.chipId}" + ) return resp.make() self.logger.info(f"User {req.userId} login on {req.chipId}") last_login_time = int(profile["last_login_date"].timestamp()) resp.lastLoginDate = last_login_time - - # If somebodies login timestamp < midnight of current day, then they are logging in for the first time today - if last_login_time < int(datetime.now().replace(hour=0,minute=0,second=0,microsecond=0).timestamp()): - is_new_day = True - is_consec_day = True + midnight_today_ts = int( + datetime.now() + .replace(hour=0, minute=0, second=0, microsecond=0) + .timestamp() + ) - # If somebodies login timestamp > midnight of current day + 1 day, then they broke their daily login streak - elif last_login_time > int((datetime.now().replace(hour=0,minute=0,second=0,microsecond=0) + timedelta(days=1)).timestamp()): + # If somebodies login timestamp < midnight of current day, then they are logging in for the first time today + if last_login_time < midnight_today_ts: + resp.firstLoginDaily = True + + # If the difference between midnight today and their last login is greater then 1 day (86400 seconds) they've broken their streak + if midnight_today_ts - last_login_time > 86400: is_consec_day = False - # else, they are simply logging in again on the same day, and we don't need to do anything for that - - self.data.profile.session_login(req.userId, is_new_day, is_consec_day) + + self.data.profile.session_login( + req.userId, resp.firstLoginDaily, is_consec_day + ) resp.vipInfo.pageYear = datetime.now().year resp.vipInfo.pageMonth = datetime.now().month resp.vipInfo.pageDay = datetime.now().day resp.vipInfo.numItem = 1 - resp.firstLoginDaily = int(is_new_day) - return resp.make() - - def handle_user_status_getDetail_request(self, data: Dict) -> List[Any]: + + def handle_user_status_getDetail_request(self, data: Dict) -> Dict: req = UserStatusGetDetailRequest(data) - ver_split = req.appVersion.split(".") - if int(ver_split[1]) >= 53: + if req.appVersion.minor >= 53: resp = UserStatusGetDetailResponseV3() else: resp = UserStatusGetDetailResponseV2() profile = self.data.profile.get_profile(req.userId) if profile is None: - self.logger.warn(f"Unknown profile {req.userId}") + self.logger.warning(f"Unknown profile {req.userId}") return resp.make() self.logger.info(f"Get detail for profile {req.userId}") @@ -183,15 +215,23 @@ class WaccaLily(WaccaS): if profile["vip_expire_time"] is None: resp.userStatus.vipExpireTime = 0 - + else: - resp.userStatus.vipExpireTime = int(profile["vip_expire_time"].timestamp()) - + resp.userStatus.vipExpireTime = int(profile["vip_expire_time"].timestamp()) + if profile["always_vip"] or self.game_config.mods.always_vip: - resp.userStatus.vipExpireTime = int((self.srvtime + timedelta(days=31)).timestamp()) + resp.userStatus.vipExpireTime = int( + (self.srvtime + timedelta(days=31)).timestamp() + ) resp.songUpdateTime = int(profile["last_login_date"].timestamp()) - resp.lastSongInfo = LastSongDetail(profile["last_song_id"],profile["last_song_difficulty"],profile["last_folder_order"],profile["last_folder_id"],profile["last_song_order"]) + resp.lastSongInfo = LastSongDetail( + profile["last_song_id"], + profile["last_song_difficulty"], + profile["last_folder_order"], + profile["last_folder_id"], + profile["last_song_order"], + ) resp.songPlayStatus = [resp.lastSongInfo.lastSongId, 1] resp.userStatus.userId = profile["id"] @@ -204,41 +244,65 @@ class WaccaLily(WaccaS): resp.userStatus.loginDays = profile["login_count_days"] resp.userStatus.loginConsecutiveDays = profile["login_count_days_consec"] resp.userStatus.loginsToday = profile["login_count_today"] - resp.userStatus.rating = profile['rating'] + resp.userStatus.rating = profile["rating"] if self.game_config.mods.infinite_wp: resp.userStatus.wp = 999999 for fav in profile_favorites: resp.favorites.append(fav["song_id"]) - + if profile["friend_view_1"] is not None: pass if profile["friend_view_2"] is not None: pass if profile["friend_view_3"] is not None: pass - - resp.seasonalPlayModeCounts.append(PlayModeCounts(self.season, 1, profile["playcount_single"])) - resp.seasonalPlayModeCounts.append(PlayModeCounts(self.season, 2, profile["playcount_multi_vs"])) - resp.seasonalPlayModeCounts.append(PlayModeCounts(self.season, 3, profile["playcount_multi_coop"])) - resp.seasonalPlayModeCounts.append(PlayModeCounts(self.season, 4, profile["playcount_stageup"])) - + + resp.seasonalPlayModeCounts.append( + PlayModeCounts(self.season, 1, profile["playcount_single"]) + ) + resp.seasonalPlayModeCounts.append( + PlayModeCounts(self.season, 2, profile["playcount_multi_vs"]) + ) + resp.seasonalPlayModeCounts.append( + PlayModeCounts(self.season, 3, profile["playcount_multi_coop"]) + ) + resp.seasonalPlayModeCounts.append( + PlayModeCounts(self.season, 4, profile["playcount_stageup"]) + ) + for opt in profile_options: resp.options.append(UserOption(opt["opt_id"], opt["value"])) - + for gate in self.game_config.gates.enabled_gates: added_gate = False for user_gate in profile_gates: if user_gate["gate_id"] == gate: - if int(ver_split[1]) >= 53: - resp.gateInfo.append(GateDetailV2(user_gate["gate_id"],user_gate["page"],user_gate["progress"], - user_gate["loops"],int(user_gate["last_used"].timestamp()),user_gate["mission_flag"])) - + if req.appVersion.minor >= 53: + resp.gateInfo.append( + GateDetailV2( + user_gate["gate_id"], + user_gate["page"], + user_gate["progress"], + user_gate["loops"], + int(user_gate["last_used"].timestamp()), + user_gate["mission_flag"], + ) + ) + else: - resp.gateInfo.append(GateDetailV1(user_gate["gate_id"],user_gate["page"],user_gate["progress"], - user_gate["loops"],int(user_gate["last_used"].timestamp()),user_gate["mission_flag"])) + resp.gateInfo.append( + GateDetailV1( + user_gate["gate_id"], + user_gate["page"], + user_gate["progress"], + user_gate["loops"], + int(user_gate["last_used"].timestamp()), + user_gate["mission_flag"], + ) + ) resp.seasonInfo.cumulativeGatePts += user_gate["total_points"] @@ -246,23 +310,23 @@ class WaccaLily(WaccaS): break if not added_gate: - if int(ver_split[1]) >= 53: + if req.appVersion.minor >= 53: resp.gateInfo.append(GateDetailV2(gate)) - + else: resp.gateInfo.append(GateDetailV1(gate)) - + for unlock in profile_song_unlocks: for x in range(1, unlock["highest_difficulty"] + 1): - resp.userItems.songUnlocks.append(SongUnlock(unlock["song_id"], x, 0, int(unlock["acquire_date"].timestamp()))) - if x > 2: - resp.scores.append(BestScoreDetailV1(unlock["song_id"], x)) - - empty_scores = len(resp.scores) + resp.userItems.songUnlocks.append( + SongUnlock( + unlock["song_id"], x, 0, int(unlock["acquire_date"].timestamp()) + ) + ) + for song in profile_scores: resp.seasonInfo.cumulativeScore += song["score"] - empty_score_idx = resp.find_score_idx(song["song_id"], song["chart_id"], 0, empty_scores) - + clear_cts = SongDetailClearCounts( song["play_ct"], song["clear_ct"], @@ -272,32 +336,38 @@ class WaccaLily(WaccaS): ) grade_cts = SongDetailGradeCountsV1( - song["grade_d_ct"], song["grade_c_ct"], song["grade_b_ct"], song["grade_a_ct"], song["grade_aa_ct"], - song["grade_aaa_ct"], song["grade_s_ct"], song["grade_ss_ct"], song["grade_sss_ct"], - song["grade_master_ct"] + song["grade_d_ct"], + song["grade_c_ct"], + song["grade_b_ct"], + song["grade_a_ct"], + song["grade_aa_ct"], + song["grade_aaa_ct"], + song["grade_s_ct"], + song["grade_ss_ct"], + song["grade_sss_ct"], + song["grade_master_ct"], ) - if empty_score_idx is not None: - resp.scores[empty_score_idx].clearCounts = clear_cts - resp.scores[empty_score_idx].clearCountsSeason = clear_cts - resp.scores[empty_score_idx].gradeCounts = grade_cts - resp.scores[empty_score_idx].score = song["score"] - resp.scores[empty_score_idx].bestCombo = song["best_combo"] - resp.scores[empty_score_idx].lowestMissCtMaybe = song["lowest_miss_ct"] - resp.scores[empty_score_idx].rating = song["rating"] - - else: - deets = BestScoreDetailV1(song["song_id"], song["chart_id"]) - deets.clearCounts = clear_cts - deets.clearCountsSeason = clear_cts - deets.gradeCounts = grade_cts - deets.score = song["score"] - deets.bestCombo = song["best_combo"] - deets.lowestMissCtMaybe = song["lowest_miss_ct"] - deets.rating = song["rating"] - + deets = BestScoreDetailV1(song["song_id"], song["chart_id"]) + deets.clearCounts = clear_cts + deets.clearCountsSeason = clear_cts + deets.gradeCounts = grade_cts + deets.score = song["score"] + deets.bestCombo = song["best_combo"] + deets.lowestMissCtMaybe = song["lowest_miss_ct"] + deets.rating = song["rating"] + + resp.scores.append(deets) + for trophy in profile_trophies: - resp.userItems.trophies.append(TrophyItem(trophy["trophy_id"], trophy["season"], trophy["progress"], trophy["badge_type"])) + resp.userItems.trophies.append( + TrophyItem( + trophy["trophy_id"], + trophy["season"], + trophy["progress"], + trophy["badge_type"], + ) + ) if self.game_config.mods.infinite_tickets: for x in range(5): @@ -309,35 +379,55 @@ class WaccaLily(WaccaS): else: expire = int(ticket["expire_date"].timestamp()) - resp.userItems.tickets.append(TicketItem(ticket["id"], ticket["ticket_id"], expire)) + resp.userItems.tickets.append( + TicketItem(ticket["id"], ticket["ticket_id"], expire) + ) if profile_items: for item in profile_items: try: - if item["type"] == WaccaConstants.ITEM_TYPES["icon"]: - resp.userItems.icons.append(IconItem(item["item_id"], 1, item["use_count"], int(item["acquire_date"].timestamp()))) + resp.userItems.icons.append( + IconItem( + item["item_id"], + 1, + item["use_count"], + int(item["acquire_date"].timestamp()), + ) + ) elif item["type"] == WaccaConstants.ITEM_TYPES["navigator"]: - resp.userItems.navigators.append(NavigatorItem(item["item_id"], 1, int(item["acquire_date"].timestamp()), item["use_count"], item["use_count"])) + resp.userItems.navigators.append( + NavigatorItem( + item["item_id"], + 1, + int(item["acquire_date"].timestamp()), + item["use_count"], + item["use_count"], + ) + ) else: - itm_send = GenericItemSend(item["item_id"], 1, int(item["acquire_date"].timestamp())) + itm_send = GenericItemSend( + item["item_id"], 1, int(item["acquire_date"].timestamp()) + ) if item["type"] == WaccaConstants.ITEM_TYPES["title"]: resp.userItems.titles.append(itm_send) - + elif item["type"] == WaccaConstants.ITEM_TYPES["user_plate"]: resp.userItems.plates.append(itm_send) - + elif item["type"] == WaccaConstants.ITEM_TYPES["note_color"]: resp.userItems.noteColors.append(itm_send) elif item["type"] == WaccaConstants.ITEM_TYPES["note_sound"]: resp.userItems.noteSounds.append(itm_send) - except: - self.logger.error(f"{__name__} Failed to load item {item['item_id']} for user {profile['user']}") + except Exception: + self.logger.error( + f"Failed to load item {item['item_id']} for user {profile['user']}" + ) resp.seasonInfo.level = profile["xp"] resp.seasonInfo.wpObtained = profile["wp_total"] @@ -348,4 +438,20 @@ class WaccaLily(WaccaS): resp.seasonInfo.noteSoundsObtained = len(resp.userItems.noteSounds) resp.seasonInfo.platesObtained = len(resp.userItems.plates) - return resp.make() \ No newline at end of file + return resp.make() + + def handle_user_info_getMyroom_request(self, data: Dict) -> Dict: + return UserInfogetMyroomResponseV2().make() + + def handle_user_status_update_request(self, data: Dict) -> Dict: + super().handle_user_status_update_request(data) + req = UserStatusUpdateRequestV2(data) + self.data.profile.update_profile_lastplayed( + req.profileId, + req.lastSongInfo.lastSongId, + req.lastSongInfo.lastSongDiff, + req.lastSongInfo.lastFolderOrd, + req.lastSongInfo.lastFolderId, + req.lastSongInfo.lastSongOrd, + ) + return BaseResponse().make() diff --git a/titles/wacca/lilyr.py b/titles/wacca/lilyr.py index a359725..7204761 100644 --- a/titles/wacca/lilyr.py +++ b/titles/wacca/lilyr.py @@ -3,11 +3,15 @@ from datetime import datetime, timedelta import json from core.config import CoreConfig +from titles.wacca.handlers import Dict from titles.wacca.lily import WaccaLily from titles.wacca.config import WaccaConfig from titles.wacca.const import WaccaConstants from titles.wacca.handlers import * +from core.const import AllnetCountryCode + + class WaccaLilyR(WaccaLily): def __init__(self, cfg: CoreConfig, game_cfg: WaccaConfig) -> None: super().__init__(cfg, game_cfg) @@ -16,39 +20,87 @@ class WaccaLilyR(WaccaLily): self.OPTIONS_DEFAULTS["set_nav_id"] = 210002 self.allowed_stages = [ - (2501, 1), - (2502, 2), - (2503, 3), - (2504, 4), - (2505, 5), - (2506, 6), - (2507, 7), - (2508, 8), - (2509, 9), - (2510, 10), - (2511, 11), - (2512, 12), - (2513, 13), (2514, 14), + (2513, 13), + (2512, 12), + (2511, 11), + (2510, 10), + (2509, 9), + (2508, 8), + (2507, 7), + (2506, 6), + (2505, 5), + (2504, 4), + (2503, 3), + (2501, 2), + (2501, 1), (210001, 0), (210002, 0), (210003, 0), ] - def handle_user_status_create_request(self, data: Dict) -> List[Any]: + def handle_housing_start_request(self, data: Dict) -> Dict: + req = HousingStartRequestV2(data) + allnet_region_id = None + + machine = self.data.arcade.get_machine(req.chipId) + if machine is not None: + arcade = self.data.arcade.get_arcade(machine["arcade"]) + allnet_region_id = arcade["region_id"] + + if req.appVersion.country == AllnetCountryCode.JAPAN.value: + if allnet_region_id is not None: + region = WaccaConstants.allnet_region_id_to_wacca_region( + allnet_region_id + ) + + if region is None: + region_id = self.region_id + else: + region_id = region + + else: + region_id = self.region_id + + elif req.appVersion.country in WaccaConstants.VALID_COUNTRIES: + region_id = WaccaConstants.Region[req.appVersion.country] + + else: + region_id = WaccaConstants.Region.NONE + + resp = HousingStartResponseV1(region_id) + return resp.make() + + def handle_user_status_create_request(self, data: Dict) -> Dict: req = UserStatusCreateRequest(data) resp = super().handle_user_status_create_request(data) - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210054) # Added lily r - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210055) # Added lily r - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210056) # Added lily r - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210057) # Added lily r - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210058) # Added lily r - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210059) # Added lily r - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210060) # Added lily r - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210061) # Added lily r - + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210054 + ) # Added lily r + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210055 + ) # Added lily r + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210056 + ) # Added lily r + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210057 + ) # Added lily r + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210058 + ) # Added lily r + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210059 + ) # Added lily r + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210060 + ) # Added lily r + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210061 + ) # Added lily r + return resp - def handle_user_status_logout_request(self, data: Dict) -> List[Any]: + def handle_user_status_logout_request(self, data: Dict) -> Dict: return BaseResponse().make() diff --git a/titles/wacca/read.py b/titles/wacca/read.py index 1c8e7f8..8fe5b6a 100644 --- a/titles/wacca/read.py +++ b/titles/wacca/read.py @@ -8,42 +8,57 @@ from core.config import CoreConfig from titles.wacca.database import WaccaData from titles.wacca.const import WaccaConstants + class WaccaReader(BaseReader): - def __init__(self, config: CoreConfig, version: int, bin_dir: Optional[str], opt_dir: Optional[str], extra: Optional[str]) -> None: + 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 = WaccaData(config) try: - self.logger.info(f"Start importer for {WaccaConstants.game_ver_to_string(version)}") + self.logger.info( + f"Start importer for {WaccaConstants.game_ver_to_string(version)}" + ) except IndexError: self.logger.error(f"Invalid wacca version {version}") exit(1) - + def read(self) -> None: - if not (path.exists(f"{self.bin_dir}/Table") and path.exists(f"{self.bin_dir}/Message")): + if not ( + path.exists(f"{self.bin_dir}/Table") + and path.exists(f"{self.bin_dir}/Message") + ): self.logger.error("Could not find Table or Message folder, nothing to read") return - + self.read_music(f"{self.bin_dir}/Table", "MusicParameterTable") - + def read_music(self, base_dir: str, table: str) -> None: if not self.check_valid_pair(base_dir, table): - self.logger.warn(f"Cannot find {table} uasset/uexp pair at {base_dir}, music will not be read") + self.logger.warning( + f"Cannot find {table} uasset/uexp pair at {base_dir}, music will not be read" + ) return - - uasset=open(f"{base_dir}/{table}.uasset", "rb") - uexp=open(f"{base_dir}/{table}.uexp", "rb") - package = wacky.jsonify(uasset,uexp) + uasset = open(f"{base_dir}/{table}.uasset", "rb") + uexp = open(f"{base_dir}/{table}.uexp", "rb") + + package = wacky.jsonify(uasset, uexp) package_json = json.dumps(package, indent=4, sort_keys=True) - data=json.loads(package_json) + data = json.loads(package_json) first_elem = data[0] - wacca_data = first_elem['rows'] + wacca_data = first_elem["rows"] for i, key in enumerate(wacca_data): song_id = int(key) - title = wacca_data[str(key)]["MusicMessage"] + title = wacca_data[str(key)]["MusicMessage"] artist = wacca_data[str(key)]["ArtistMessage"] bpm = wacca_data[str(key)]["Bpm"] jacket_asset_name = wacca_data[str(key)]["JacketAssetName"] @@ -52,29 +67,69 @@ class WaccaReader(BaseReader): designer = wacca_data[str(key)]["NotesDesignerNormal"] if diff > 0: - self.data.static.put_music(self.version, song_id, 1, title, artist, bpm, diff, designer, jacket_asset_name) + self.data.static.put_music( + self.version, + song_id, + 1, + title, + artist, + bpm, + diff, + designer, + jacket_asset_name, + ) self.logger.info(f"Read song {song_id} chart 1") diff = float(wacca_data[str(key)]["DifficultyHardLv"]) designer = wacca_data[str(key)]["NotesDesignerHard"] - + if diff > 0: - self.data.static.put_music(self.version, song_id, 2, title, artist, bpm, diff, designer, jacket_asset_name) + self.data.static.put_music( + self.version, + song_id, + 2, + title, + artist, + bpm, + diff, + designer, + jacket_asset_name, + ) self.logger.info(f"Read song {song_id} chart 2") diff = float(wacca_data[str(key)]["DifficultyExtremeLv"]) designer = wacca_data[str(key)]["NotesDesignerExpert"] if diff > 0: - self.data.static.put_music(self.version, song_id, 3, title, artist, bpm, diff, designer, jacket_asset_name) + self.data.static.put_music( + self.version, + song_id, + 3, + title, + artist, + bpm, + diff, + designer, + jacket_asset_name, + ) self.logger.info(f"Read song {song_id} chart 3") diff = float(wacca_data[str(key)]["DifficultyInfernoLv"]) designer = wacca_data[str(key)]["NotesDesignerInferno"] if diff > 0: - self.data.static.put_music(self.version, song_id, 4, title, artist, bpm, diff, designer, jacket_asset_name) + self.data.static.put_music( + self.version, + song_id, + 4, + title, + artist, + bpm, + diff, + designer, + jacket_asset_name, + ) self.logger.info(f"Read song {song_id} chart 4") - + def check_valid_pair(self, dir: str, file: str) -> bool: - return path.exists(f"{dir}/{file}.uasset") and path.exists(f"{dir}/{file}.uexp") \ No newline at end of file + return path.exists(f"{dir}/{file}.uasset") and path.exists(f"{dir}/{file}.uexp") diff --git a/titles/wacca/reverse.py b/titles/wacca/reverse.py index 3d2b571..6206940 100644 --- a/titles/wacca/reverse.py +++ b/titles/wacca/reverse.py @@ -9,6 +9,7 @@ from titles.wacca.const import WaccaConstants from titles.wacca.handlers import * + class WaccaReverse(WaccaLilyR): def __init__(self, cfg: CoreConfig, game_cfg: WaccaConfig) -> None: super().__init__(cfg, game_cfg) @@ -18,20 +19,20 @@ class WaccaReverse(WaccaLilyR): self.OPTIONS_DEFAULTS["set_nav_id"] = 310001 self.allowed_stages = [ - (3001, 1), - (3002, 2), - (3003, 3), - (3004, 4), - (3005, 5), - (3006, 6), - (3007, 7), - (3008, 8), - (3009, 9), - (3010, 10), - (3011, 11), - (3012, 12), - (3013, 13), (3014, 14), + (3013, 13), + (3012, 12), + (3011, 11), + (3010, 10), + (3009, 9), + (3008, 8), + (3007, 7), + (3006, 6), + (3005, 5), + (3004, 4), + (3003, 3), + (3002, 2), + (3001, 1), # Touhou (210001, 0), (210002, 0), @@ -46,18 +47,18 @@ class WaccaReverse(WaccaLilyR): (310006, 0), ] - def handle_user_status_login_request(self, data: Dict) -> List[Any]: + def handle_user_status_login_request(self, data: Dict) -> Dict: resp = super().handle_user_status_login_request(data) resp["params"].append([]) return resp - def handle_user_status_getDetail_request(self, data: Dict) -> List[Any]: + def handle_user_status_getDetail_request(self, data: Dict) -> Dict: req = UserStatusGetDetailRequest(data) resp = UserStatusGetDetailResponseV4() profile = self.data.profile.get_profile(req.userId) if profile is None: - self.logger.warn(f"Unknown profile {req.userId}") + self.logger.warning(f"Unknown profile {req.userId}") return resp.make() self.logger.info(f"Get detail for profile {req.userId}") @@ -79,15 +80,23 @@ class WaccaReverse(WaccaLilyR): if profile["vip_expire_time"] is None: resp.userStatus.vipExpireTime = 0 - + else: - resp.userStatus.vipExpireTime = int(profile["vip_expire_time"].timestamp()) - + resp.userStatus.vipExpireTime = int(profile["vip_expire_time"].timestamp()) + if profile["always_vip"] or self.game_config.mods.always_vip: - resp.userStatus.vipExpireTime = int((self.srvtime + timedelta(days=31)).timestamp()) + resp.userStatus.vipExpireTime = int( + (self.srvtime + timedelta(days=31)).timestamp() + ) resp.songUpdateTime = int(profile["last_login_date"].timestamp()) - resp.lastSongInfo = LastSongDetail(profile["last_song_id"],profile["last_song_difficulty"],profile["last_folder_order"],profile["last_folder_id"],profile["last_song_order"]) + resp.lastSongInfo = LastSongDetail( + profile["last_song_id"], + profile["last_song_difficulty"], + profile["last_folder_order"], + profile["last_folder_id"], + profile["last_song_order"], + ) resp.songPlayStatus = [resp.lastSongInfo.lastSongId, 1] resp.userStatus.userId = profile["id"] @@ -100,42 +109,63 @@ class WaccaReverse(WaccaLilyR): resp.userStatus.loginDays = profile["login_count_days"] resp.userStatus.loginConsecutiveDays = profile["login_count_days_consec"] resp.userStatus.loginsToday = profile["login_count_today"] - resp.userStatus.rating = profile['rating'] + resp.userStatus.rating = profile["rating"] if self.game_config.mods.infinite_wp: resp.userStatus.wp = 999999 for fav in profile_favorites: resp.favorites.append(fav["song_id"]) - + if profile["friend_view_1"] is not None: pass if profile["friend_view_2"] is not None: pass if profile["friend_view_3"] is not None: pass - - resp.seasonalPlayModeCounts.append(PlayModeCounts(self.season, 1, profile["playcount_single"])) - resp.seasonalPlayModeCounts.append(PlayModeCounts(self.season, 2, profile["playcount_multi_vs"])) - resp.seasonalPlayModeCounts.append(PlayModeCounts(self.season, 3, profile["playcount_multi_coop"])) - resp.seasonalPlayModeCounts.append(PlayModeCounts(self.season, 4, profile["playcount_stageup"])) - + + resp.seasonalPlayModeCounts.append( + PlayModeCounts(self.season, 1, profile["playcount_single"]) + ) + resp.seasonalPlayModeCounts.append( + PlayModeCounts(self.season, 2, profile["playcount_multi_vs"]) + ) + resp.seasonalPlayModeCounts.append( + PlayModeCounts(self.season, 3, profile["playcount_multi_coop"]) + ) + resp.seasonalPlayModeCounts.append( + PlayModeCounts(self.season, 4, profile["playcount_stageup"]) + ) + resp.seasonalPlayModeCounts.append( + PlayModeCounts(self.season, 5, profile["playcount_time_free"]) + ) + + # For some fucking reason if this isn't here time play is disabled + resp.seasonalPlayModeCounts.append(PlayModeCounts(0, 1, 1)) + for opt in profile_options: resp.options.append(UserOption(opt["opt_id"], opt["value"])) - + if profile_bingo is not None: resp.bingoStatus = BingoDetail(profile_bingo["page_number"]) for x in profile_bingo["page_progress"]: resp.bingoStatus.pageStatus.append(BingoPageStatus(x[0], x[1], x[2])) - + for gate in self.game_config.gates.enabled_gates: added_gate = False for user_gate in profile_gates: if user_gate["gate_id"] == gate: - - resp.gateInfo.append(GateDetailV2(user_gate["gate_id"],user_gate["page"],user_gate["progress"], - user_gate["loops"],int(user_gate["last_used"].timestamp()),user_gate["mission_flag"])) + resp.gateInfo.append( + GateDetailV2( + user_gate["gate_id"], + user_gate["page"], + user_gate["progress"], + user_gate["loops"], + int(user_gate["last_used"].timestamp()), + user_gate["mission_flag"], + ) + ) resp.seasonInfo.cumulativeGatePts += user_gate["total_points"] @@ -144,18 +174,18 @@ class WaccaReverse(WaccaLilyR): if not added_gate: resp.gateInfo.append(GateDetailV2(gate)) - + for unlock in profile_song_unlocks: for x in range(1, unlock["highest_difficulty"] + 1): - resp.userItems.songUnlocks.append(SongUnlock(unlock["song_id"], x, 0, int(unlock["acquire_date"].timestamp()))) - if x > 2: - resp.scores.append(BestScoreDetailV2(unlock["song_id"], x)) - - empty_scores = len(resp.scores) + resp.userItems.songUnlocks.append( + SongUnlock( + unlock["song_id"], x, 0, int(unlock["acquire_date"].timestamp()) + ) + ) + for song in profile_scores: resp.seasonInfo.cumulativeScore += song["score"] - empty_score_idx = resp.find_score_idx(song["song_id"], song["chart_id"], 0, empty_scores) - + clear_cts = SongDetailClearCounts( song["play_ct"], song["clear_ct"], @@ -165,33 +195,41 @@ class WaccaReverse(WaccaLilyR): ) grade_cts = SongDetailGradeCountsV2( - song["grade_d_ct"], song["grade_c_ct"], song["grade_b_ct"], song["grade_a_ct"], song["grade_aa_ct"], - song["grade_aaa_ct"], song["grade_s_ct"], song["grade_ss_ct"], song["grade_sss_ct"], - song["grade_master_ct"], song["grade_sp_ct"], song["grade_ssp_ct"], song["grade_sssp_ct"] + song["grade_d_ct"], + song["grade_c_ct"], + song["grade_b_ct"], + song["grade_a_ct"], + song["grade_aa_ct"], + song["grade_aaa_ct"], + song["grade_s_ct"], + song["grade_ss_ct"], + song["grade_sss_ct"], + song["grade_master_ct"], + song["grade_sp_ct"], + song["grade_ssp_ct"], + song["grade_sssp_ct"], ) - if empty_score_idx is not None: - resp.scores[empty_score_idx].clearCounts = clear_cts - resp.scores[empty_score_idx].clearCountsSeason = clear_cts - resp.scores[empty_score_idx].gradeCounts = grade_cts - resp.scores[empty_score_idx].score = song["score"] - resp.scores[empty_score_idx].bestCombo = song["best_combo"] - resp.scores[empty_score_idx].lowestMissCtMaybe = song["lowest_miss_ct"] - resp.scores[empty_score_idx].rating = song["rating"] - - else: - deets = BestScoreDetailV2(song["song_id"], song["chart_id"]) - deets.clearCounts = clear_cts - deets.clearCountsSeason = clear_cts - deets.gradeCounts = grade_cts - deets.score = song["score"] - deets.bestCombo = song["best_combo"] - deets.lowestMissCtMaybe = song["lowest_miss_ct"] - deets.rating = song["rating"] - resp.scores.append(deets) - + deets = BestScoreDetailV2(song["song_id"], song["chart_id"]) + deets.clearCounts = clear_cts + deets.clearCountsSeason = clear_cts + deets.gradeCounts = grade_cts + deets.score = song["score"] + deets.bestCombo = song["best_combo"] + deets.lowestMissCtMaybe = song["lowest_miss_ct"] + deets.rating = song["rating"] + + resp.scores.append(deets) + for trophy in profile_trophies: - resp.userItems.trophies.append(TrophyItem(trophy["trophy_id"], trophy["season"], trophy["progress"], trophy["badge_type"])) + resp.userItems.trophies.append( + TrophyItem( + trophy["trophy_id"], + trophy["season"], + trophy["progress"], + trophy["badge_type"], + ) + ) if self.game_config.mods.infinite_tickets: for x in range(5): @@ -203,38 +241,58 @@ class WaccaReverse(WaccaLilyR): else: expire = int(ticket["expire_date"].timestamp()) - resp.userItems.tickets.append(TicketItem(ticket["id"], ticket["ticket_id"], expire)) + resp.userItems.tickets.append( + TicketItem(ticket["id"], ticket["ticket_id"], expire) + ) if profile_items: for item in profile_items: try: - if item["type"] == WaccaConstants.ITEM_TYPES["icon"]: - resp.userItems.icons.append(IconItem(item["item_id"], 1, item["use_count"], int(item["acquire_date"].timestamp()))) + resp.userItems.icons.append( + IconItem( + item["item_id"], + 1, + item["use_count"], + int(item["acquire_date"].timestamp()), + ) + ) elif item["type"] == WaccaConstants.ITEM_TYPES["navigator"]: - resp.userItems.navigators.append(NavigatorItem(item["item_id"], 1, int(item["acquire_date"].timestamp()), item["use_count"], item["use_count"])) + resp.userItems.navigators.append( + NavigatorItem( + item["item_id"], + 1, + int(item["acquire_date"].timestamp()), + item["use_count"], + item["use_count"], + ) + ) else: - itm_send = GenericItemSend(item["item_id"], 1, int(item["acquire_date"].timestamp())) + itm_send = GenericItemSend( + item["item_id"], 1, int(item["acquire_date"].timestamp()) + ) if item["type"] == WaccaConstants.ITEM_TYPES["title"]: resp.userItems.titles.append(itm_send) - + elif item["type"] == WaccaConstants.ITEM_TYPES["user_plate"]: resp.userItems.plates.append(itm_send) elif item["type"] == WaccaConstants.ITEM_TYPES["touch_effect"]: resp.userItems.touchEffect.append(itm_send) - + elif item["type"] == WaccaConstants.ITEM_TYPES["note_color"]: resp.userItems.noteColors.append(itm_send) elif item["type"] == WaccaConstants.ITEM_TYPES["note_sound"]: resp.userItems.noteSounds.append(itm_send) - except: - self.logger.error(f"{__name__} Failed to load item {item['item_id']} for user {profile['user']}") + except Exception: + self.logger.error( + f"{__name__} Failed to load item {item['item_id']} for user {profile['user']}" + ) resp.seasonInfo.level = profile["xp"] resp.seasonInfo.wpObtained = profile["wp_total"] @@ -247,12 +305,22 @@ class WaccaReverse(WaccaLilyR): return resp.make() - def handle_user_status_create_request(self, data: Dict) -> List[Any]: + def handle_user_status_create_request(self, data: Dict) -> Dict: req = UserStatusCreateRequest(data) resp = super().handle_user_status_create_request(data) - - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 310001) # Added reverse - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 310002) # Added reverse - - return resp + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 310001 + ) # Added reverse + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 310002 + ) # Added reverse + + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["touch_effect"], 312000 + ) # Added reverse + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["touch_effect"], 312001 + ) # Added reverse + + return resp diff --git a/titles/wacca/s.py b/titles/wacca/s.py index f302f44..4b1e997 100644 --- a/titles/wacca/s.py +++ b/titles/wacca/s.py @@ -9,27 +9,28 @@ from titles.wacca.const import WaccaConstants from titles.wacca.handlers import * + class WaccaS(WaccaBase): allowed_stages = [ - (1501, 1), - (1502, 2), - (1503, 3), - (1504, 4), - (1505, 5), - (1506, 6), - (1507, 7), - (1508, 8), - (1509, 9), - (1510, 10), - (1511, 11), - (1512, 12), (1513, 13), + (1512, 12), + (1511, 11), + (1510, 10), + (1509, 9), + (1508, 8), + (1507, 7), + (1506, 6), + (1505, 5), + (1514, 4), + (1513, 3), + (1512, 2), + (1511, 1), ] - + def __init__(self, cfg: CoreConfig, game_cfg: WaccaConfig) -> None: super().__init__(cfg, game_cfg) self.version = WaccaConstants.VER_WACCA_S - - def handle_advertise_GetNews_request(self, data: Dict) -> List[Any]: + + def handle_advertise_GetNews_request(self, data: Dict) -> Dict: resp = GetNewsResponseV2() return resp.make() diff --git a/titles/wacca/schema/__init__.py b/titles/wacca/schema/__init__.py index 1addd87..2ccb661 100644 --- a/titles/wacca/schema/__init__.py +++ b/titles/wacca/schema/__init__.py @@ -3,4 +3,4 @@ from titles.wacca.schema.score import WaccaScoreData from titles.wacca.schema.item import WaccaItemData from titles.wacca.schema.static import WaccaStaticData -__all__ = ["WaccaProfileData", "WaccaScoreData", "WaccaItemData", "WaccaStaticData"] \ No newline at end of file +__all__ = ["WaccaProfileData", "WaccaScoreData", "WaccaItemData", "WaccaStaticData"] diff --git a/titles/wacca/schema/item.py b/titles/wacca/schema/item.py index 76e901f..df3380a 100644 --- a/titles/wacca/schema/item.py +++ b/titles/wacca/schema/item.py @@ -12,166 +12,196 @@ item = Table( "wacca_item", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("item_id", Integer, nullable=False), - Column("type", Integer, nullable=False), + Column("type", Integer, nullable=False), Column("acquire_date", TIMESTAMP, nullable=False, server_default=func.now()), Column("use_count", Integer, server_default="0"), UniqueConstraint("user", "item_id", "type", name="wacca_item_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) ticket = Table( "wacca_ticket", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("ticket_id", Integer, nullable=False), Column("acquire_date", TIMESTAMP, nullable=False, server_default=func.now()), Column("expire_date", TIMESTAMP), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) song_unlock = Table( "wacca_song_unlock", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("song_id", Integer, nullable=False), - Column("highest_difficulty", Integer, nullable=False), + Column("highest_difficulty", Integer, nullable=False), Column("acquire_date", TIMESTAMP, nullable=False, server_default=func.now()), UniqueConstraint("user", "song_id", name="wacca_song_unlock_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) trophy = Table( "wacca_trophy", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("trophy_id", Integer, nullable=False), Column("season", Integer, nullable=False), Column("progress", Integer, nullable=False, server_default="0"), Column("badge_type", Integer, nullable=False, server_default="0"), UniqueConstraint("user", "trophy_id", "season", name="wacca_trophy_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) -class WaccaItemData(BaseData): + +class WaccaItemData(BaseData): def get_song_unlocks(self, user_id: int) -> Optional[List[Row]]: sql = song_unlock.select(song_unlock.c.user == user_id) result = self.execute(sql) - if result is None: return None - + if result is None: + return None + return result.fetchall() def unlock_song(self, user_id: int, song_id: int, difficulty: int) -> Optional[int]: sql = insert(song_unlock).values( - user=user_id, - song_id=song_id, - highest_difficulty=difficulty + user=user_id, song_id=song_id, highest_difficulty=difficulty ) conflict = sql.on_duplicate_key_update( highest_difficulty=case( - (song_unlock.c.highest_difficulty >= difficulty, song_unlock.c.highest_difficulty), + ( + song_unlock.c.highest_difficulty >= difficulty, + song_unlock.c.highest_difficulty, + ), (song_unlock.c.highest_difficulty < difficulty, difficulty), ) ) result = self.execute(conflict) if result is None: - self.logger.error(f"{__name__} failed to unlock song! user: {user_id}, song_id: {song_id}, difficulty: {difficulty}") + self.logger.error( + f"{__name__} failed to unlock song! user: {user_id}, song_id: {song_id}, difficulty: {difficulty}" + ) return None - + return result.lastrowid def put_item(self, user_id: int, item_type: int, item_id: int) -> Optional[int]: sql = insert(item).values( - user = user_id, - item_id = item_id, - type = item_type, + user=user_id, + item_id=item_id, + type=item_type, ) - conflict = sql.on_duplicate_key_update( - use_count = item.c.use_count + 1 - ) + conflict = sql.on_duplicate_key_update(use_count=item.c.use_count + 1) result = self.execute(conflict) if result is None: - self.logger.error(f"{__name__} failed to insert item! user: {user_id}, item_id: {item_id}, item_type: {item_type}") + self.logger.error( + f"{__name__} failed to insert item! user: {user_id}, item_id: {item_id}, item_type: {item_type}" + ) return None - + return result.lastrowid - - def get_items(self, user_id: int, item_type: int = None, item_id: int = None) -> Optional[List[Row]]: + + def get_items( + self, user_id: int, item_type: int = None, item_id: int = None + ) -> Optional[List[Row]]: """ A catch-all item lookup given a profile and option item type and ID specifiers """ sql = item.select( - and_(item.c.user == user_id, - item.c.type == item_type if item_type is not None else True, - item.c.item_id == item_id if item_id is not None else True) - ) - - result = self.execute(sql) - if result is None: return None - return result.fetchall() - - def get_tickets(self, user_id: int) -> Optional[List[Row]]: - sql = select(ticket).where(ticket.c.user == user_id) - - result = self.execute(sql) - if result is None: return None - return result.fetchall() - - def add_ticket(self, user_id: int, ticket_id: int) -> None: - sql = insert(ticket).values( - user = user_id, - ticket_id = ticket_id + and_( + item.c.user == user_id, + item.c.type == item_type if item_type is not None else True, + item.c.item_id == item_id if item_id is not None else True, + ) ) result = self.execute(sql) if result is None: - self.logger.error(f"add_ticket: Failed to insert wacca ticket! user_id: {user_id} ticket_id {ticket_id}") + return None + return result.fetchall() + + def get_tickets(self, user_id: int) -> Optional[List[Row]]: + sql = select(ticket).where(ticket.c.user == user_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def add_ticket(self, user_id: int, ticket_id: int) -> None: + sql = insert(ticket).values(user=user_id, ticket_id=ticket_id) + + result = self.execute(sql) + if result is None: + self.logger.error( + f"add_ticket: Failed to insert wacca ticket! user_id: {user_id} ticket_id {ticket_id}" + ) return None return result.lastrowid - + def spend_ticket(self, id: int) -> None: sql = delete(ticket).where(ticket.c.id == id) result = self.execute(sql) if result is None: - self.logger.warn(f"Failed to delete ticket id {id}") + self.logger.warning(f"Failed to delete ticket id {id}") return None - def get_trophies(self, user_id: int, season: int = None) -> Optional[List[Row]]: + def get_trophies(self, user_id: int, season: int = None) -> Optional[List[Row]]: if season is None: sql = select(trophy).where(trophy.c.user == user_id) else: - sql = select(trophy).where(and_(trophy.c.user == user_id, trophy.c.season == season)) - + sql = select(trophy).where( + and_(trophy.c.user == user_id, trophy.c.season == season) + ) + result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - - def update_trophy(self, user_id: int, trophy_id: int, season: int, progress: int, badge_type: int) -> Optional[int]: + + def update_trophy( + self, user_id: int, trophy_id: int, season: int, progress: int, badge_type: int + ) -> Optional[int]: sql = insert(trophy).values( - user = user_id, - trophy_id = trophy_id, - season = season, - progress = progress, - badge_type = badge_type + user=user_id, + trophy_id=trophy_id, + season=season, + progress=progress, + badge_type=badge_type, ) - conflict = sql.on_duplicate_key_update( - progress = progress - ) + conflict = sql.on_duplicate_key_update(progress=progress) result = self.execute(conflict) if result is None: - self.logger.error(f"update_trophy: Failed to insert wacca trophy! user_id: {user_id} trophy_id: {trophy_id} progress {progress}") + self.logger.error( + f"update_trophy: Failed to insert wacca trophy! user_id: {user_id} trophy_id: {trophy_id} progress {progress}" + ) return None return result.lastrowid - diff --git a/titles/wacca/schema/profile.py b/titles/wacca/schema/profile.py index 7237149..ff13782 100644 --- a/titles/wacca/schema/profile.py +++ b/titles/wacca/schema/profile.py @@ -7,12 +7,17 @@ from sqlalchemy.engine import Row from sqlalchemy.dialects.mysql import insert from core.data.schema import BaseData, metadata +from ..handlers.helpers import PlayType profile = Table( "wacca_profile", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("version", Integer), Column("username", String(8), nullable=False), Column("xp", Integer, server_default="0"), @@ -20,7 +25,7 @@ profile = Table( Column("wp_total", Integer, server_default="0"), Column("wp_spent", Integer, server_default="0"), Column("dan_type", Integer, server_default="0"), - Column("dan_level", Integer, server_default="0"), + Column("dan_level", Integer, server_default="0"), Column("title_0", Integer, server_default="0"), Column("title_1", Integer, server_default="0"), Column("title_2", Integer, server_default="0"), @@ -36,6 +41,7 @@ profile = Table( Column("playcount_multi_vs", Integer, server_default="0"), Column("playcount_multi_coop", Integer, server_default="0"), Column("playcount_stageup", Integer, server_default="0"), + Column("playcount_time_free", Integer, server_default="0"), Column("friend_view_1", Integer), Column("friend_view_2", Integer), Column("friend_view_3", Integer), @@ -48,14 +54,18 @@ profile = Table( Column("last_login_date", TIMESTAMP, server_default=func.now()), Column("gate_tutorial_flags", JSON), UniqueConstraint("user", "version", name="wacca_profile_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) option = Table( "wacca_option", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("opt_id", Integer, nullable=False), Column("value", Integer, nullable=False), UniqueConstraint("user", "opt_id", name="wacca_option_uk"), @@ -64,38 +74,59 @@ option = Table( bingo = Table( "wacca_bingo", metadata, - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + primary_key=True, + nullable=False, + ), Column("page_number", Integer, nullable=False), Column("page_progress", JSON, nullable=False), UniqueConstraint("user", "page_number", name="wacca_bingo_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) friend = Table( "wacca_friend", metadata, - Column("profile_sender", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column("profile_reciever", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "profile_sender", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column( + "profile_reciever", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("is_accepted", Boolean, server_default="0"), - PrimaryKeyConstraint('profile_sender', 'profile_reciever', name='arcade_owner_pk'), - mysql_charset='utf8mb4' + PrimaryKeyConstraint("profile_sender", "profile_reciever", name="arcade_owner_pk"), + mysql_charset="utf8mb4", ) favorite = Table( "wacca_favorite_song", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("song_id", Integer, nullable=False), UniqueConstraint("user", "song_id", name="wacca_favorite_song_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) gate = Table( "wacca_gate", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("gate_id", Integer, nullable=False), Column("page", Integer, nullable=False, server_default="0"), Column("progress", Integer, nullable=False, server_default="0"), @@ -106,68 +137,90 @@ gate = Table( UniqueConstraint("user", "gate_id", name="wacca_gate_uk"), ) + class WaccaProfileData(BaseData): - def create_profile(self, aime_id: int, username: str, version: int) -> Optional[int]: + def create_profile( + self, aime_id: int, username: str, version: int + ) -> Optional[int]: """ Given a game version, aime id, and username, create a profile and return it's ID """ - sql = insert(profile).values( - user=aime_id, - username=username, - version=version - ) + sql = insert(profile).values(user=aime_id, username=username, version=version) - conflict = sql.on_duplicate_key_update( - username = sql.inserted.username - ) + conflict = sql.on_duplicate_key_update(username=sql.inserted.username) result = self.execute(conflict) if result is None: - self.logger.error(f"{__name__} Failed to insert wacca profile! aime id: {aime_id} username: {username}") + self.logger.error( + f"{__name__} Failed to insert wacca profile! aime id: {aime_id} username: {username}" + ) return None return result.lastrowid - - def update_profile_playtype(self, profile_id: int, play_type: int, game_version: str) -> None: + + def update_profile_playtype( + self, profile_id: int, play_type: int, game_version: str + ) -> None: sql = profile.update(profile.c.id == profile_id).values( - playcount_single = profile.c.playcount_single + 1 if play_type == 1 else profile.c.playcount_single, - - playcount_multi_vs = profile.c.playcount_multi_vs + 1 if play_type == 2 else profile.c.playcount_multi_vs, - - playcount_multi_coop = profile.c.playcount_multi_coop + 1 if play_type == 3 else profile.c.playcount_multi_coop, - - playcount_stageup = profile.c.playcount_stageup + 1 if play_type == 4 else profile.c.playcount_stageup, - - last_game_ver = game_version, - ) - - result = self.execute(sql) - if result is None: - self.logger.error(f"update_profile: failed to update profile! profile: {profile_id}") - return None - - def update_profile_lastplayed(self, profile_id: int, last_song_id: int, last_song_difficulty: int, last_folder_order: int, - last_folder_id: int, last_song_order: int) -> None: - sql = profile.update(profile.c.id == profile_id).values( - last_song_id = last_song_id, - last_song_difficulty = last_song_difficulty, - last_folder_order = last_folder_order, - last_folder_id = last_folder_id, - last_song_order = last_song_order - ) - result = self.execute(sql) - if result is None: - self.logger.error(f"update_profile_lastplayed: failed to update profile! profile: {profile_id}") - return None - - def update_profile_dan(self, profile_id: int, dan_level: int, dan_type: int) -> Optional[int]: - sql = profile.update(profile.c.id == profile_id).values( - dan_level = dan_level, - dan_type = dan_type + playcount_single=profile.c.playcount_single + 1 + if play_type == PlayType.PlayTypeSingle.value + else profile.c.playcount_single, + playcount_multi_vs=profile.c.playcount_multi_vs + 1 + if play_type == PlayType.PlayTypeVs.value + else profile.c.playcount_multi_vs, + playcount_multi_coop=profile.c.playcount_multi_coop + 1 + if play_type == PlayType.PlayTypeCoop.value + else profile.c.playcount_multi_coop, + playcount_stageup=profile.c.playcount_stageup + 1 + if play_type == PlayType.PlayTypeStageup.value + else profile.c.playcount_stageup, + playcount_time_free=profile.c.playcount_time_free + 1 + if play_type == PlayType.PlayTypeTimeFree.value + else profile.c.playcount_time_free, + last_game_ver=game_version, ) result = self.execute(sql) if result is None: - self.logger.warn(f"update_profile_dan: Failed to update! profile {profile_id}") + self.logger.error( + f"update_profile: failed to update profile! profile: {profile_id}" + ) + return None + + def update_profile_lastplayed( + self, + profile_id: int, + last_song_id: int, + last_song_difficulty: int, + last_folder_order: int, + last_folder_id: int, + last_song_order: int, + ) -> None: + sql = profile.update(profile.c.id == profile_id).values( + last_song_id=last_song_id, + last_song_difficulty=last_song_difficulty, + last_folder_order=last_folder_order, + last_folder_id=last_folder_id, + last_song_order=last_song_order, + ) + result = self.execute(sql) + if result is None: + self.logger.error( + f"update_profile_lastplayed: failed to update profile! profile: {profile_id}" + ) + return None + + def update_profile_dan( + self, profile_id: int, dan_level: int, dan_type: int + ) -> Optional[int]: + sql = profile.update(profile.c.id == profile_id).values( + dan_level=dan_level, dan_type=dan_type + ) + + result = self.execute(sql) + if result is None: + self.logger.warning( + f"update_profile_dan: Failed to update! profile {profile_id}" + ) return None return result.lastrowid @@ -180,11 +233,14 @@ class WaccaProfileData(BaseData): elif profile_id > 0: sql = profile.select(profile.c.id == profile_id) else: - self.logger.error(f"get_profile: Bad arguments!! profile_id {profile_id} aime_id {aime_id}") + self.logger.error( + f"get_profile: Bad arguments!! profile_id {profile_id} aime_id {aime_id}" + ) return None - + result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() def get_options(self, user_id: int, option_id: int = None) -> Optional[List[Row]]: @@ -192,71 +248,83 @@ class WaccaProfileData(BaseData): Get a specific user option for a profile, or all of them if none specified """ sql = option.select( - and_(option.c.user == user_id, - option.c.opt_id == option_id if option_id is not None else True) + and_( + option.c.user == user_id, + option.c.opt_id == option_id if option_id is not None else True, + ) ) - + result = self.execute(sql) - if result is None: return None + if result is None: + return None if option_id is not None: return result.fetchone() else: return result.fetchall() - - def update_option(self, user_id: int, option_id: int, value: int) -> Optional[int]: - sql = insert(option).values( - user = user_id, - opt_id = option_id, - value = value - ) - conflict = sql.on_duplicate_key_update( - value = sql.inserted.value - ) + def update_option(self, user_id: int, option_id: int, value: int) -> Optional[int]: + sql = insert(option).values(user=user_id, opt_id=option_id, value=value) + + conflict = sql.on_duplicate_key_update(value=value) result = self.execute(conflict) if result is None: - self.logger.error(f"{__name__} failed to insert option! profile: {user_id}, option: {option_id}, value: {value}") + self.logger.error( + f"{__name__} failed to insert option! profile: {user_id}, option: {option_id}, value: {value}" + ) return None - + return result.lastrowid - + def add_favorite_song(self, user_id: int, song_id: int) -> Optional[int]: - sql = favorite.insert().values( - user=user_id, - song_id=song_id - ) + sql = favorite.insert().values(user=user_id, song_id=song_id) result = self.execute(sql) - if result is None: - self.logger.error(f"{__name__} failed to insert favorite! profile: {user_id}, song_id: {song_id}") + if result is None: + self.logger.error( + f"{__name__} failed to insert favorite! profile: {user_id}, song_id: {song_id}" + ) return None return result.lastrowid def remove_favorite_song(self, user_id: int, song_id: int) -> None: - sql = favorite.delete(and_(favorite.c.user == user_id, favorite.c.song_id == song_id)) + sql = favorite.delete( + and_(favorite.c.user == user_id, favorite.c.song_id == song_id) + ) result = self.execute(sql) - if result is None: - self.logger.error(f"{__name__} failed to remove favorite! profile: {user_id}, song_id: {song_id}") + if result is None: + self.logger.error( + f"{__name__} failed to remove favorite! profile: {user_id}, song_id: {song_id}" + ) return None def get_favorite_songs(self, user_id: int) -> Optional[List[Row]]: sql = favorite.select(favorite.c.user == user_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - + def get_gates(self, user_id: int) -> Optional[List[Row]]: sql = select(gate).where(gate.c.user == user_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - - def update_gate(self, user_id: int, gate_id: int, page: int, progress: int, loop: int, mission_flag: int, - total_points: int) -> Optional[int]: + + def update_gate( + self, + user_id: int, + gate_id: int, + page: int, + progress: int, + loop: int, + mission_flag: int, + total_points: int, + ) -> Optional[int]: sql = insert(gate).values( user=user_id, gate_id=gate_id, @@ -264,7 +332,7 @@ class WaccaProfileData(BaseData): progress=progress, loops=loop, mission_flag=mission_flag, - total_points=total_points + total_points=total_points, ) conflict = sql.on_duplicate_key_update( @@ -276,16 +344,19 @@ class WaccaProfileData(BaseData): ) result = self.execute(conflict) - if result is None: - self.logger.error(f"{__name__} failed to update gate! user: {user_id}, gate_id: {gate_id}") + if result is None: + self.logger.error( + f"{__name__} failed to update gate! user: {user_id}, gate_id: {gate_id}" + ) return None return result.lastrowid - + def get_friends(self, user_id: int) -> Optional[List[Row]]: sql = friend.select(friend.c.profile_sender == user_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() def profile_to_aime_user(self, profile_id: int) -> Optional[int]: @@ -293,136 +364,159 @@ class WaccaProfileData(BaseData): result = self.execute(sql) if result is None: - self.logger.info(f"profile_to_aime_user: No user found for profile {profile_id}") + self.logger.info( + f"profile_to_aime_user: No user found for profile {profile_id}" + ) return None this_profile = result.fetchone() if this_profile is None: - self.logger.info(f"profile_to_aime_user: No user found for profile {profile_id}") + self.logger.info( + f"profile_to_aime_user: No user found for profile {profile_id}" + ) return None - return this_profile['user'] - - def session_login(self, profile_id: int, is_new_day: bool, is_consec_day: bool) -> None: + return this_profile["user"] + + def session_login( + self, profile_id: int, is_new_day: bool, is_consec_day: bool + ) -> None: # TODO: Reset consec days counter sql = profile.update(profile.c.id == profile_id).values( - login_count = profile.c.login_count + 1, - login_count_consec = profile.c.login_count_consec + 1, - login_count_days = profile.c.login_count_days + 1 if is_new_day else profile.c.login_count_days, - login_count_days_consec = profile.c.login_count_days_consec + 1 if is_new_day and is_consec_day else profile.c.login_count_days_consec, - login_count_today = 1 if is_new_day else profile.c.login_count_today + 1, - last_login_date = func.now() - ) - - result = self.execute(sql) - if result is None: - self.logger.error(f"session_login: failed to update profile! profile: {profile_id}") - return None - - def session_logout(self, profile_id: int) -> None: - sql = profile.update(profile.c.id == id).values( - login_count_consec = 0 - ) - - result = self.execute(sql) - if result is None: - self.logger.error(f"{__name__} failed to update profile! profile: {profile_id}") - return None - - def add_xp(self, profile_id: int, xp: int) -> None: - sql = profile.update(profile.c.id == profile_id).values( - xp = profile.c.xp + xp + login_count=profile.c.login_count + 1, + login_count_consec=profile.c.login_count_consec + 1, + login_count_days=profile.c.login_count_days + 1 + if is_new_day + else profile.c.login_count_days, + login_count_days_consec=profile.c.login_count_days_consec + 1 + if is_new_day and is_consec_day + else profile.c.login_count_days_consec, + login_count_today=1 if is_new_day else profile.c.login_count_today + 1, + last_login_date=func.now(), ) result = self.execute(sql) if result is None: - self.logger.error(f"add_xp: Failed to update profile! profile_id {profile_id} xp {xp}") + self.logger.error( + f"session_login: failed to update profile! profile: {profile_id}" + ) + return None + + def session_logout(self, profile_id: int) -> None: + sql = profile.update(profile.c.id == id).values(login_count_consec=0) + + result = self.execute(sql) + if result is None: + self.logger.error( + f"{__name__} failed to update profile! profile: {profile_id}" + ) + return None + + def add_xp(self, profile_id: int, xp: int) -> None: + sql = profile.update(profile.c.id == profile_id).values(xp=profile.c.xp + xp) + + result = self.execute(sql) + if result is None: + self.logger.error( + f"add_xp: Failed to update profile! profile_id {profile_id} xp {xp}" + ) return None def add_wp(self, profile_id: int, wp: int) -> None: sql = profile.update(profile.c.id == profile_id).values( - wp = profile.c.wp + wp, - wp_total = profile.c.wp_total + wp, + wp=profile.c.wp + wp, + wp_total=profile.c.wp_total + wp, ) result = self.execute(sql) if result is None: - self.logger.error(f"add_wp: Failed to update profile! profile_id {profile_id} wp {wp}") + self.logger.error( + f"add_wp: Failed to update profile! profile_id {profile_id} wp {wp}" + ) return None def spend_wp(self, profile_id: int, wp: int) -> None: sql = profile.update(profile.c.id == profile_id).values( - wp = profile.c.wp - wp, - wp_spent = profile.c.wp_spent + wp, + wp=profile.c.wp - wp, + wp_spent=profile.c.wp_spent + wp, ) result = self.execute(sql) if result is None: - self.logger.error(f"spend_wp: Failed to update profile! profile_id {profile_id} wp {wp}") + self.logger.error( + f"spend_wp: Failed to update profile! profile_id {profile_id} wp {wp}" + ) return None - + def activate_vip(self, profile_id: int, expire_time) -> None: sql = profile.update(profile.c.id == profile_id).values( - vip_expire_time = expire_time + vip_expire_time=expire_time ) result = self.execute(sql) if result is None: - self.logger.error(f"activate_vip: Failed to update profile! profile_id {profile_id} expire_time {expire_time}") + self.logger.error( + f"activate_vip: Failed to update profile! profile_id {profile_id} expire_time {expire_time}" + ) return None def update_user_rating(self, profile_id: int, new_rating: int) -> None: - sql = profile.update(profile.c.id == profile_id).values( - rating = new_rating - ) + sql = profile.update(profile.c.id == profile_id).values(rating=new_rating) result = self.execute(sql) if result is None: - self.logger.error(f"update_user_rating: Failed to update profile! profile_id {profile_id} new_rating {new_rating}") + self.logger.error( + f"update_user_rating: Failed to update profile! profile_id {profile_id} new_rating {new_rating}" + ) return None def update_bingo(self, aime_id: int, page: int, progress: int) -> Optional[int]: sql = insert(bingo).values( - user=aime_id, - page_number=page, - page_progress=progress - ) - - conflict = sql.on_duplicate_key_update( - page_number=page, - page_progress=progress + user=aime_id, page_number=page, page_progress=progress ) + conflict = sql.on_duplicate_key_update(page_number=page, page_progress=progress) + result = self.execute(conflict) - if result is None: + if result is None: self.logger.error(f"put_bingo: failed to update! aime_id: {aime_id}") return None return result.lastrowid - + def get_bingo(self, aime_id: int) -> Optional[List[Row]]: - sql = select(bingo).where(bingo.c.user==aime_id) + sql = select(bingo).where(bingo.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() - + def get_bingo_page(self, aime_id: int, page: Dict) -> Optional[List[Row]]: - sql = select(bingo).where(and_(bingo.c.user==aime_id, bingo.c.page_number==page)) + sql = select(bingo).where( + and_(bingo.c.user == aime_id, bingo.c.page_number == page) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() def update_vip_time(self, profile_id: int, time_left) -> None: - sql = profile.update(profile.c.id == profile_id).values(vip_expire_time = time_left) + sql = profile.update(profile.c.id == profile_id).values( + vip_expire_time=time_left + ) result = self.execute(sql) if result is None: self.logger.error(f"Failed to update VIP time for profile {profile_id}") - + def update_tutorial_flags(self, profile_id: int, flags: Dict) -> None: - sql = profile.update(profile.c.id == profile_id).values(gate_tutorial_flags = flags) - + sql = profile.update(profile.c.id == profile_id).values( + gate_tutorial_flags=flags + ) + result = self.execute(sql) if result is None: - self.logger.error(f"Failed to update tutorial flags for profile {profile_id}") \ No newline at end of file + self.logger.error( + f"Failed to update tutorial flags for profile {profile_id}" + ) diff --git a/titles/wacca/schema/score.py b/titles/wacca/schema/score.py index 4b0c26c..5a3a53f 100644 --- a/titles/wacca/schema/score.py +++ b/titles/wacca/schema/score.py @@ -13,9 +13,13 @@ best_score = Table( "wacca_score_best", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("song_id", Integer), - Column("chart_id", Integer), + Column("chart_id", Integer), Column("score", Integer), Column("play_ct", Integer), Column("clear_ct", Integer), @@ -39,14 +43,18 @@ best_score = Table( Column("lowest_miss_ct", Integer), Column("rating", Integer), UniqueConstraint("user", "song_id", "chart_id", name="wacca_score_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) playlog = Table( "wacca_score_playlog", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("song_id", Integer), Column("chart_id", Integer), Column("score", Integer), @@ -61,14 +69,18 @@ playlog = Table( Column("late_ct", Integer), Column("season", Integer), Column("date_scored", TIMESTAMP, server_default=func.now()), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) stageup = Table( "wacca_score_stageup", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("version", Integer), Column("stage_id", Integer), Column("clear_status", Integer), @@ -77,19 +89,29 @@ stageup = Table( Column("song2_score", Integer), Column("song3_score", Integer), Column("play_ct", Integer, server_default="1"), - UniqueConstraint("user", "stage_id", name="wacca_score_stageup_uk"), - mysql_charset='utf8mb4' + UniqueConstraint("user", "stage_id", name="wacca_score_stageup_uk"), + mysql_charset="utf8mb4", ) + class WaccaScoreData(BaseData): - def put_best_score(self, user_id: int, song_id: int, chart_id: int, score: int, clear: List[int], - grade: List[int], best_combo: int, lowest_miss_ct: int) -> Optional[int]: + def put_best_score( + self, + user_id: int, + song_id: int, + chart_id: int, + score: int, + clear: List[int], + grade: List[int], + best_combo: int, + lowest_miss_ct: int, + ) -> Optional[int]: """ Update the user's best score for a chart """ while len(grade) < 13: grade.append(0) - + sql = insert(best_score).values( user=user_id, song_id=song_id, @@ -115,7 +137,7 @@ class WaccaScoreData(BaseData): grade_sssp_ct=grade[12], best_combo=best_combo, lowest_miss_ct=lowest_miss_ct, - rating=0 + rating=0, ) conflict = sql.on_duplicate_key_update( @@ -144,13 +166,30 @@ class WaccaScoreData(BaseData): result = self.execute(conflict) if result is None: - self.logger.error(f"{__name__}: failed to insert best score! profile: {user_id}, song: {song_id}, chart: {chart_id}") + self.logger.error( + f"{__name__}: failed to insert best score! profile: {user_id}, song: {song_id}, chart: {chart_id}" + ) return None - + return result.lastrowid - - def put_playlog(self, user_id: int, song_id: int, chart_id: int, this_score: int, clear: int, grade: int, max_combo: int, - marv_ct: int, great_ct: int, good_ct: int, miss_ct: int, fast_ct: int, late_ct: int, season: int) -> Optional[int]: + + def put_playlog( + self, + user_id: int, + song_id: int, + chart_id: int, + this_score: int, + clear: int, + grade: int, + max_combo: int, + marv_ct: int, + great_ct: int, + good_ct: int, + miss_ct: int, + fast_ct: int, + late_ct: int, + season: int, + ) -> Optional[int]: """ Add an entry to the user's play log """ @@ -168,85 +207,112 @@ class WaccaScoreData(BaseData): miss_ct=miss_ct, fast_ct=fast_ct, late_ct=late_ct, - season=season + season=season, ) result = self.execute(sql) if result is None: - self.logger.error(f"{__name__} failed to insert playlog! profile: {user_id}, song: {song_id}, chart: {chart_id}") + self.logger.error( + f"{__name__} failed to insert playlog! profile: {user_id}, song: {song_id}, chart: {chart_id}" + ) return None - + return result.lastrowid - def get_best_score(self, user_id: int, song_id: int, chart_id: int) -> Optional[Row]: + def get_best_score( + self, user_id: int, song_id: int, chart_id: int + ) -> Optional[Row]: sql = best_score.select( - and_(best_score.c.user == user_id, best_score.c.song_id == song_id, best_score.c.chart_id == chart_id) + and_( + best_score.c.user == user_id, + best_score.c.song_id == song_id, + best_score.c.chart_id == chart_id, + ) ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() def get_best_scores(self, user_id: int) -> Optional[List[Row]]: - sql = best_score.select( - best_score.c.user == user_id - ) + sql = best_score.select(best_score.c.user == user_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - def update_song_rating(self, user_id: int, song_id: int, chart_id: int, new_rating: int) -> None: + def update_song_rating( + self, user_id: int, song_id: int, chart_id: int, new_rating: int + ) -> None: sql = best_score.update( and_( - best_score.c.user == user_id, - best_score.c.song_id == song_id, - best_score.c.chart_id == chart_id - )).values( - rating = new_rating + best_score.c.user == user_id, + best_score.c.song_id == song_id, + best_score.c.chart_id == chart_id, ) + ).values(rating=new_rating) result = self.execute(sql) - if result is None: - self.logger.error(f"update_song_rating: failed to update rating! user_id: {user_id} song_id: {song_id} chart_id {chart_id} new_rating {new_rating}") + if result is None: + self.logger.error( + f"update_song_rating: failed to update rating! user_id: {user_id} song_id: {song_id} chart_id {chart_id} new_rating {new_rating}" + ) return None - def put_stageup(self, user_id: int, version: int, stage_id: int, clear_status: int, clear_song_ct: int, score1: int, - score2: int, score3: int) -> Optional[int]: + def put_stageup( + self, + user_id: int, + version: int, + stage_id: int, + clear_status: int, + clear_song_ct: int, + score1: int, + score2: int, + score3: int, + ) -> Optional[int]: sql = insert(stageup).values( - user = user_id, - version = version, - stage_id = stage_id, - clear_status = clear_status, - clear_song_ct = clear_song_ct, - song1_score = score1, - song2_score = score2, - song3_score = score3, + user=user_id, + version=version, + stage_id=stage_id, + clear_status=clear_status, + clear_song_ct=clear_song_ct, + song1_score=score1, + song2_score=score2, + song3_score=score3, ) conflict = sql.on_duplicate_key_update( - clear_status = clear_status, - clear_song_ct = clear_song_ct, - song1_score = score1, - song2_score = score2, - song3_score = score3, - play_ct = stageup.c.play_ct + 1 + clear_status=clear_status, + clear_song_ct=clear_song_ct, + song1_score=score1, + song2_score=score2, + song3_score=score3, + play_ct=stageup.c.play_ct + 1, ) result = self.execute(conflict) if result is None: - self.logger.warn(f"put_stageup: failed to update! user_id: {user_id} version: {version} stage_id: {stage_id}") + self.logger.warning( + f"put_stageup: failed to update! user_id: {user_id} version: {version} stage_id: {stage_id}" + ) return None return result.lastrowid def get_stageup(self, user_id: int, version: int) -> Optional[List[Row]]: - sql = select(stageup).where(and_(stageup.c.user==user_id, stageup.c.version==version)) + sql = select(stageup).where( + and_(stageup.c.user == user_id, stageup.c.version == version) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - - def get_stageup_stage(self, user_id: int, version: int, stage_id: int) -> Optional[Row]: + + def get_stageup_stage( + self, user_id: int, version: int, stage_id: int + ) -> Optional[Row]: sql = select(stageup).where( and_( stageup.c.user == user_id, @@ -256,5 +322,6 @@ class WaccaScoreData(BaseData): ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() diff --git a/titles/wacca/schema/static.py b/titles/wacca/schema/static.py index a5e8b41..18e4e23 100644 --- a/titles/wacca/schema/static.py +++ b/titles/wacca/schema/static.py @@ -23,46 +23,62 @@ music = Table( Column("chartDesigner", String(255)), Column("jacketFile", String(255)), UniqueConstraint("version", "songId", "chartId", name="wacca_static_music_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) + class WaccaStaticData(BaseData): - def put_music(self, version: int, song_id: int, chart_id: int, title: str, artist: str, bpm: str, - difficulty: float, chart_designer: str, jacket: str) -> Optional[int]: + def put_music( + self, + version: int, + song_id: int, + chart_id: int, + title: str, + artist: str, + bpm: str, + difficulty: float, + chart_designer: str, + jacket: str, + ) -> Optional[int]: sql = insert(music).values( - version = version, - songId = song_id, - chartId = chart_id, - title = title, - artist = artist, - bpm = bpm, - difficulty = difficulty, - chartDesigner = chart_designer, - jacketFile = jacket + version=version, + songId=song_id, + chartId=chart_id, + title=title, + artist=artist, + bpm=bpm, + difficulty=difficulty, + chartDesigner=chart_designer, + jacketFile=jacket, ) conflict = sql.on_duplicate_key_update( - title = title, - artist = artist, - bpm = bpm, - difficulty = difficulty, - chartDesigner = chart_designer, - jacketFile = jacket + title=title, + artist=artist, + bpm=bpm, + difficulty=difficulty, + chartDesigner=chart_designer, + jacketFile=jacket, ) result = self.execute(conflict) - if result is None: - self.logger.warn(f"Failed to insert music {song_id} chart {chart_id}") + if result is None: + self.logger.warning(f"Failed to insert music {song_id} chart {chart_id}") return None return result.lastrowid - def get_music_chart(self, version: int, song_id: int, chart_id: int) -> Optional[List[Row]]: - sql = select(music).where(and_( - music.c.version == version, - music.c.songId == song_id, - music.c.chartId == chart_id - )) + def get_music_chart( + self, version: int, song_id: int, chart_id: int + ) -> Optional[List[Row]]: + sql = select(music).where( + and_( + music.c.version == version, + music.c.songId == song_id, + music.c.chartId == chart_id, + ) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone()