diff --git a/Dockerfile b/Dockerfile index 81252b1..f946002 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,6 +15,7 @@ COPY dbutils.py dbutils.py COPY read.py read.py ADD core core ADD titles titles +ADD config config ADD logs logs ADD cert cert diff --git a/LICENSE.txt b/LICENSE.txt deleted file mode 100644 index 22c2c75..0000000 --- a/LICENSE.txt +++ /dev/null @@ -1,13 +0,0 @@ - DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE - Version 2, December 2004 - -Copyright (C) 2004 Sam Hocevar - -Everyone is permitted to copy and distribute verbatim or modified -copies of this license document, and changing it is allowed as long -as the name is changed. - - DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. You just DO WHAT THE FUCK YOU WANT TO. diff --git a/changelog.md b/changelog.md index 437a3b2..9ad97c5 100644 --- a/changelog.md +++ b/changelog.md @@ -1,74 +1,14 @@ # Changelog Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to. -## 20240318 -### CXB -+ Fixing handle_data_shop_list_detail_request for Sunrise S1 - -## 20240302 -### SAO -+ Fixing new profile creation with right heroes and start VP -+ Fix to the Unanalyzed Log responses returning the wrong rewards -+ Documentation revised - -## 20240226 -### CXB -+ Fixing paths for rev.py -+ Changed encoding for handle_data_item_list_icon_request - -## 20240202 -### SAO -+ Added reader assets and edited the game specific documentation - -## 20240118 -### System -+ Added game version names to the readme - -## 20240109 -### System -+ Removed `ADD config config` from dockerfile [#83](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/83) (Thanks zaphkito!) - -### 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!) - +## 20231015 ### maimai DX -+ Fix user charges failing to save ++ Added support for FESTiVAL PLUS -### 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 +### Card Maker ++ Added support for maimai DX FESTiVAL PLUS +## 20231001 ### Initial D THE ARCADE + Added support for Initial D THE ARCADE S2 + Story mode progress added @@ -80,45 +20,6 @@ Documenting updates to ARTEMiS, to be updated every time the master branch is pu + 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) diff --git a/core/__init__.py b/core/__init__.py index 3194006..185d9bc 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,8 +1,6 @@ -# ruff: noqa: F401, I001, I002 -# Import ordering is important here! from core.config import CoreConfig -from core.allnet import AllnetServlet, BillingServlet -from core.aimedb import AimedbServlette +from core.allnet import AllnetServlet +from core.aimedb import AimedbFactory from core.title import TitleServlet from core.utils import Utils from core.mucha import MuchaServlet diff --git a/core/adb_handlers/__init__.py b/core/adb_handlers/__init__.py index bd019bc..0c96baf 100644 --- a/core/adb_handlers/__init__.py +++ b/core/adb_handlers/__init__.py @@ -1,29 +1,6 @@ -# ruff: noqa: F401 -from .base import ( - CMD_CODE_GOODBYE, - HEADER_SIZE, - ADBBaseRequest, - ADBBaseResponse, - ADBHeader, - ADBHeaderException, - ADBStatus, - CompanyCodes, - LogStatus, - PortalRegStatus, - ReaderFwVer, -) -from .campaign import ( - ADBCampaignClearRequest, - ADBCampaignClearResponse, - ADBCampaignResponse, - ADBOldCampaignRequest, - ADBOldCampaignResponse, -) -from .felica import ( - ADBFelicaLookup2Request, - ADBFelicaLookup2Response, - ADBFelicaLookupRequest, - ADBFelicaLookupResponse, -) -from .log import ADBLogExRequest, ADBLogExResponse, ADBLogRequest, ADBStatusLogRequest -from .lookup import ADBLookupExResponse, ADBLookupRequest, ADBLookupResponse +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 index 851c8c3..0f208dd 100644 --- a/core/adb_handlers/base.py +++ b/core/adb_handlers/base.py @@ -1,10 +1,8 @@ -import re import struct +from construct import Struct, Int16ul, Int32ul, PaddedString from enum import Enum -from typing import Final, Union - -from construct import Int16ul, Int32ul, PaddedString, Struct - +import re +from typing import Union, Final class LogStatus(Enum): NONE = 0 @@ -13,13 +11,11 @@ class LogStatus(Enum): END = 3 OTHER = 4 - class PortalRegStatus(Enum): NO_REG = 0 PORTAL = 1 SEGA_ID = 2 - class ADBStatus(Enum): UNKNOWN = 0 GOOD = 1 @@ -33,7 +29,6 @@ class ADBStatus(Enum): LOCK_SYS = 9 LOCK_USER = 10 - class CompanyCodes(Enum): NONE = 0 SEGA = 1 @@ -41,13 +36,12 @@ class CompanyCodes(Enum): KONAMI = 3 TAITO = 4 - -class ReaderFwVer(Enum): # Newer readers use a singly byte value +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" @@ -59,11 +53,11 @@ class ReaderFwVer(Enum): # Newer readers use a singly byte value 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") + i = int.from_bytes(byte, 'little') try: return ReaderFwVer(i) except ValueError: @@ -71,92 +65,64 @@ class ReaderFwVer(Enum): # Newer readers use a singly byte value 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 + 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 + 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 - + 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 + 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)}" - ) - + 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: + 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!" - ) + 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 - ): + 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: @@ -166,58 +132,33 @@ class ADBHeader: "response_code" / Int16ul, "length" / Int16ul, "status" / Int16ul, - "game_id" / PaddedString(6, "utf_8"), + "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, - ) + "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 - ) + 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 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""" diff --git a/core/adb_handlers/campaign.py b/core/adb_handlers/campaign.py index 53e68dd..a1a372e 100644 --- a/core/adb_handlers/campaign.py +++ b/core/adb_handlers/campaign.py @@ -1,8 +1,7 @@ -from construct import Bytes, Int32sl, Int32ul, Padding, Struct +from construct import Struct, Int16ul, Padding, Bytes, Int32ul, Int32sl from .base import * - class Campaign: def __init__(self) -> None: self.id = 0 @@ -12,7 +11,7 @@ class Campaign: 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( @@ -24,53 +23,39 @@ class Campaign: "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, - ) - ) - + ).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, - ) - ) - + ).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: + 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) @@ -79,42 +64,32 @@ class ADBCampaignResponse(ADBBaseResponse): 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: + def __init__(self, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x0C, length: int = 0x30, status: int = 1) -> 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, @@ -122,46 +97,36 @@ class ADBOldCampaignResponse(ADBBaseResponse): "info2" / Int32sl, "info3" / Int32sl, ).build( - info0=self.info0, - info1=self.info1, - info2=self.info2, - info3=self.info3, + 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: + def __init__(self, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x0E, length: int = 0x50, status: int = 1) -> 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 index df341eb..479e84d 100644 --- a/core/adb_handlers/felica.py +++ b/core/adb_handlers/felica.py @@ -1,10 +1,7 @@ +from construct import Struct, Int32sl, Padding, Int8ub, Int16sl from typing import Union - -from construct import Int8ub, Int32sl, Padding, Struct - from .base import * - class ADBFelicaLookupRequest(ADBBaseRequest): def __init__(self, data: bytes) -> None: super().__init__(data) @@ -12,88 +9,58 @@ class ADBFelicaLookupRequest(ADBBaseRequest): 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: + 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" - ) - + 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": + 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: + + 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))) + "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.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.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: + 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.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": + 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: + def make(self) -> bytes: resp_struct = Struct( "user_id" / Int32sl, "relation1" / Int32sl, @@ -103,17 +70,15 @@ class ADBFelicaLookup2Response(ADBBaseResponse): "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, - ) - ) + ).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) diff --git a/core/adb_handlers/log.py b/core/adb_handlers/log.py index 65eb013..28fbdf3 100644 --- a/core/adb_handlers/log.py +++ b/core/adb_handlers/log.py @@ -1,82 +1,56 @@ +from construct import Struct, Padding, Int8sl from typing import Final, List -from construct import Int8sl, Padding, Struct - 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.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 - ) + def __init__(self, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", protocol_ver: int = 12423, code: int = 20, length: int = 64, status: int = 1) -> 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)) + resp_struct = Struct( + "log_result" / Int8sl[NUM_LOGS], + Padding(12) + ) - body = resp_struct.build(dict(log_result=[1] * NUM_LOGS)) + 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 index 2830bac..0640493 100644 --- a/core/adb_handlers/lookup.py +++ b/core/adb_handlers/lookup.py @@ -1,41 +1,26 @@ +from construct import Struct, Int32sl, Padding, Int8sl from typing import Union -from construct import Int8sl, Int32sl, Padding, Struct - 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: + def __init__(self, user_id: Union[int, None], game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x06, length: int = 0x30, 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 @@ -47,36 +32,30 @@ class ADBLookupResponse(ADBBaseResponse): 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) + 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: + 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": + 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 @@ -91,15 +70,13 @@ class ADBLookupExResponse(ADBBaseResponse): "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, - ) - ) + 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 dc24fa4..e65c2c7 100644 --- a/core/aimedb.py +++ b/core/aimedb.py @@ -1,210 +1,134 @@ -import asyncio -import logging -from logging.handlers import TimedRotatingFileHandler -from typing import Callable, Dict, Optional, Tuple, Union - -import coloredlogs +from twisted.internet.protocol import Factory, Protocol +import logging, coloredlogs from Crypto.Cipher import AES +import struct +from typing import Dict, Tuple, Callable, Union +from typing_extensions import Final +from logging.handlers import TimedRotatingFileHandler from core.config import CoreConfig -from core.data import Data from core.utils import create_sega_auth_key - +from core.data import Data from .adb_handlers import * -class AimedbServlette: - request_list: Dict[ - int, Tuple[Callable[[bytes, int], Union[ADBBaseResponse, bytes]], int, str] - ] = {} +class AimedbProtocol(Protocol): + 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") self.config = core_cfg self.data = Data(core_cfg) - - self.logger = logging.getLogger("aimedb") - if not hasattr(self.logger, "initted"): - log_fmt_str = "[%(asctime)s] Aimedb | %(levelname)s | %(message)s" - log_fmt = logging.Formatter(log_fmt_str) - - 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=core_cfg.aimedb.loglevel, logger=self.logger, fmt=log_fmt_str - ) - self.logger.initted = True - - if not core_cfg.aimedb.key: + if core_cfg.aimedb.key == "": self.logger.error("!!!KEY NOT SET!!!") exit(1) - 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(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(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(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(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(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") - - def register_handler( - self, - cmd: int, - resp: int, - handler: Callable[[bytes, int], Union[ADBBaseResponse, bytes]], - name: str, - ) -> None: + 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 start(self) -> None: - self.logger.info(f"Start on port {self.config.aimedb.port}") - addr = ( - self.config.aimedb.listen_address - if self.config.aimedb.listen_address - else self.config.server.listen_address - ) - asyncio.create_task( - asyncio.start_server(self.dataReceived, addr, self.config.aimedb.port) - ) + 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") + + def connectionLost(self, reason) -> None: self.logger.debug( - f"Connection made from {writer.get_extra_info('peername')[0]}" + f"{self.transport.getPeer().host} Disconnected - {reason.value}" ) - while True: - try: - data: bytes = await reader.read(4096) - if len(data) == 0: - self.logger.debug("Connection closed") - return - await self.process_data(data, reader, writer) - await writer.drain() - except ConnectionResetError as e: - self.logger.debug("Connection reset, disconnecting") - return - async def process_data( - self, data: bytes, reader: asyncio.StreamReader, writer: asyncio.StreamWriter - ) -> Optional[bytes]: - addr = writer.get_extra_info("peername")[0] + def dataReceived(self, data: bytes) -> None: cipher = AES.new(self.config.aimedb.key.encode(), AES.MODE_ECB) try: decrypted = cipher.decrypt(data) - + except Exception as e: self.logger.error(f"Failed to decrypt {data.hex()} because {e}") - return + return None - self.logger.debug(f"{addr} wrote {decrypted.hex()}") + self.logger.debug(f"{self.transport.getPeer().host} wrote {decrypted.hex()}") try: head = ADBHeader.from_data(decrypted) - + except ADBHeaderException as e: self.logger.error(f"Error parsing ADB header: {e}") - try: + try: encrypted = cipher.encrypt(ADBBaseResponse().make()) - writer.write(encrypted) - await writer.drain() - return + self.transport.write(encrypted) except Exception as e: self.logger.error(f"Failed to encrypt default response because {e}") - + return - if head.keychip_id == "ABCD1234567" or head.store_id == 0xFFF0: + if head.keychip_id == "ABCD1234567" or head.store_id == 0xfff0: self.logger.warning(f"Request from uninitialized AMLib: {vars(head)}") - if head.cmd == 0x66: - self.logger.info("Goodbye") - writer.close() - return - - handler, resp_code, name = self.request_list.get( - head.cmd, (self.handle_default, None, "default") - ) + handler, resp_code, name = self.request_list.get(head.cmd, (self.handle_default, None, 'default')) if resp_code is None: self.logger.warning(f"No handler for cmd {hex(head.cmd)}") - + elif resp_code > 0: - self.logger.info(f"{name} from {head.keychip_id} ({head.game_id}) @ {addr}") - - resp = await handler(decrypted, resp_code) + self.logger.info(f"{name} from {head.keychip_id} ({head.game_id}) @ {self.transport.getPeer().host}") + + resp = handler(decrypted, resp_code) 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) elif type(resp) == bytes: resp_bytes = resp - - elif resp is None: # Nothing to send, probably a goodbye - self.logger.warn(f"None return by handler for {name}") + + elif resp is None: # Nothing to send, probably a goodbye return - + else: - self.logger.error( - f"Unsupported type returned by ADB handler for {name}: {type(resp)}" - ) - raise TypeError( - f"Unsupported type returned by ADB handler for {name}: {type(resp)}" - ) + raise TypeError(f"Unsupported type returned by ADB handler for {name}: {type(resp)}") - try: + try: encrypted = cipher.encrypt(resp_bytes) self.logger.debug(f"Response {resp_bytes.hex()}") - writer.write(encrypted) + self.transport.write(encrypted) except Exception as e: self.logger.error(f"Failed to encrypt {resp_bytes.hex()} because {e}") - - async def handle_default( - self, data: bytes, resp_code: int, length: int = 0x20 - ) -> ADBBaseResponse: + + 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, - ) + return ADBBaseResponse(resp_code, length, 1, req.game_id, req.store_id, req.keychip_id, req.protocol_ver) - async def handle_hello(self, data: bytes, resp_code: int) -> ADBBaseResponse: - return await self.handle_default(data, resp_code) + def handle_hello(self, data: bytes, resp_code: int) -> ADBBaseResponse: + return self.handle_default(data, resp_code) - async def handle_campaign(self, data: bytes, resp_code: int) -> ADBBaseResponse: + def handle_campaign(self, data: bytes, resp_code: int) -> ADBBaseResponse: h = ADBHeader.from_data(data) if h.protocol_ver >= 0x3030: req = h @@ -212,41 +136,38 @@ class AimedbServlette: else: req = ADBOldCampaignRequest(data) - - self.logger.info( - f"Legacy campaign request for campaign {req.campaign_id} (protocol version {hex(h.protocol_ver)})" - ) + + 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 - async def handle_lookup(self, data: bytes, resp_code: int) -> ADBBaseResponse: + def handle_lookup(self, data: bytes, resp_code: int) -> ADBBaseResponse: req = ADBLookupRequest(data) - user_id = await self.data.card.get_user_id_from_card(req.access_code) - is_banned = await self.data.card.get_card_banned(req.access_code) - is_locked = await self.data.card.get_card_locked(req.access_code) + 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}") - - if user_id and user_id > 0: - await self.data.card.update_card_last_login(req.access_code) + ret = ADBLookupResponse.from_req(req.head, user_id) + + self.logger.info( + f"access_code {req.access_code} -> user_id {ret.user_id}" + ) return ret - async def handle_lookup_ex(self, data: bytes, resp_code: int) -> ADBBaseResponse: + def handle_lookup_ex(self, data: bytes, resp_code: int) -> ADBBaseResponse: req = ADBLookupRequest(data) - user_id = await self.data.card.get_user_id_from_card(req.access_code) + user_id = self.data.card.get_user_id_from_card(req.access_code) - is_banned = await self.data.card.get_card_banned(req.access_code) - is_locked = await self.data.card.get_card_locked(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: @@ -256,30 +177,23 @@ class AimedbServlette: elif is_locked: ret.head.status = ADBStatus.LOCK_USER - self.logger.info(f"access_code {req.access_code} -> user_id {ret.user_id}") + 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, - ) + 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 - if user_id and user_id > 0: - await self.data.card.update_card_last_login(req.access_code) return ret - async def handle_felica_lookup(self, data: bytes, resp_code: int) -> bytes: + 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 + 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 @@ -288,25 +202,27 @@ class AimedbServlette: """ 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}") + self.logger.info( + f"idm {req.idm} ipm {req.pmm} -> access_code {ac}" + ) return ADBFelicaLookupResponse.from_req(req.head, ac) - async def handle_felica_register(self, data: bytes, resp_code: int) -> bytes: + 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 = await self.data.user.create_user() + 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 = await self.data.card.create_card(user_id, ac) + card_id = self.data.card.create_card(user_id, ac) if card_id is None: self.logger.error("Failed to register card!") @@ -321,14 +237,12 @@ class AimedbServlette: f"Registration blocked!: access code {ac} (IDm: {req.idm} PMm: {req.pmm})" ) - if user_id > 0: - await self.data.card.update_card_last_login(ac) return ADBFelicaLookupResponse.from_req(req.head, ac) - async def handle_felica_lookup_ex(self, data: bytes, resp_code: int) -> bytes: + def handle_felica_lookup_ex(self, data: bytes, resp_code: int) -> bytes: req = ADBFelicaLookup2Request(data) access_code = self.data.card.to_access_code(req.idm) - user_id = await self.data.card.get_user_id_from_card(access_code=access_code) + user_id = self.data.card.get_user_id_from_card(access_code=access_code) if user_id is None: user_id = -1 @@ -340,27 +254,16 @@ class AimedbServlette: resp = ADBFelicaLookup2Response.from_req(req.head, user_id, access_code) 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, - ) + 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 - if user_id and user_id > 0: - await self.data.card.update_card_last_login(access_code) return resp - async def handle_campaign_clear( - self, data: bytes, resp_code: int - ) -> ADBBaseResponse: + def handle_campaign_clear(self, data: bytes, resp_code: int) -> ADBBaseResponse: req = ADBCampaignClearRequest(data) resp = ADBCampaignClearResponse.from_req(req.head) @@ -368,19 +271,19 @@ class AimedbServlette: # We don't support campaign stuff return resp - async def handle_register(self, data: bytes, resp_code: int) -> bytes: + 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 = await self.data.user.create_user() + 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 = await self.data.card.create_card(user_id, req.access_code) + card_id = self.data.card.create_card(user_id, req.access_code) if card_id is None: self.logger.error("Failed to register card!") @@ -391,55 +294,73 @@ class AimedbServlette: ) else: - self.logger.info(f"Registration blocked!: access code {req.access_code}") + 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" - - else: - await self.data.card.update_card_last_login(req.access_code) + 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 - async def handle_status_log(self, data: bytes, resp_code: int) -> bytes: + 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, - ) + return ADBBaseResponse(resp_code, 0x20, 1, req.head.game_id, req.head.store_id, req.head.keychip_id, req.head.protocol_ver) - async def handle_log(self, data: bytes, resp_code: int) -> bytes: + 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, - ) + 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) - async def handle_log_ex(self, data: bytes, resp_code: int) -> bytes: + 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}" - ) + 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.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 + ) + + 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 6883e00..e83aae0 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -1,47 +1,39 @@ -import base64 -import json -import logging -import math -import urllib.parse -import zlib -from datetime import datetime -from enum import Enum +from typing import Dict, List, Any, Optional, Tuple, Union, Final +import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler -from os import W_OK, access, environ, mkdir, path -from typing import Any, Dict, Final, List, Optional, Union - -import coloredlogs +from twisted.web.http import Request +from datetime import datetime import pytz -import yaml -from Crypto.Hash import SHA +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 starlette.applications import Starlette -from starlette.requests import Request -from starlette.responses import PlainTextResponse -from starlette.routing import Route +from time import strptime +from os import path +import urllib.parse +import math from .config import CoreConfig -from .const import * -from .data import Data -from .title import TitleServlet 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 @@ -88,7 +80,7 @@ class DLI_STATUS(Enum): 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 + 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": @@ -97,11 +89,9 @@ class DLI_STATUS(Enum): except ValueError: return cls.UNKNOWN - class AllnetServlet: - allnet_registry: Dict[str, Any] = {} - 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) @@ -130,24 +120,25 @@ class AllnetServlet: ) self.logger.initialized = True - def startup(self) -> None: + plugins = Utils.get_all_titles() + + if len(plugins) == 0: + self.logger.error("No games detected!") + self.logger.info( - f"Ready on port {self.config.allnet.port if self.config.allnet.standalone else self.config.server.port}" + f"Serving {len(TitleServlet.title_registry)} game codes port {core_cfg.allnet.port}" ) - if not TitleServlet.title_registry: - TitleServlet(self.config, self.config_folder) - async def handle_poweron(self, request: Request): + def handle_poweron(self, request: Request, _: Dict): request_ip = Utils.get_ip_addr(request) - pragma_header = request.headers.get("Pragma", "") + pragma_header = request.getHeader('Pragma') is_dfi = pragma_header is not None and pragma_header == "DFI" - data = await request.body() - + try: if is_dfi: - req_urlencode = self.from_dfi(data) + req_urlencode = self.from_dfi(request.content.getvalue()) else: - req_urlencode = data + req_urlencode = request.content.getvalue().decode() req_dict = self.allnet_req_to_dict(req_urlencode) if req_dict is None: @@ -156,14 +147,7 @@ class AllnetServlet: 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.serial - or not req.ip - or not req.firm_ver - or not req.boot_ver - ): + 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)}" ) @@ -171,7 +155,7 @@ class AllnetServlet: except AllnetRequestException as e: if e.message != "": self.logger.error(e) - return PlainTextResponse() + return b"" if req.format_ver == 3: resp = AllnetPowerOnResponse3(req.token) @@ -182,50 +166,43 @@ class AllnetServlet: self.logger.debug(f"Allnet request: {vars(req)}") - machine = await 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}." - await self.data.base.log_event( + 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 PlainTextResponse( - urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n" - ) + return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8") if machine is not None: - arcade = await self.data.arcade.get_arcade(machine["arcade"]) + arcade = self.data.arcade.get_arcade(machine["arcade"]) if self.config.server.check_arcade_ip: if arcade["ip"] and arcade["ip"] is not None and arcade["ip"] != req.ip: msg = f"Serial {req.serial} attempted allnet auth from bad IP {req.ip} (expected {arcade['ip']})." - await self.data.base.log_event( + 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 PlainTextResponse( - urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n" - ) - - elif ( - not arcade["ip"] or arcade["ip"] is None - ) and self.config.server.strict_ip_checking: + 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)." - await self.data.base.log_event( + 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 PlainTextResponse( - urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n" - ) + return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8") + country = ( arcade["country"] if machine["country"] is None else machine["country"] @@ -234,7 +211,7 @@ class AllnetServlet: country = AllnetCountryCode.JAPAN.value resp.country = country - resp.place_id = f"{arcade['id']:04X}" + 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 "" @@ -254,82 +231,64 @@ class AllnetServlet: 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" + 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}." - await self.data.base.log_event( + 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 PlainTextResponse( - urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n" - ) + 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.server.hostname}:{self.config.server.port}/{req.game_id}/{req.ver.replace('.', '')}/" - resp.host = f"{self.config.server.hostname}:{self.config.server.port}" - + 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 PlainTextResponse(resp_str + "\n") + return (resp_str + "\n").encode("utf-8") + int_ver = req.ver.replace(".", "") - try: - resp.uri, resp.host = TitleServlet.title_registry[ - req.game_id - ].get_allnet_info(req.game_id, int(int_ver), req.serial) - except Exception as e: - self.logger.error(f"Error running get_allnet_info for {req.game_id} - {e}") - resp.stat = ALLNET_STAT.bad_game.value - resp_dict = {k: v for k, v in vars(resp).items() if v is not None} - return PlainTextResponse( - urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n" - ) + 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}" - await self.data.base.log_event( - "allnet", "ALLNET_AUTH_SUCCESS", logging.INFO, msg - ) + self.data.base.log_event("allnet", "ALLNET_AUTH_SUCCESS", logging.INFO, msg) self.logger.info(msg) 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}") + self.logger.debug(f"Allnet response: {resp_dict}") resp_str += "\n" """if is_dfi: request.responseHeaders.addRawHeader('Pragma', 'DFI') return self.to_dfi(resp_str)""" - return PlainTextResponse(resp_str) + return resp_str.encode("utf-8") - async def handle_dlorder(self, request: Request): + def handle_dlorder(self, request: Request, _: Dict): request_ip = Utils.get_ip_addr(request) - pragma_header = request.headers.get("Pragma", "") + pragma_header = request.getHeader('Pragma') is_dfi = pragma_header is not None and pragma_header == "DFI" - data = await request.body() - + try: if is_dfi: - req_urlencode = self.from_dfi(data) + req_urlencode = self.from_dfi(request.content.getvalue()) else: - req_urlencode = data.decode() + req_urlencode = request.content.getvalue().decode() req_dict = self.allnet_req_to_dict(req_urlencode) if req_dict is None: @@ -346,7 +305,7 @@ class AllnetServlet: except AllnetRequestException as e: if e.message != "": self.logger.error(e) - return PlainTextResponse() + return b"" self.logger.info( f"DownloadOrder from {request_ip} -> {req.game_id} v{req.ver} serial {req.serial}" @@ -357,112 +316,84 @@ class AllnetServlet: not self.config.allnet.allow_online_updates or not self.config.allnet.update_cfg_folder ): - return PlainTextResponse( - urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n" - ) + 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.server.hostname}:{self.config.server.port}/dl/ini/{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.server.hostname}:{self.config.server.port}/dl/ini/{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}") - await self.data.base.log_event( - "allnet", - "DLORDER_REQ_SUCCESS", - logging.INFO, - f"{Utils.get_ip_addr(request)} requested DL Order for {req.serial} {req.game_id} v{req.ver}", - ) + 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 PlainTextResponse(res_str) + return res_str - async def handle_dlorder_ini(self, request: Request) -> bytes: - req_file = ( - request.path_params.get("file", "").replace("%0A", "").replace("\n", "") - ) + def handle_dlorder_ini(self, request: Request, match: Dict) -> bytes: + if "file" not in match: + return b"" - if not req_file: - return PlainTextResponse(status_code=404) + 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" - ) - await self.data.base.log_event( - "allnet", - "DLORDER_INI_SENT", - logging.INFO, - f"{Utils.get_ip_addr(request)} successfully recieved {req_file}", - ) - - return PlainTextResponse( - open( - f"{self.config.allnet.update_cfg_folder}/{req_file}", - "r", - encoding="utf-8", - ).read() - ) + 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 PlainTextResponse() + return b"" - async def handle_dlorder_report(self, request: Request) -> bytes: - req_raw = await request.body() + 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 PlainTextResponse("NG") - + 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 PlainTextResponse("NG") + 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 PlainTextResponse("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." - ) - - await self.data.base.log_event( - "allnet", "DL_REPORT", logging.INFO, msg, dl_data - ) + 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 PlainTextResponse("OK") + return "OK" - async def handle_loaderstaterecorder(self, request: Request) -> bytes: - req_data = await request.body() + 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) @@ -471,25 +402,131 @@ class AllnetServlet: 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 PlainTextResponse("NG") + 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})" + 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.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() + + 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}") + + 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 {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) + + return f"result=1&requestno={req.requestno}&message=Keychip Serial bad\r\n".encode() + + 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}" ) - return PlainTextResponse("OK") + self.logger.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 - async def handle_alive(self, request: Request) -> bytes: - return PlainTextResponse("OK") + while req.playcnt > req.playlimit: + kc_playlimit += 1024 + kc_nearfull += 1024 - async def handle_naomitest(self, request: Request) -> 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) + nearfull_sig = signer.sign(digest).hex() + + # TODO: playhistory + + #resp = BillingResponse(playlimit, playlimit_sig, nearfull, nearfull_sig) + resp = BillingResponse(playlimit, playlimit_sig, nearfull, nearfull_sig, req.requestno, req.protocolver) + + 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 handle_naomitest(self, request: Request, _: Dict) -> bytes: self.logger.info(f"Ping from {Utils.get_ip_addr(request)}") - return PlainTextResponse("naomi ok") + return b"naomi ok" + + def billing_req_to_dict(self, data: bytes): + """ + Parses an billing request string into a python dictionary + """ + try: + 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: + self.logger.error(f"billing_req_to_dict: {e} while parsing {data}") + return None def allnet_req_to_dict(self, data: str) -> Optional[List[Dict[str, Any]]]: """ @@ -513,171 +550,11 @@ class AllnetServlet: return unzipped.decode("utf-8") def to_dfi(self, data: str) -> bytes: - unzipped = data.encode("utf-8") + unzipped = data.encode('utf-8') zipped = zlib.compress(unzipped) return base64.b64encode(zipped) -class BillingServlet: - def __init__(self, core_cfg: CoreConfig, cfg_folder: str) -> None: - self.config = core_cfg - self.config_folder = cfg_folder - self.data = Data(core_cfg) - - self.logger = logging.getLogger("billing") - if not hasattr(self.logger, "initialized"): - log_fmt_str = "[%(asctime)s] Billing | %(levelname)s | %(message)s" - log_fmt = logging.Formatter(log_fmt_str) - - fileHandler = TimedRotatingFileHandler( - "{0}/{1}.log".format(self.config.server.log_dir, "billing"), - 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.billing.loglevel, logger=self.logger, fmt=log_fmt_str - ) - self.logger.initialized = True - - def startup(self) -> None: - self.logger.info( - f"Ready on port {self.config.billing.port if self.config.billing.standalone else self.config.server.port}" - ) - - def billing_req_to_dict(self, data: bytes): - """ - Parses an billing request string into a python dictionary - """ - try: - 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: - self.logger.error(f"billing_req_to_dict: {e} while parsing {data}") - return None - - async def handle_billing_request(self, request: Request): - req_raw = await request.body() - - if request.headers.get("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 {req_raw}") - return PlainTextResponse() - - self.logger.debug(f"request {req_dict}") - - 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 PlainTextResponse( - "result=5&linelimit=&message=field is missing or formatting is incorrect\r\n" - ) - - 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}") - - kc_serial_bytes = req.keychipid.encode() - - machine = await self.data.arcade.get_machine(req.keychipid) - if machine is None and not self.config.server.allow_unregistered_serials: - msg = f"Unrecognised serial {req.keychipid} attempted billing checkin from {request_ip} for {req.gameid} v{req.gamever}." - await self.data.base.log_event( - "allnet", "BILLING_CHECKIN_NG_SERIAL", logging.WARN, msg - ) - self.logger.warning(msg) - - return PlainTextResponse( - f"result=1&requestno={req.requestno}&message=Keychip Serial bad\r\n" - ) - - msg = ( - f"Billing checkin from {request_ip}: game {req.gameid} ver {req.gamever} keychip {req.keychipid} playcount " - f"{req.playcnt} billing_type {req.billingtype.name} nearfull {req.nearfull} playlimit {req.playlimit}" - ) - self.logger.info(msg) - await self.data.base.log_event( - "billing", "BILLING_CHECKIN_OK", logging.INFO, msg - ) - if req.traceleft > 0: - self.logger.warn(f"{req.traceleft} unsent tracelogs") - kc_playlimit = req.playlimit - kc_nearfull = req.nearfull - - while req.playcnt > req.playlimit: - kc_playlimit += 1024 - kc_nearfull += 1024 - - 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) - nearfull_sig = signer.sign(digest).hex() - - # TODO: playhistory - - resp = BillingResponse( - playlimit, - playlimit_sig, - nearfull, - nearfull_sig, - req.requestno, - req.protocolver, - ) - - 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 PlainTextResponse("result=6&waittime=0&linelimit=20\r\n") - - return PlainTextResponse(resp_str) - - class AllnetPowerOnRequest: def __init__(self, req: Dict) -> None: if req is None: @@ -693,7 +570,6 @@ class AllnetPowerOnRequest: self.format_ver = float(req.get("format_ver", "1.00")) self.token: str = req.get("token", "0") - class AllnetPowerOnResponse: def __init__(self) -> None: self.stat = 1 @@ -706,7 +582,7 @@ class AllnetPowerOnResponse: self.region_name0 = "W" self.region_name1 = "" self.region_name2 = "" - self.region_name3 = "" + self.region_name3 = "" self.setting = "1" self.year = datetime.now().year self.month = datetime.now().month @@ -715,7 +591,6 @@ class AllnetPowerOnResponse: self.minute = datetime.now().minute self.second = datetime.now().second - class AllnetPowerOnResponse3(AllnetPowerOnResponse): def __init__(self, token) -> None: super().__init__() @@ -763,31 +638,27 @@ class AllnetDownloadOrderResponse: self.serial = serial self.uri = uri - class TraceDataType(Enum): CHARGE = 0 EVENT = 1 CREDIT = 2 - 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") - + 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: @@ -815,7 +686,6 @@ class BillingInfo: except Exception as e: raise KeyError(e) - class TraceData: def __init__(self, data: Dict) -> None: try: @@ -824,26 +694,25 @@ class TraceData: 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... + 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_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)) @@ -856,7 +725,6 @@ class TraceDataCharge(TraceData): except Exception as e: raise KeyError(e) - class TraceDataEvent(TraceData): def __init__(self, data: Dict) -> None: super().__init__(data) @@ -865,7 +733,6 @@ class TraceDataEvent(TraceData): except Exception as e: raise KeyError(e) - class TraceDataCredit(TraceData): def __init__(self, data: Dict) -> None: super().__init__(data) @@ -888,7 +755,6 @@ class TraceDataCredit(TraceData): except Exception as e: raise KeyError(e) - class BillingResponse: def __init__( self, @@ -921,7 +787,6 @@ class AllnetRequestException(Exception): self.message = message super().__init__(self.message) - class DLReport: def __init__(self, data: Dict, report_type: DLIMG_TYPE) -> None: self.serial = data.get("serial") @@ -936,97 +801,51 @@ class DLReport: 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.wdav = data.get("wdav") # app only self.dov = data.get("dov") - self.wdov = data.get("wdov") # app only + self.wdov = data.get("wdov") # app only self.rep_type = report_type self.err = "" - + def validate(self) -> bool: - if self.serial is None: + 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: + + 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 - - -cfg_dir = environ.get("DIANA_CFG_DIR", "config") -cfg: CoreConfig = CoreConfig() -if path.exists(f"{cfg_dir}/core.yaml"): - cfg.update(yaml.safe_load(open(f"{cfg_dir}/core.yaml"))) - -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) - -billing = BillingServlet(cfg, cfg_dir) -app_billing = Starlette( - cfg.server.is_develop, - [ - Route("/request", billing.handle_billing_request, methods=["POST"]), - Route("/request/", billing.handle_billing_request, methods=["POST"]), - ], - on_startup=[billing.startup], -) - -allnet = AllnetServlet(cfg, cfg_dir) -route_lst = [ - Route("/sys/servlet/PowerOn", allnet.handle_poweron, methods=["GET", "POST"]), - Route("/sys/servlet/DownloadOrder", allnet.handle_dlorder, methods=["GET", "POST"]), - Route( - "/sys/servlet/LoaderStateRecorder", - allnet.handle_loaderstaterecorder, - methods=["GET", "POST"], - ), - Route("/sys/servlet/Alive", allnet.handle_alive, methods=["GET", "POST"]), - Route("/naomitest.html", allnet.handle_naomitest), -] - -if cfg.allnet.allow_online_updates: - route_lst += [ - Route("/report-api/Report", allnet.handle_dlorder_report, methods=["POST"]), - Route("/dl/ini/{file:str}", allnet.handle_dlorder_ini), - ] - -app_allnet = Starlette(cfg.server.is_develop, route_lst, on_startup=[allnet.startup]) diff --git a/core/app.py b/core/app.py deleted file mode 100644 index b0911f2..0000000 --- a/core/app.py +++ /dev/null @@ -1,107 +0,0 @@ -import logging -from logging.handlers import TimedRotatingFileHandler -from os import W_OK, access, environ, mkdir, path -from typing import List - -import coloredlogs -import yaml -from starlette.applications import Starlette -from starlette.requests import Request -from starlette.responses import PlainTextResponse -from starlette.routing import Route - -from core import ( - AllnetServlet, - BillingServlet, - CoreConfig, - MuchaServlet, - TitleServlet, -) -from core.frontend import FrontendServlet - - -async def dummy_rt(request: Request): - return PlainTextResponse("Service OK") - - -cfg_dir = environ.get("ARTEMIS_CFG_DIR", "config") -cfg: CoreConfig = CoreConfig() -if path.exists(f"{cfg_dir}/core.yaml"): - cfg.update(yaml.safe_load(open(f"{cfg_dir}/core.yaml"))) - -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) - -logger = logging.getLogger("core") -log_fmt_str = "[%(asctime)s] Core | %(levelname)s | %(message)s" -log_fmt = logging.Formatter(log_fmt_str) - -fileHandler = TimedRotatingFileHandler( - "{0}/{1}.log".format(cfg.server.log_dir, "core"), when="d", backupCount=10 -) -fileHandler.setFormatter(log_fmt) - -consoleHandler = logging.StreamHandler() -consoleHandler.setFormatter(log_fmt) - -logger.addHandler(fileHandler) -logger.addHandler(consoleHandler) - -log_lv = logging.DEBUG if cfg.server.is_develop else logging.INFO -logger.setLevel(log_lv) -coloredlogs.install(level=log_lv, logger=logger, fmt=log_fmt_str) - -logger.info( - f"Artemis starting in {'develop' if cfg.server.is_develop else 'production'} mode" -) - -title = TitleServlet(cfg, cfg_dir) # This has to be loaded first to load plugins -mucha = MuchaServlet(cfg, cfg_dir) - -route_lst: List[Route] = [ - # Mucha - Route("/mucha_front/boardauth.do", mucha.handle_boardauth, methods=["POST"]), - Route("/mucha_front/updatacheck.do", mucha.handle_updatecheck, methods=["POST"]), - Route("/mucha_front/downloadstate.do", mucha.handle_dlstate, methods=["POST"]), - # General - Route("/", dummy_rt), - Route("/robots.txt", FrontendServlet.robots), -] - -if not cfg.billing.standalone: - billing = BillingServlet(cfg, cfg_dir) - route_lst += [ - Route("/request", billing.handle_billing_request, methods=["POST"]), - Route("/request/", billing.handle_billing_request, methods=["POST"]), - ] - -if not cfg.allnet.standalone: - allnet = AllnetServlet(cfg, cfg_dir) - route_lst += [ - Route("/sys/servlet/PowerOn", allnet.handle_poweron, methods=["GET", "POST"]), - Route( - "/sys/servlet/DownloadOrder", allnet.handle_dlorder, methods=["GET", "POST"] - ), - Route( - "/sys/servlet/LoaderStateRecorder", - allnet.handle_loaderstaterecorder, - methods=["GET", "POST"], - ), - Route("/sys/servlet/Alive", allnet.handle_alive, methods=["GET", "POST"]), - Route("/naomitest.html", allnet.handle_naomitest), - ] - - if cfg.allnet.allow_online_updates: - route_lst += [ - Route("/report-api/Report", allnet.handle_dlorder_report, methods=["POST"]), - Route("/dl/ini/{file:str}", allnet.handle_dlorder_ini), - ] - -for code, game in title.title_registry.items(): - route_lst += game.get_routes() - -app = Starlette(cfg.server.is_develop, route_lst) diff --git a/core/config.py b/core/config.py index 4802e30..68db052 100644 --- a/core/config.py +++ b/core/config.py @@ -1,5 +1,4 @@ -import logging -import os +import logging, os from typing import Any @@ -9,43 +8,10 @@ class ServerConfig: @property def listen_address(self) -> str: - """ - Address Artemis will bind to and listen on - """ return CoreConfig.get_config_field( self.__config, "core", "server", "listen_address", default="127.0.0.1" ) - @property - def hostname(self) -> str: - """ - Hostname sent to games - """ - return CoreConfig.get_config_field( - self.__config, "core", "server", "hostname", default="localhost" - ) - - @property - def port(self) -> int: - """ - Port the game will listen on - """ - return CoreConfig.get_config_field( - self.__config, "core", "server", "port", default=80 - ) - - @property - def ssl_key(self) -> str: - return CoreConfig.get_config_field( - self.__config, "core", "server", "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 allow_user_registration(self) -> bool: return CoreConfig.get_config_field( @@ -77,23 +43,9 @@ class ServerConfig: ) @property - def proxy_port(self) -> int: - """ - What port the proxy is listening on. This will be sent instead of 'port' if - is_using_proxy is True and this value is non-zero - """ + def threading(self) -> bool: return CoreConfig.get_config_field( - self.__config, "core", "server", "proxy_port", default=0 - ) - - @property - def proxy_port_ssl(self) -> int: - """ - What port the proxy is listening for secure connections on. This will be sent - instead of 'port' if is_using_proxy is True and this value is non-zero - """ - return CoreConfig.get_config_field( - self.__config, "core", "server", "proxy_port_ssl", default=0 + self.__config, "core", "server", "threading", default=False ) @property @@ -127,6 +79,36 @@ class TitleConfig: ) ) + @property + def hostname(self) -> str: + 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 + ) + + @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( @@ -177,7 +159,7 @@ class DatabaseConfig: @property def protocol(self) -> str: return CoreConfig.get_config_field( - self.__config, "core", "database", "protocol", default="mysql" + self.__config, "core", "database", "type", default="mysql" ) @property @@ -194,6 +176,16 @@ class DatabaseConfig: ) ) + @property + def user_table_autoincrement_start(self) -> int: + 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( @@ -212,7 +204,7 @@ class FrontendConfig: self.__config = parent_config @property - def enable(self) -> bool: + def enable(self) -> int: return CoreConfig.get_config_field( self.__config, "core", "frontend", "enable", default=False ) @@ -220,7 +212,7 @@ class FrontendConfig: @property def port(self) -> int: return CoreConfig.get_config_field( - self.__config, "core", "frontend", "port", default=8080 + self.__config, "core", "frontend", "port", default=8090 ) @property @@ -231,21 +223,17 @@ class FrontendConfig: ) ) - @property - def secret(self) -> str: - return CoreConfig.get_config_field( - self.__config, "core", "frontend", "secret", default="" - ) - class AllnetConfig: def __init__(self, parent_config: "CoreConfig") -> None: self.__config = parent_config @property - def standalone(self) -> bool: - return CoreConfig.get_config_field( - self.__config, "core", "allnet", "standalone", default=False + def loglevel(self) -> int: + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "core", "allnet", "loglevel", default="info" + ) ) @property @@ -255,11 +243,9 @@ class AllnetConfig: ) @property - def loglevel(self) -> int: - return CoreConfig.str_to_loglevel( - CoreConfig.get_config_field( - self.__config, "core", "allnet", "loglevel", default="info" - ) + def ip_check(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "core", "allnet", "ip_check", default=False ) @property @@ -279,20 +265,6 @@ class BillingConfig: def __init__(self, parent_config: "CoreConfig") -> None: self.__config = parent_config - @property - def standalone(self) -> bool: - return CoreConfig.get_config_field( - self.__config, "core", "billing", "standalone", default=True - ) - - @property - def loglevel(self) -> int: - return CoreConfig.str_to_loglevel( - CoreConfig.get_config_field( - self.__config, "core", "billing", "loglevel", default="info" - ) - ) - @property def port(self) -> int: return CoreConfig.get_config_field( @@ -322,18 +294,6 @@ class AimedbConfig: def __init__(self, parent_config: "CoreConfig") -> None: self.__config = parent_config - @property - def enable(self) -> bool: - return CoreConfig.get_config_field( - self.__config, "core", "aimedb", "enable", default=True - ) - - @property - def listen_address(self) -> bool: - return CoreConfig.get_config_field( - self.__config, "core", "aimedb", "listen_address", default="" - ) - @property def loglevel(self) -> int: return CoreConfig.str_to_loglevel( @@ -371,6 +331,12 @@ class MuchaConfig: def __init__(self, parent_config: "CoreConfig") -> None: self.__config = parent_config + @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( @@ -379,6 +345,12 @@ class MuchaConfig: ) ) + @property + def hostname(self) -> str: + return CoreConfig.get_config_field( + self.__config, "core", "mucha", "hostname", default="localhost" + ) + class CoreConfig(dict): def __init__(self) -> None: @@ -402,19 +374,6 @@ class CoreConfig(dict): else: return logging.INFO - @classmethod - def loglevel_to_str(cls, level: int) -> str: - if level == logging.ERROR: - return "error" - elif level == logging.WARN: - return "warn" - elif level == logging.INFO: - return "info" - elif level == logging.DEBUG: - return "debug" - else: - return "notset" - @classmethod def get_config_field( cls, __config: dict, module, *path: str, default: Any = "" diff --git a/core/data/__init__.py b/core/data/__init__.py index f9a5ab5..eb30d05 100644 --- a/core/data/__init__.py +++ b/core/data/__init__.py @@ -1,3 +1,2 @@ -# ruff: noqa: F401 -from .cache import cached -from .database import Data +from core.data.database import Data +from core.data.cache import cached diff --git a/core/data/alembic/README b/core/data/alembic/README deleted file mode 100644 index 98e4f9c..0000000 --- a/core/data/alembic/README +++ /dev/null @@ -1 +0,0 @@ -Generic single-database configuration. \ No newline at end of file diff --git a/core/data/alembic/alembic.ini b/core/data/alembic/alembic.ini deleted file mode 100644 index 26b89ea..0000000 --- a/core/data/alembic/alembic.ini +++ /dev/null @@ -1,64 +0,0 @@ -# A generic, single database configuration. - -[alembic] -script_location=. - -# template used to generate migration files -# file_template = %%(rev)s_%%(slug)s - -# max length of characters to apply to the -# "slug" field -#truncate_slug_length = 40 - -# set to 'true' to run the environment during -# the 'revision' command, regardless of autogenerate -# revision_environment = false - -# set to 'true' to allow .pyc and .pyo files without -# a source .py file to be detected as revisions in the -# versions/ directory -# sourceless = false - -# version location specification; this defaults -# to migrations//versions. When using multiple version -# directories, initial revisions must be specified with --version-path -# version_locations = %(here)s/bar %(here)s/bat migrations//versions - -# the output encoding used when revision files -# are written from script.py.mako -# output_encoding = utf-8 - -# Logging configuration -[loggers] -keys = root,sqlalchemy,alembic - -[handlers] -keys = console - -[formatters] -keys = generic - -[logger_root] -level = WARN -handlers = console -qualname = - -[logger_sqlalchemy] -level = WARN -handlers = -qualname = sqlalchemy.engine - -[logger_alembic] -level = INFO -handlers = -qualname = alembic - -[handler_console] -class = StreamHandler -args = (sys.stderr,) -level = NOTSET -formatter = generic - -[formatter_generic] -format = %(levelname)-5.5s [%(name)s] %(message)s -datefmt = %H:%M:%S diff --git a/core/data/alembic/env.py b/core/data/alembic/env.py deleted file mode 100644 index c098f6e..0000000 --- a/core/data/alembic/env.py +++ /dev/null @@ -1,81 +0,0 @@ -from __future__ import with_statement - -from logging.config import fileConfig - -from alembic import context -from core.data.schema.base import metadata -from sqlalchemy import engine_from_config, pool - -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. -config = context.config - -# Interpret the config file for Python logging. -# This line sets up loggers basically. -fileConfig(config.config_file_name) - -# add your model's MetaData object here -# for 'autogenerate' support -# from myapp import mymodel -# target_metadata = mymodel.Base.metadata -target_metadata = metadata - -# other values from the config, defined by the needs of env.py, -# can be acquired: -# my_important_option = config.get_main_option("my_important_option") -# ... etc. - - -def run_migrations_offline(): - """Run migrations in 'offline' mode. - - This configures the context with just a URL - and not an Engine, though an Engine is acceptable - here as well. By skipping the Engine creation - we don't even need a DBAPI to be available. - - Calls to context.execute() here emit the given string to the - script output. - - """ - raise Exception("Not implemented or configured!") - - url = config.get_main_option("sqlalchemy.url") - context.configure(url=url, target_metadata=target_metadata, literal_binds=True) - - with context.begin_transaction(): - context.run_migrations() - - -def run_migrations_online(): - """Run migrations in 'online' mode. - - In this scenario we need to create an Engine - and associate a connection with the context. - - """ - ini_section = config.get_section(config.config_ini_section) - overrides = context.get_x_argument(as_dictionary=True) - for override in overrides: - ini_section[override] = overrides[override] - - connectable = engine_from_config( - ini_section, prefix="sqlalchemy.", poolclass=pool.NullPool - ) - - with connectable.connect() as connection: - context.configure( - connection=connection, - target_metadata=target_metadata, - compare_type=True, - compare_server_default=True, - ) - - with context.begin_transaction(): - context.run_migrations() - - -if context.is_offline_mode(): - run_migrations_offline() -else: - run_migrations_online() diff --git a/core/data/alembic/script.py.mako b/core/data/alembic/script.py.mako deleted file mode 100644 index 2c01563..0000000 --- a/core/data/alembic/script.py.mako +++ /dev/null @@ -1,24 +0,0 @@ -"""${message} - -Revision ID: ${up_revision} -Revises: ${down_revision | comma,n} -Create Date: ${create_date} - -""" -from alembic import op -import sqlalchemy as sa -${imports if imports else ""} - -# revision identifiers, used by Alembic. -revision = ${repr(up_revision)} -down_revision = ${repr(down_revision)} -branch_labels = ${repr(branch_labels)} -depends_on = ${repr(depends_on)} - - -def upgrade(): - ${upgrades if upgrades else "pass"} - - -def downgrade(): - ${downgrades if downgrades else "pass"} diff --git a/core/data/alembic/versions/6a7e8277763b_gekichu_rating_tables.py b/core/data/alembic/versions/6a7e8277763b_gekichu_rating_tables.py deleted file mode 100644 index 2d4074a..0000000 --- a/core/data/alembic/versions/6a7e8277763b_gekichu_rating_tables.py +++ /dev/null @@ -1,56 +0,0 @@ -"""GekiChu rating tables - -Revision ID: 6a7e8277763b -Revises: d8950c7ce2fc -Create Date: 2024-03-13 12:18:53.210018 - -""" -from alembic import op -from sqlalchemy import Column, Integer, String - - -# revision identifiers, used by Alembic. -revision = '6a7e8277763b' -down_revision = 'd8950c7ce2fc' -branch_labels = None -depends_on = None - -GEKICHU_RATING_TABLE_NAMES = [ - "chuni_profile_rating", - "ongeki_profile_rating", -] - -def upgrade(): - for table_name in GEKICHU_RATING_TABLE_NAMES: - op.create_table( - table_name, - Column("id", Integer, primary_key=True, nullable=False), - Column("user", Integer, nullable=False), - Column("version", Integer, nullable=False), - Column("type", String(255), nullable=False), - Column("index", Integer, nullable=False), - Column("musicId", Integer), - Column("difficultId", Integer), - Column("romVersionCode", Integer), - Column("score", Integer), - mysql_charset="utf8mb4", - ) - op.create_foreign_key( - None, - table_name, - "aime_user", - ["user"], - ["id"], - ondelete="cascade", - onupdate="cascade", - ) - op.create_unique_constraint( - f"{table_name}_uk", - table_name, - ["user", "version", "type", "index"], - ) - - -def downgrade(): - for table_name in GEKICHU_RATING_TABLE_NAMES: - op.drop_table(table_name) diff --git a/core/data/alembic/versions/835b862f9bf0_initial_migration.py b/core/data/alembic/versions/835b862f9bf0_initial_migration.py deleted file mode 100644 index 30ca38e..0000000 --- a/core/data/alembic/versions/835b862f9bf0_initial_migration.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Initial Migration - -Revision ID: 835b862f9bf0 -Revises: -Create Date: 2024-01-09 13:06:10.787432 - -""" - -# revision identifiers, used by Alembic. -revision = "835b862f9bf0" -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - pass - - -def downgrade(): - pass diff --git a/core/data/alembic/versions/d8950c7ce2fc_remove_old_db_mgmt_system.py b/core/data/alembic/versions/d8950c7ce2fc_remove_old_db_mgmt_system.py deleted file mode 100644 index f7e00f4..0000000 --- a/core/data/alembic/versions/d8950c7ce2fc_remove_old_db_mgmt_system.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Remove old db mgmt system - -Revision ID: d8950c7ce2fc -Revises: 835b862f9bf0 -Create Date: 2024-01-09 13:43:51.381175 - -""" - -import sqlalchemy as sa -from alembic import op - -# revision identifiers, used by Alembic. -revision = "d8950c7ce2fc" -down_revision = "835b862f9bf0" -branch_labels = None -depends_on = None - - -def upgrade(): - op.drop_table("schema_versions") - - -def downgrade(): - op.create_table( - "schema_versions", - sa.Column("game", sa.String(4), primary_key=True, nullable=False), - sa.Column("version", sa.Integer, nullable=False, server_default="1"), - mysql_charset="utf8mb4", - ) diff --git a/core/data/alembic/versions/ead361541998_chunithm_luminous.py b/core/data/alembic/versions/ead361541998_chunithm_luminous.py deleted file mode 100644 index 33b949c..0000000 --- a/core/data/alembic/versions/ead361541998_chunithm_luminous.py +++ /dev/null @@ -1,88 +0,0 @@ -"""CHUNITHM Luminous - -Revision ID: ead361541998 -Revises: d8950c7ce2fc -Create Date: 2024-03-05 08:39:14.630558 - -""" - -from alembic import op -from sqlalchemy import Column, UniqueConstraint -from sqlalchemy.types import Boolean, Integer - -# revision identifiers, used by Alembic. -revision = "ead361541998" -down_revision = "d8950c7ce2fc" -branch_labels = None -depends_on = None - - -def upgrade(): - op.create_table( - "chuni_profile_net_battle", - Column("id", Integer, primary_key=True, nullable=False), - Column("user", Integer, nullable=False), - Column("isRankUpChallengeFailed", Boolean), - Column("highestBattleRankId", Integer), - Column("battleIconId", Integer), - Column("battleIconNum", Integer), - Column("avatarEffectPoint", Integer), - mysql_charset="utf8mb4", - ) - op.create_foreign_key( - None, - "chuni_profile_net_battle", - "aime_user", - ["user"], - ["id"], - ondelete="cascade", - onupdate="cascade", - ) - - op.create_table( - "chuni_item_cmission", - Column("id", Integer, primary_key=True, nullable=False), - Column("user", Integer, nullable=False), - Column("missionId", Integer, nullable=False), - Column("point", Integer), - UniqueConstraint("user", "missionId", name="chuni_item_cmission_uk"), - mysql_charset="utf8mb4", - ) - op.create_foreign_key( - None, - "chuni_item_cmission", - "aime_user", - ["user"], - ["id"], - ondelete="cascade", - onupdate="cascade", - ) - - op.create_table( - "chuni_item_cmission_progress", - Column("id", Integer, primary_key=True, nullable=False), - Column("user", Integer, nullable=False), - Column("missionId", Integer, nullable=False), - Column("order", Integer), - Column("stage", Integer), - Column("progress", Integer), - UniqueConstraint( - "user", "missionId", "order", name="chuni_item_cmission_progress_uk" - ), - mysql_charset="utf8mb4", - ) - op.create_foreign_key( - None, - "chuni_item_cmission_progress", - "aime_user", - ["user"], - ["id"], - ondelete="cascade", - onupdate="cascade", - ) - - -def downgrade(): - op.drop_table("chuni_profile_net_battle") - op.drop_table("chuni_item_cmission") - op.drop_table("chuni_item_cmission_progress") diff --git a/core/data/cache.py b/core/data/cache.py index da12b45..1490826 100644 --- a/core/data/cache.py +++ b/core/data/cache.py @@ -1,9 +1,8 @@ -import hashlib -import logging -import pickle -from functools import wraps from typing import Any, Callable - +from functools import wraps +import hashlib +import pickle +import logging from core.config import CoreConfig cfg: CoreConfig = None # type: ignore diff --git a/core/data/database.py b/core/data/database.py index 8f0a991..e39d864 100644 --- a/core/data/database.py +++ b/core/data/database.py @@ -1,16 +1,13 @@ -import logging -import os -import secrets -import string -from hashlib import sha256 -from logging.handlers import TimedRotatingFileHandler -from typing import Optional - -import alembic.config -import bcrypt -import coloredlogs -from sqlalchemy import create_engine +import logging, coloredlogs +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 * @@ -18,13 +15,13 @@ 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 @@ -44,20 +41,20 @@ class Data: if Data.user is None: Data.user = UserData(self.config, self.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.logger = logging.getLogger("database") # Prevent the logger from adding handlers multiple times - if not getattr(self.logger, "handler_set", None): + 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( @@ -80,207 +77,281 @@ class Data: ) self.logger.handler_set = True # type: ignore - def __alembic_cmd(self, command: str, *args: str) -> None: - old_dir = os.path.abspath(os.path.curdir) - base_dir = os.path.join( - os.path.abspath(os.path.curdir), "core", "data", "alembic" - ) - alembicArgs = [ - "-c", - os.path.join(base_dir, "alembic.ini"), - "-x", - f"script_location={base_dir}", - "-x", - f"sqlalchemy.url={self.__url}", - command, - ] - alembicArgs.extend(args) - os.chdir(base_dir) - alembic.config.main(argv=alembicArgs) - os.chdir(old_dir) - def create_database(self): self.logger.info("Creating databases...") - metadata.create_all( - self.engine, - checkfirst=True, - ) + try: + metadata.create_all(self.__engine.connect()) + except SQLAlchemyError as e: + self.logger.error(f"Failed to create databases! {e}") + return - for _, mod in Utils.get_all_titles().items(): - if hasattr(mod, "database"): - mod.database(self.config) - metadata.create_all( - self.engine, - checkfirst=True, + 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] ) - # Stamp the end revision as if alembic had created it, so it can take off after this. - self.__alembic_cmd( - "stamp", - "head", + 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 schema_upgrade(self, ver: str = None): - self.__alembic_cmd( - "upgrade", - "head" if not ver else ver, - ) + 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 - def schema_downgrade(self, ver: str): - self.__alembic_cmd( - "downgrade", - ver, - ) + for root, dirs, files in os.walk("./titles"): + for dir in dirs: + if not dir.startswith("__"): + try: + mod = importlib.import_module(f"titles.{dir}") - async def create_owner( - self, email: Optional[str] = None, code: Optional[str] = "00000000000000000000" - ) -> None: + 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 = await self.user.create_user( - username="sysowner", email=email, password=hash.decode(), permission=255 - ) + 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 = await self.card.create_card(user_id, code) + 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 {code}, and password {pw} Make sure to change this password and assign a real card ASAP!" + 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!" ) - async def migrate(self) -> None: - exist = await self.base.execute("SELECT * FROM alembic_version") - if exist is not None: - self.logger.warn( - "No need to migrate as you have already migrated to alembic. If you are trying to upgrade the schema, use `upgrade` instead!" + 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("Upgrading to latest with legacy system") - if not await self.legacy_upgrade(): - self.logger.warn( - "No need to migrate as you have already deleted the old schema_versions system. If you are trying to upgrade the schema, use `upgrade` instead!" - ) + 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 - self.logger.info("Done") - self.logger.info("Stamp with initial revision") - self.__alembic_cmd( - "stamp", - "835b862f9bf0", - ) + 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 - self.logger.info("Upgrade") - self.__alembic_cmd( - "upgrade", - "head", - ) + 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 - async def legacy_upgrade(self) -> bool: - vers = await self.base.execute("SELECT * FROM schema_versions") - if vers is None: - self.logger.warn( - "Cannot legacy upgrade, schema_versions table unavailable!" - ) - return False + if update_ver == latest_ver: + self.logger.info(f"{game} is already latest version") + continue - db_vers = {} - vers_list = vers.fetchall() - for x in vers_list: - db_vers[x["game"]] = x["version"] - - core_now_ver = int(db_vers["CORE"]) + 1 - while os.path.exists( - f"core/data/schema/versions/CORE_{core_now_ver}_upgrade.sql" - ): - with open( - f"core/data/schema/versions/CORE_{core_now_ver}_upgrade.sql", "r" - ) as f: - result = await self.base.execute(f.read()) - - if result is None: - self.logger.error( - f"Invalid upgrade script CORE_{core_now_ver}_upgrade.sql" - ) - break - - result = await self.base.execute( - f"UPDATE schema_versions SET version = {core_now_ver} WHERE game = 'CORE'" - ) - if result is None: - self.logger.error( - f"Failed to update schema version for CORE to {core_now_ver}" - ) - break - - self.logger.info(f"Upgrade CORE to version {core_now_ver}") - core_now_ver += 1 - - for _, mod in Utils.get_all_titles().items(): - game_codes = getattr(mod, "game_codes", []) - for game in game_codes: - if game not in db_vers: - self.logger.warn( - f"{game} does not have an antry in schema_versions, skipping" - ) - continue - - now_ver = int(db_vers[game]) + 1 - while os.path.exists( - f"core/data/schema/versions/{game}_{now_ver}_upgrade.sql" - ): + 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}_{now_ver}_upgrade.sql", "r" + f"core/data/schema/versions/{game}_{y}_upgrade.sql", + "r", + encoding="utf-8", ) as f: - result = await self.base.execute(f.read()) + sql = f.read() - if result is None: - self.logger.error( - f"Invalid upgrade script {game}_{now_ver}_upgrade.sql" - ) - break - - result = await self.base.execute( - f"UPDATE schema_versions SET version = {now_ver} WHERE game = '{game}'" + result = self.base.execute(sql) + if result is None: + self.logger.error( + f"Error execuing sql script for game {game} v{y}!" ) - if result is None: - self.logger.error( - f"Failed to update schema version for {game} to {now_ver}" - ) - break + failed = True + break + else: + self.logger.warning(f"Could not find script {game}_{y}_upgrade.sql") + failed = True - self.logger.info(f"Upgrade {game} to version {now_ver}") - now_ver += 1 - - return True - - async def create_revision(self, message: str) -> None: - if not message: - self.logger.info("Message is required for create-revision") - return - - self.__alembic_cmd( - "revision", - "-m", - message, - ) - - async def create_revision_auto(self, message: str) -> None: - if not message: - self.logger.info("Message is required for create-revision") - return - - self.__alembic_cmd( - "revision", - "--autogenerate", - "-m", - message, - ) + 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 618c93f..45931d7 100644 --- a/core/data/schema/__init__.py +++ b/core/data/schema/__init__.py @@ -1,6 +1,6 @@ -from core.data.schema.arcade import ArcadeData -from core.data.schema.base import BaseData, metadata -from core.data.schema.card import CardData from core.data.schema.user import UserData +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"] diff --git a/core/data/schema/arcade.py b/core/data/schema/arcade.py index 00fc293..2fb8e43 100644 --- a/core/data/schema/arcade.py +++ b/core/data/schema/arcade.py @@ -1,15 +1,14 @@ -import re -from typing import List, Optional - -from sqlalchemy import Column, Table, and_, or_ +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, JSON +from sqlalchemy.sql import func, select from sqlalchemy.dialects.mysql import insert from sqlalchemy.engine import Row -from sqlalchemy.sql import select -from sqlalchemy.sql.schema import ForeignKey, PrimaryKeyConstraint -from sqlalchemy.types import JSON, Boolean, Integer, String +import re -from core.const import * from core.data.schema.base import BaseData, metadata +from core.const import * arcade = Table( "arcade", @@ -70,7 +69,7 @@ arcade_owner = Table( class ArcadeData(BaseData): - async def get_machine(self, serial: str = None, id: int = None) -> Optional[Row]: + def get_machine(self, serial: str = None, id: int = None) -> Optional[Row]: if serial is not None: serial = serial.replace("-", "") if len(serial) == 11: @@ -90,12 +89,12 @@ class ArcadeData(BaseData): self.logger.error(f"{__name__ }: Need either serial or ID to look up!") return None - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def put_machine( + def put_machine( self, arcade_id: int, serial: str = "", @@ -111,13 +110,13 @@ class ArcadeData(BaseData): arcade=arcade_id, keychip=serial, board=board, game=game, is_cab=is_cab ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.lastrowid - async def set_machine_serial(self, machine_id: int, serial: str) -> None: - result = await self.execute( + 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: @@ -126,8 +125,8 @@ class ArcadeData(BaseData): ) return result.lastrowid - async def set_machine_boardid(self, machine_id: int, boardid: str) -> None: - result = await self.execute( + 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: @@ -135,21 +134,21 @@ class ArcadeData(BaseData): f"Failed to update board id for machine {machine_id} -> {boardid}" ) - async def get_arcade(self, id: int) -> Optional[Row]: + def get_arcade(self, id: int) -> Optional[Row]: sql = arcade.select(arcade.c.id == id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - - async def get_arcade_machines(self, id: int) -> Optional[List[Row]]: + + def get_arcade_machines(self, id: int) -> Optional[List[Row]]: sql = machine.select(machine.c.arcade == id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_arcade( + def put_arcade( self, name: str, nickname: str = None, @@ -172,77 +171,62 @@ class ArcadeData(BaseData): regional_id=regional_id, ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.lastrowid - async 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 = await self.execute(sql) + 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() - - async 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 = await self.execute(sql) + + 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() - async def get_arcade_owners(self, arcade_id: int) -> Optional[Row]: + def get_arcade_owners(self, arcade_id: int) -> Optional[Row]: sql = select(arcade_owner).where(arcade_owner.c.arcade == arcade_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def add_arcade_owner(self, arcade_id: int, user_id: int) -> None: + def add_arcade_owner(self, arcade_id: int, user_id: int) -> None: sql = insert(arcade_owner).values(arcade=arcade_id, user=user_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.lastrowid - async def format_serial( + 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 - ): + 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 - async 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 = await self.execute(sql) + 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() - async def get_arcades_by_ip(self, ip: str) -> Optional[List[Row]]: + def get_arcades_by_ip(self, ip: str) -> Optional[List[Row]]: sql = arcade.select().where(arcade.c.ip == ip) - result = await self.execute(sql) + 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 c3f9357..ef980e5 100644 --- a/core/data/schema/base.py +++ b/core/data/schema/base.py @@ -1,19 +1,28 @@ import json import logging from random import randrange -from typing import Any, Dict, List, Optional - -from sqlalchemy import Column, MetaData, Table -from sqlalchemy.engine.base import Connection +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 from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy.sql import func, text -from sqlalchemy.types import JSON, TIMESTAMP, Integer, String +from sqlalchemy import MetaData, Table, Column +from sqlalchemy.types import Integer, String, TIMESTAMP, JSON +from sqlalchemy.dialects.mysql import insert from core.config import CoreConfig metadata = MetaData() +schema_ver = Table( + "schema_versions", + metadata, + Column("game", String(4), primary_key=True, nullable=False), + Column("version", Integer, nullable=False, server_default="1"), + mysql_charset="utf8mb4", +) + event_log = Table( "event_log", metadata, @@ -34,13 +43,11 @@ class BaseData: self.conn = conn self.logger = logging.getLogger("database") - async 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.debug(f"SQL Execute: {''.join(str(sql).splitlines())}") + self.logger.info(f"SQL Execute: {''.join(str(sql).splitlines())}") res = self.conn.execute(text(sql), opts) except SQLAlchemyError as e: @@ -75,7 +82,52 @@ class BaseData: """ return randrange(10000, 9999999) - async def log_event( + 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 + + row = result.fetchone() + if row is None: + return None + + return row["version"] + + 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})" + ) + return None + return result.lastrowid + + 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( @@ -85,7 +137,7 @@ class BaseData: message=message, details=json.dumps(details), ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: self.logger.error( @@ -95,9 +147,9 @@ class BaseData: return result.lastrowid - async def get_event_log(self, entries: int = 100) -> Optional[List[Dict]]: + def get_event_log(self, entries: int = 100) -> Optional[List[Dict]]: sql = event_log.select().limit(entries).all() - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None diff --git a/core/data/schema/card.py b/core/data/schema/card.py index e13e45c..a95684e 100644 --- a/core/data/schema/card.py +++ b/core/data/schema/card.py @@ -1,10 +1,9 @@ -from typing import List, Optional - -from sqlalchemy import Column, Table, UniqueConstraint -from sqlalchemy.engine import Row -from sqlalchemy.sql import func +from typing import Dict, List, Optional +from sqlalchemy import Table, Column, UniqueConstraint +from sqlalchemy.types import Integer, String, Boolean, TIMESTAMP from sqlalchemy.sql.schema import ForeignKey -from sqlalchemy.types import TIMESTAMP, Boolean, Integer, String +from sqlalchemy.sql import func +from sqlalchemy.engine import Row from core.data.schema.base import BaseData, metadata @@ -28,101 +27,91 @@ aime_card = Table( class CardData(BaseData): - async def get_card_by_access_code(self, access_code: str) -> Optional[Row]: + def get_card_by_access_code(self, access_code: str) -> Optional[Row]: sql = aime_card.select(aime_card.c.access_code == access_code) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_card_by_id(self, card_id: int) -> Optional[Row]: + def get_card_by_id(self, card_id: int) -> Optional[Row]: sql = aime_card.select(aime_card.c.id == card_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def update_access_code(self, old_ac: str, new_ac: str) -> None: + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: self.logger.error( f"Failed to change card access code from {old_ac} to {new_ac}" ) - async def get_user_id_from_card(self, access_code: str) -> Optional[int]: + 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 """ - card = await self.get_card_by_access_code(access_code) + card = self.get_card_by_access_code(access_code) if card is None: return None return int(card["user"]) - async def get_card_banned(self, access_code: str) -> Optional[bool]: + 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 = await self.get_card_by_access_code(access_code) + card = self.get_card_by_access_code(access_code) if card is None: return None if card["is_banned"]: return True return False - - async def get_card_locked(self, access_code: str) -> Optional[bool]: + 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 = await self.get_card_by_access_code(access_code) + card = self.get_card_by_access_code(access_code) if card is None: return None if card["is_locked"]: return True return False - async def delete_card(self, card_id: int) -> None: + def delete_card(self, card_id: int) -> None: sql = aime_card.delete(aime_card.c.id == card_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: self.logger.error(f"Failed to delete card with id {card_id}") - async def get_user_cards(self, aime_id: int) -> Optional[List[Row]]: + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def create_card(self, user_id: int, access_code: str) -> Optional[int]: + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.lastrowid - async def update_card_last_login(self, access_code: str) -> None: - sql = aime_card.update(aime_card.c.access_code == access_code).values( - last_login_date=func.now() - ) - - result = await self.execute(sql) - if result is None: - self.logger.warn(f"Failed to update last login time for {access_code}") - def to_access_code(self, luid: str) -> str: """ Given a felica cards internal 16 hex character luid, convert it to a 0-padded 20 digit access code as a string diff --git a/core/data/schema/user.py b/core/data/schema/user.py index aaf07a6..221ba81 100644 --- a/core/data/schema/user.py +++ b/core/data/schema/user.py @@ -1,11 +1,12 @@ -from typing import List, Optional - -import bcrypt -from sqlalchemy import Column, Table +from enum import Enum +from typing import Optional, List +from sqlalchemy import Table, Column +from sqlalchemy.types import Integer, String, TIMESTAMP +from sqlalchemy.sql import func from sqlalchemy.dialects.mysql import insert -from sqlalchemy.engine import Row from sqlalchemy.sql import func, select -from sqlalchemy.types import TIMESTAMP, Integer, String +from sqlalchemy.engine import Row +import bcrypt from core.data.schema.base import BaseData, metadata @@ -24,8 +25,14 @@ aime_user = Table( ) +class PermissionBits(Enum): + PermUser = 1 + PermMod = 2 + PermSysAdmin = 4 + + class UserData(BaseData): - async def create_user( + def create_user( self, id: int = None, username: str = None, @@ -53,71 +60,64 @@ class UserData(BaseData): username=username, email=email, password=password, permissions=permission ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: return None return result.lastrowid - async def get_user(self, user_id: int) -> Optional[Row]: + def get_user(self, user_id: int) -> Optional[Row]: sql = select(aime_user).where(aime_user.c.id == user_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return False return result.fetchone() - async def check_password(self, user_id: int, passwd: bytes = None) -> bool: - usr = await self.get_user(user_id) + 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()) - async def delete_user(self, user_id: int) -> None: + def reset_autoincrement(self, ai_value: int) -> None: + # 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) + + def delete_user(self, user_id: int) -> None: sql = aime_user.delete(aime_user.c.id == user_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: self.logger.error(f"Failed to delete user with id {user_id}") - async def get_unregistered_users(self) -> List[Row]: + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def find_user_by_email(self, email: str) -> Row: + def find_user_by_email(self, email: str) -> Row: sql = select(aime_user).where(aime_user.c.email == email) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return False return result.fetchone() - async def find_user_by_username(self, username: str) -> List[Row]: + def find_user_by_username(self, username: str) -> List[Row]: sql = aime_user.select(aime_user.c.username.like(f"%{username}%")) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return False return result.fetchall() - - async def change_password(self, user_id: int, new_passwd: str) -> bool: - sql = aime_user.update(aime_user.c.id == user_id).values(password=new_passwd) - - result = await self.execute(sql) - return result is not None - - async def change_username(self, user_id: int, new_name: str) -> bool: - sql = aime_user.update(aime_user.c.id == user_id).values(username=new_name) - - result = await self.execute(sql) - return result is not None diff --git a/core/data/schema/versions/SDDT_5_rollback.sql b/core/data/schema/versions/SDDT_5_rollback.sql index 61bb352..007716c 100644 --- a/core/data/schema/versions/SDDT_5_rollback.sql +++ b/core/data/schema/versions/SDDT_5_rollback.sql @@ -1,8 +1,8 @@ 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 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; @@ -19,4 +19,4 @@ DROP TABLE ongeki_static_tech_music; DROP TABLE ongeki_static_client_testmode; DROP TABLE ongeki_static_game_point; -SET FOREIGN_KEY_CHECKS=1; \ No newline at end of file +SET FOREIGN_KEY_CHECKS=1; diff --git a/core/data/schema/versions/SDDT_6_upgrade.sql b/core/data/schema/versions/SDDT_6_upgrade.sql index 1afa186..82d5336 100644 --- a/core/data/schema/versions/SDDT_6_upgrade.sql +++ b/core/data/schema/versions/SDDT_6_upgrade.sql @@ -1,8 +1,8 @@ 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 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; @@ -12,87 +12,87 @@ 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 + 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) + 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) + 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) + 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) + 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) + 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`) + 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; \ No newline at end of file +SET FOREIGN_KEY_CHECKS=1; diff --git a/core/data/schema/versions/SDEZ_8_rollback.sql b/core/data/schema/versions/SDEZ_8_rollback.sql index cfc5d4c..65f700e 100644 --- a/core/data/schema/versions/SDEZ_8_rollback.sql +++ b/core/data/schema/versions/SDEZ_8_rollback.sql @@ -1,8 +1,4 @@ -ALTER TABLE mai2_profile_detail - DROP COLUMN currentPlayCount, - DROP COLUMN renameCredit; +ALTER TABLE mai2_profile_detail DROP COLUMN currentPlayCount; +ALTER TABLE mai2_profile_detail DROP COLUMN renameCredit; -ALTER TABLE mai2_playlog - DROP COLUMN extBool1; - -DROP TABLE IF EXISTS `mai2_playlog_2p`; +ALTER TABLE mai2_playlog DROP COLUMN extBool1; diff --git a/core/data/schema/versions/SDEZ_9_upgrade.sql b/core/data/schema/versions/SDEZ_9_upgrade.sql index 4481075..fa6016c 100644 --- a/core/data/schema/versions/SDEZ_9_upgrade.sql +++ b/core/data/schema/versions/SDEZ_9_upgrade.sql @@ -1,20 +1,4 @@ -ALTER TABLE mai2_profile_detail - ADD currentPlayCount INT NULL AFTER playCount, - ADD renameCredit INT NULL AFTER banState; +ALTER TABLE mai2_profile_detail ADD currentPlayCount INT NULL; +ALTER TABLE mai2_profile_detail ADD renameCredit INT NULL; -ALTER TABLE mai2_playlog - ADD extBool1 BOOLEAN NULL AFTER extNum4; - -CREATE TABLE `mai2_playlog_2p` ( - `id` INT NOT NULL AUTO_INCREMENT, - `user` INT NOT NULL, - `userId1` BIGINT, - `userId2` BIGINT, - `userName1` VARCHAR(255), - `userName2` VARCHAR(255), - `regionId` INT, - `placeId` INT, - `user2pPlaylogDetailList` JSON, - PRIMARY KEY (`id`), - FOREIGN KEY (`user`) REFERENCES `aime_user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +ALTER TABLE mai2_playlog ADD extBool1 BOOLEAN NULL; diff --git a/core/frontend.py b/core/frontend.py index c5e8c1f..0ee2211 100644 --- a/core/frontend.py +++ b/core/frontend.py @@ -1,117 +1,88 @@ -import logging -import re -from base64 import b64decode -from datetime import datetime, timezone -from enum import Enum +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 os import W_OK, access, environ, mkdir, path -from typing import Any, Dict, List, Optional, Union - -import bcrypt -import coloredlogs +from twisted.web.server import Session +from zope.interface import Interface, Attribute, implementer +from twisted.python.components import registerAdapter import jinja2 -import jwt -import yaml -from starlette.applications import Starlette -from starlette.requests import Request -from starlette.responses import PlainTextResponse, RedirectResponse, Response -from starlette.routing import Mount, Route +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 + 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 + 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 ShopPermissionOffset(Enum): - VIEW = 0 # View info and cabs - BOOKKEEP = 1 # View bookeeping info - EDITOR = 2 # Can edit name, settings - REGISTRAR = 3 # Can add cabs - # 4 - 6 reserved for future use - OWNER = 7 # Can do anything +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) - -class ShopOwner: - def __init__(self, usr_id: int = 0, usr_name: str = "", perms: int = 0) -> None: - self.user_id = usr_id - self.username = usr_name - self.permissions = perms - - -class UserSession: - def __init__( - self, usr_id: int = 0, ip: str = "", perms: int = 0, ongeki_ver: int = 7 - ): - self.user_id = usr_id - self.current_ip = ip - self.permissions = perms - self.ongeki_version = ongeki_ver - - -class FrontendServlet: 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.environment = jinja2.Environment(loader=jinja2.FileSystemLoader(".")) - self.game_list: Dict[str, Dict[str, Any]] = {} - self.sn_cvt: Dict[str, str] = {} - self.logger = logging.getLogger("frontend") - if not hasattr(self.logger, "inited"): - fileHandler = TimedRotatingFileHandler( - "{0}/{1}.log".format(self.config.server.log_dir, "frontend"), - when="d", - backupCount=10, - ) - fileHandler.setFormatter(log_fmt) + self.environment = jinja2.Environment(loader=jinja2.FileSystemLoader(".")) + self.game_list: List[Dict[str, str]] = [] + self.children: Dict[str, Any] = {} - consoleHandler = logging.StreamHandler() - consoleHandler.setFormatter(log_fmt) + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.config.server.log_dir, "frontend"), + when="d", + backupCount=10, + ) + fileHandler.setFormatter(log_fmt) - self.logger.addHandler(fileHandler) - self.logger.addHandler(consoleHandler) + consoleHandler = logging.StreamHandler() + consoleHandler.setFormatter(log_fmt) - self.logger.setLevel(cfg.frontend.loglevel) - coloredlogs.install( - level=cfg.frontend.loglevel, logger=self.logger, fmt=log_fmt_str - ) + self.logger.addHandler(fileHandler) + self.logger.addHandler(consoleHandler) - self.logger.inited = True + 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") - and hasattr(game_mod, "index") - and hasattr(game_mod, "game_codes") - ): + if hasattr(game_mod, "frontend"): try: - if game_mod.index.is_game_enabled( - game_mod.game_codes[0], self.config, config_dir - ): - game_fe = game_mod.frontend(cfg, self.environment, config_dir) - self.game_list[game_fe.nav_name] = { - "url": f"/{game_dir}", - "class": game_fe, - } - - if hasattr(game_fe, "SN_PREFIX") and hasattr( - game_fe, "NETID_PREFIX" - ): - if len(game_fe.SN_PREFIX) == len(game_fe.NETID_PREFIX): - for x in range(len(game_fe.SN_PREFIX)): - self.sn_cvt[game_fe.SN_PREFIX[x]] = ( - game_fe.NETID_PREFIX[x] - ) + 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( @@ -119,781 +90,371 @@ class FrontendServlet: ) self.environment.globals["game_list"] = self.game_list - self.environment.globals["sn_cvt"] = self.sn_cvt - self.base = FE_Base(cfg, self.environment) - self.gate = FE_Gate(cfg, self.environment) - self.user = FE_User(cfg, self.environment) - self.system = FE_System(cfg, self.environment) - self.arcade = FE_Arcade(cfg, self.environment) - self.machine = FE_Machine(cfg, self.environment) + 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) - def get_routes(self) -> List[Route]: - g_routes = [] - for nav_name, g_data in self.environment.globals["game_list"].items(): - g_routes.append(Mount(g_data["url"], routes=g_data["class"].get_routes())) - return [ - Route("/", self.base.render_GET, methods=["GET"]), - Mount( - "/user", - routes=[ - Route("/", self.user.render_GET, methods=["GET"]), - Route("/{user_id:int}", self.user.render_GET, methods=["GET"]), - Route("/update.pw", self.user.render_POST, methods=["POST"]), - Route("/update.name", self.user.update_username, methods=["POST"]), - Route("/edit.card", self.user.edit_card, methods=["POST"]), - Route("/add.card", self.user.add_card, methods=["POST"]), - Route("/logout", self.user.render_logout, methods=["GET"]), - ], - ), - Mount( - "/gate", - routes=[ - Route("/", self.gate.render_GET, methods=["GET", "POST"]), - Route("/gate.login", self.gate.render_login, methods=["POST"]), - Route("/gate.create", self.gate.render_create, methods=["POST"]), - Route("/create", self.gate.render_create_get, methods=["GET"]), - ], - ), - Mount( - "/sys", - routes=[ - Route("/", self.system.render_GET, methods=["GET"]), - Route("/lookup.user", self.system.lookup_user, methods=["GET"]), - Route("/lookup.shop", self.system.lookup_shop, methods=["GET"]), - ], - ), - Mount( - "/shop", - routes=[ - Route("/", self.arcade.render_GET, methods=["GET"]), - Route("/{shop_id:int}", self.arcade.render_GET, methods=["GET"]), - ], - ), - Mount( - "/cab", - routes=[ - Route("/", self.machine.render_GET, methods=["GET"]), - Route( - "/{machine_id:int}", self.machine.render_GET, methods=["GET"] - ), - ], - ), - Mount("/game", routes=g_routes), - Route("/robots.txt", self.robots), - ] - - def startup(self) -> None: - self.config.update( - { - "frontend": { - "standalone": True, - "loglevel": CoreConfig.loglevel_to_str( - self.config.frontend.loglevel - ), - "secret": self.config.frontend.secret, - } - } - ) - self.logger.info(f"Serving {len(self.game_list)} games") - - @classmethod - async def robots(cls, request: Request) -> PlainTextResponse: - return PlainTextResponse( - "User-agent: *\nDisallow: /\n\nUser-agent: AdsBot-Google\nDisallow: /" + 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: + +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 = "index" - - async def render_GET(self, request: Request): - self.logger.debug(f"{Utils.get_ip_addr(request)} -> {request.url}") - template = self.environment.get_template("core/templates/index.jinja") - sesh = self.validate_session(request) - resp = Response( - template.render( - server_name=self.core_config.server.name, - title=self.core_config.server.name, - game_list=self.environment.globals["game_list"], - sesh=vars(sesh) if sesh is not None else vars(UserSession()), - ), - media_type="text/html; charset=utf-8", - ) - - if sesh is None: - resp.delete_cookie("DIANA_SESH") - return resp - - def get_routes(self) -> List[Route]: - return [] - - @classmethod - def test_perm( - cls, permission: int, offset: Union[PermissionOffset, ShopPermissionOffset] - ) -> bool: - logging.getLogger("frontend").debug(f"{permission} vs {1 << offset.value}") - return permission & 1 << offset.value == 1 << offset.value - - @classmethod - def test_perm_minimum( - cls, permission: int, offset: Union[PermissionOffset, ShopPermissionOffset] - ) -> bool: - return permission >= 1 << offset.value - - def decode_session(self, token: str) -> UserSession: - sesh = UserSession() - if not token: - return sesh - try: - tk = jwt.decode( - token, - b64decode(self.core_config.frontend.secret), - options={"verify_signature": True}, - algorithms=["HS256"], - ) - sesh.user_id = tk["user_id"] - sesh.current_ip = tk["current_ip"] - sesh.permissions = tk["permissions"] - - if sesh.user_id <= 0: - self.logger.error( - "User session failed to validate due to an invalid ID!" - ) - return UserSession() - return sesh - except jwt.ExpiredSignatureError: - self.logger.error( - "User session failed to validate due to an expired signature!" - ) - return sesh - except jwt.InvalidSignatureError: - self.logger.error( - "User session failed to validate due to an invalid signature!" - ) - return sesh - except jwt.DecodeError as e: - self.logger.error(f"User session failed to decode! {e}") - return sesh - except jwt.InvalidTokenError as e: - self.logger.error(f"User session is invalid! {e}") - return sesh - except KeyError as e: - self.logger.error(f"{e} missing from User session!") - return UserSession() - except Exception as e: - self.logger.error( - f"Unknown exception occoured when decoding User session! {e}" - ) - return UserSession() - - def validate_session(self, request: Request) -> Optional[UserSession]: - sesh = request.cookies.get("DIANA_SESH", "") - if not sesh: - return None - - usr_sesh = self.decode_session(sesh) - req_ip = Utils.get_ip_addr(request) - - if usr_sesh.current_ip != req_ip: - self.logger.error( - f"User session failed to validate due to mismatched IPs! {usr_sesh.current_ip} -> {req_ip}" - ) - return None - - if usr_sesh.permissions <= 0 or usr_sesh.permissions > 255: - self.logger.error( - f"User session failed to validate due to an invalid permission value! {usr_sesh.permissions}" - ) - return None - - return usr_sesh - - def encode_session(self, sesh: UserSession, exp_seconds: int = 86400) -> str: - try: - return jwt.encode( - { - "user_id": sesh.user_id, - "current_ip": sesh.current_ip, - "permissions": sesh.permissions, - "ongeki_version": sesh.ongeki_version, - "exp": int(datetime.now(tz=timezone.utc).timestamp()) + exp_seconds, - }, - b64decode(self.core_config.frontend.secret), - algorithm="HS256", - ) - except jwt.InvalidKeyError: - self.logger.error( - "Failed to encode User session because the secret is invalid!" - ) - return "" - except Exception as e: - self.logger.error( - f"Unknown exception occoured when encoding User session! {e}" - ) - return "" + self.nav_name = "nav_name" class FE_Gate(FE_Base): - async def render_GET(self, request: Request): - self.logger.debug(f"{Utils.get_ip_addr(request)} -> {request.url.path}") + def render_GET(self, request: Request): + self.logger.debug(f"{Utils.get_ip_addr(request)} -> {request.uri.decode()}") + uri: str = request.uri.decode() - usr_sesh = self.validate_session(request) - if usr_sesh and usr_sesh.user_id > 0: - return RedirectResponse("/user/", 303) + sesh = request.getSession() + usr_sesh = IUserSession(sesh) + if usr_sesh.userId > 0: + return redirectTo(b"/user", request) - if "e" in request.query_params: + if uri.startswith("/gate/create"): + return self.create_user(request) + + if b"e" in request.args: try: - err = int(request.query_params.get("e", ["0"])[0]) + err = int(request.args[b"e"][0].decode()) except Exception: err = 0 else: err = 0 - template = self.environment.get_template("core/templates/gate/gate.jinja") - resp = Response( - template.render( - title=f"{self.core_config.server.name} | Login Gate", - error=err, - sesh=vars(UserSession()), - ), - media_type="text/html; charset=utf-8", - ) - resp.delete_cookie("DIANA_SESH") - return resp + 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") - async def render_login(self, request: Request): + def render_POST(self, request: Request): + uri = request.uri.decode() ip = Utils.get_ip_addr(request) - frm = await request.form() - access_code: str = frm.get("access_code", None) - if not access_code: - return RedirectResponse("/gate/?e=1", 303) - passwd: bytes = frm.get("passwd", "").encode() - if passwd == b"": - passwd = None + 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 = await self.data.card.get_user_id_from_card(access_code) - if uid is None: - self.logger.debug(f"Failed to find user for card {access_code}") - return RedirectResponse("/gate/?e=1", 303) + 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) - user = await self.data.user.get_user(uid) - if user is None: - self.logger.error(f"Failed to load user {uid}") - return RedirectResponse("/gate/?e=1", 303) + if passwd is None: + sesh = self.data.user.check_password(uid) - if passwd is None: - sesh = await 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 sesh is not None: - return RedirectResponse(f"/gate/create?ac={access_code}", 303) + if not self.data.user.check_password(uid, passwd): + return redirectTo(b"/gate?e=1", request) - return RedirectResponse("/gate/?e=1", 303) + self.logger.info(f"Successful login of user {uid} at {ip}") - if not await self.data.user.check_password(uid, passwd): - self.logger.debug(f"Failed password for access code {access_code}") - return RedirectResponse("/gate/?e=1", 303) + sesh = request.getSession() + usr_sesh = IUserSession(sesh) + usr_sesh.userId = uid + usr_sesh.current_ip = ip + usr_sesh.permissions = user['permissions'] - self.logger.info(f"Successful login of user {uid} at {ip}") + return redirectTo(b"/user", request) - sesh = UserSession() - sesh.user_id = uid - sesh.current_ip = ip - sesh.permissions = user["permissions"] + 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] - usr_sesh = self.encode_session(sesh) - self.logger.debug(f"Created session with JWT {usr_sesh}") - resp = RedirectResponse("/user/", 303) - resp.set_cookie("DIANA_SESH", usr_sesh) + uid = self.data.card.get_user_id_from_card(access_code) + if uid is None: + return redirectTo(b"/gate?e=1", request) - return resp + salt = bcrypt.gensalt() + hashed = bcrypt.hashpw(passwd, salt) - async def render_create(self, request: Request): - ip = Utils.get_ip_addr(request) - frm = await request.form() - access_code: str = frm.get("access_code", "") - username: str = frm.get("username", "") - email: str = frm.get("email", "") - passwd: bytes = frm.get("passwd", "").encode() - - if not access_code or not username or not email or not passwd: - return RedirectResponse("/gate/?e=1", 303) - - uid = await self.data.card.get_user_id_from_card(access_code) - if uid is None: - return RedirectResponse("/gate/?e=1", 303) - - salt = bcrypt.gensalt() - hashed = bcrypt.hashpw(passwd, salt) - - result = await self.data.user.create_user( - uid, username, email.lower(), hashed.decode(), 1 - ) - if result is None: - return RedirectResponse("/gate/?e=3", 303) - - if not await self.data.user.check_password(uid, passwd): - return RedirectResponse("/gate/", 303) - - sesh = UserSession() - sesh.user_id = uid - sesh.current_ip = ip - sesh.permissions = 1 - - usr_sesh = self.encode_session(sesh) - self.logger.debug(f"Created session with JWT {usr_sesh}") - resp = RedirectResponse("/user/", 303) - resp.set_cookie("DIANA_SESH", usr_sesh) - - return resp - - async def render_create_get(self, request: Request): - ac = request.query_params.get("ac", "") - if len(ac) != 20: - return RedirectResponse("/gate/?e=2", 303) - - card = await self.data.card.get_card_by_access_code(ac) - if card is None: - return RedirectResponse("/gate/?e=1", 303) - - user = await 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']}" + result = self.data.user.create_user( + uid, username, email.lower(), hashed.decode(), 1 ) - return RedirectResponse("/gate/?e=0", 303) + if result is None: + return redirectTo(b"/gate?e=3", request) - if user["password"] is not None: - return RedirectResponse("/gate/?e=1", 303) + if not self.data.user.check_password(uid, passwd): + return redirectTo(b"/gate", request) - template = self.environment.get_template("core/templates/gate/create.jinja") - return Response( - template.render( - title=f"{self.core_config.server.name} | Create User", - code=ac, - sesh={"user_id": 0, "permissions": 0}, - ), - media_type="text/html; charset=utf-8", - ) + 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): - async def render_GET(self, request: Request): - uri = request.url.path - user_id = request.path_params.get("user_id", None) - self.logger.debug(f"{Utils.get_ip_addr(request)} -> {uri}") - template = self.environment.get_template("core/templates/user/index.jinja") - - usr_sesh = self.validate_session(request) - if not usr_sesh: - return RedirectResponse("/gate/", 303) - - if user_id: - if ( - not self.test_perm(usr_sesh.permissions, PermissionOffset.USERMOD) - and user_id != usr_sesh.user_id - ): - self.logger.warn( - f"User {usr_sesh.user_id} does not have permission to view user {user_id}" - ) - return RedirectResponse("/user/", 303) + 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: - user_id = usr_sesh.user_id - - user = await self.data.user.get_user(user_id) + usrid = usr_sesh.userId + + user = self.data.user.get_user(usrid) if user is None: - self.logger.debug(f"User {user_id} not found") - return RedirectResponse("/user/", 303) - - cards = await self.data.card.get_user_cards(user_id) - + 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" + if c['is_locked']: + status = 'Locked' + elif c['is_banned']: + status = 'Banned' else: - status = "Active" + 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']}) - # idm = c['idm'] - ac = c["access_code"] + 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") - if ac.startswith("5"): # or idm is not None: - c_type = "AmusementIC" - elif ac.startswith("3"): - c_type = "Banapass" - elif ac.startswith("010"): - c_type = "Aime" # TODO: Aime verification - elif ac.startswith("0008"): - c_type = "Generated AIC" - else: - c_type = "Unknown" - - card_data.append( - { - "access_code": ac, - "status": status, - "chip_id": "", # None if c['chip_id'] is None else f"{c['chip_id']:X}", - "idm": "", - "type": c_type, - "memo": "", - } - ) - - if "e" in request.query_params: - try: - err = int(request.query_params.get("e", 0)) - except Exception: - err = 0 - - else: - err = 0 - - if "s" in request.query_params: - try: - succ = int(request.query_params.get("s", 0)) - except Exception: - succ = 0 - - else: - succ = 0 - - return Response( - template.render( - title=f"{self.core_config.server.name} | Account", - sesh=vars(usr_sesh), - cards=card_data, - error=err, - success=succ, - username=user["username"], - arcades=arcade_data, - ), - media_type="text/html; charset=utf-8", - ) - - async def render_logout(self, request: Request): - resp = RedirectResponse("/gate/", 303) - resp.delete_cookie("DIANA_SESH") - return resp - - async def edit_card(self, request: Request) -> RedirectResponse: - return RedirectResponse("/user/", 303) - - async def add_card(self, request: Request) -> RedirectResponse: - return RedirectResponse("/user/", 303) - - async def render_POST(self, request: Request): - frm = await request.form() - usr_sesh = self.validate_session(request) - if not usr_sesh or not self.test_perm( - usr_sesh.permissions, PermissionOffset.USERMOD - ): - return RedirectResponse("/gate/", 303) - - old_pw: str = frm.get("current_pw", None) - pw1: str = frm.get("password1", None) - pw2: str = frm.get("password2", None) - - if old_pw is None or pw1 is None or pw2 is None: - return RedirectResponse("/user/?e=4", 303) - - if pw1 != pw2: - return RedirectResponse("/user/?e=6", 303) - - if not await self.data.user.check_password(usr_sesh.user_id, old_pw.encode()): - return RedirectResponse("/user/?e=5", 303) - - if ( - len(pw1) < 10 - or not any(ele.isupper() for ele in pw1) - or not any(ele.islower() for ele in pw1) - or not any(ele.isdigit() for ele in pw1) - or not any(not ele.isalnum() for ele in pw1) - ): - return RedirectResponse("/user/?e=7", 303) - - salt = bcrypt.gensalt() - hashed = bcrypt.hashpw(pw1.encode(), salt) - if not await self.data.user.change_password(usr_sesh.user_id, hashed.decode()): - return RedirectResponse("/gate/?e=1", 303) - - return RedirectResponse("/user/?s=1", 303) - - async def update_username(self, request: Request): - frm = await request.form() - new_name: bytes = frm.get("new_name", "") - usr_sesh = self.validate_session(request) - if not usr_sesh or not self.test_perm( - usr_sesh.permissions, PermissionOffset.USERMOD - ): - return RedirectResponse("/gate/", 303) - - if new_name is None or not new_name: - return RedirectResponse("/user/?e=4", 303) - - if len(new_name) > 10: - return RedirectResponse("/user/?e=8", 303) - - if not await self.data.user.change_username(usr_sesh.user_id, new_name): - return RedirectResponse("/user/?e=8", 303) - - return RedirectResponse("/user/?s=2", 303) + def render_POST(self, request: Request): + pass class FE_System(FE_Base): - async def render_GET(self, request: Request): - template = self.environment.get_template("core/templates/sys/index.jinja") - self.logger.debug(f"{Utils.get_ip_addr(request)} -> {request.url.path}") - - usr_sesh = self.validate_session(request) - if not usr_sesh or not self.test_perm_minimum( - usr_sesh.permissions, PermissionOffset.USERMOD - ): - return RedirectResponse("/gate/", 303) - - return Response( - template.render( - title=f"{self.core_config.server.name} | System", - sesh=vars(usr_sesh), - usrlist=[], - ), - media_type="text/html; charset=utf-8", - ) - - async def lookup_user(self, request: Request): - template = self.environment.get_template("core/templates/sys/index.jinja") + def render_GET(self, request: Request): + uri = request.uri.decode() + template = self.environment.get_template("core/frontend/sys/index.jinja") usrlist: List[Dict] = [] - usr_sesh = self.validate_session(request) - if not usr_sesh or not self.test_perm( - usr_sesh.permissions, PermissionOffset.USERMOD - ): - return RedirectResponse("/gate/", 303) + aclist: List[Dict] = [] + cablist: List[Dict] = [] - uid_search = request.query_params.get("usrId", None) - email_search = request.query_params.get("usrEmail", None) - uname_search = request.query_params.get("usrName", None) + 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: - u = await self.data.user.get_user(uid_search) - if u is not None: - usrlist.append(u._asdict()) + 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: - u = await self.data.user.find_user_by_email(email_search) - 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: - ul = await self.data.user.find_user_by_username(uname_search) - for u in ul: - 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()) - return Response( - template.render( - title=f"{self.core_config.server.name} | System", - sesh=vars(usr_sesh), - usrlist=usrlist, - shoplist=[], - ), - media_type="text/html; charset=utf-8", - ) + 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") - async def lookup_shop(self, request: Request): - shoplist = [] - template = self.environment.get_template("core/templates/sys/index.jinja") + 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()) - usr_sesh = self.validate_session(request) - if not usr_sesh or not self.test_perm( - usr_sesh.permissions, PermissionOffset.ACMOD - ): - return RedirectResponse("/gate/", 303) + 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()) - shopid_search = request.query_params.get("shopId", None) - sn_search = request.query_params.get("serialNum", None) + 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()) - if shopid_search: - if shopid_search.isdigit(): - shopid_search = int(shopid_search) - try: - sinfo = await self.data.arcade.get_arcade(shopid_search) - except Exception as e: - self.logger.error( - f"Failed to fetch shop info for shop {shopid_search} in lookup_shop - {e}" - ) - sinfo = None - if sinfo: - shoplist.append({"name": sinfo["name"], "id": sinfo["id"]}) + 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") - else: - return Response( - template.render( - title=f"{self.core_config.server.name} | System", - sesh=vars(usr_sesh), - usrlist=[], - shoplist=shoplist, - error=4, - ), - media_type="text/html; charset=utf-8", - ) + 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()) - if sn_search: - sn_search = sn_search.upper().replace("-", "").strip() - if sn_search.isdigit() and len(sn_search) == 12: - prefix = sn_search[:4] - suffix = sn_search[5:] + 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()) - netid_prefix = self.environment.globals["sn_cvt"].get(prefix, "") - sn_search = netid_prefix + suffix + 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()) - if re.match(r"^AB[DGL]N\d{7}$", sn_search) or re.match( - r"^A\d{2}[EX]\d{2}[A-Z]\d{4,8}$", sn_search - ): - cabinfo = await self.data.arcade.get_machine(sn_search) - if cabinfo is None: - sinfo = None - else: - sinfo = await self.data.arcade.get_arcade(cabinfo["arcade"]) - if sinfo: - shoplist.append({"name": sinfo["name"], "id": sinfo["id"]}) + return template.render( + title=f"{self.core_config.server.name} | System", + sesh=vars(usr_sesh), + usrlist=usrlist, + aclist=aclist, + cablist=cablist, + ).encode("utf-16") - else: - return Response( - template.render( - title=f"{self.core_config.server.name} | System", - sesh=vars(usr_sesh), - usrlist=[], - shoplist=shoplist, - error=10, - ), - media_type="text/html; charset=utf-8", - ) - return Response( - template.render( - title=f"{self.core_config.server.name} | System", - sesh=vars(usr_sesh), - usrlist=[], - shoplist=shoplist, - ), - media_type="text/html; charset=utf-8", - ) +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): - async def render_GET(self, request: Request): - template = self.environment.get_template("core/templates/arcade/index.jinja") - shop_id = request.path_params.get("shop_id", None) + 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) - usr_sesh = self.validate_session(request) - if not usr_sesh or not self.test_perm( - usr_sesh.permissions, PermissionOffset.ACMOD - ): - self.logger.warn( - f"User {usr_sesh.user_id} does not have permission to view shops!" - ) - return RedirectResponse("/gate/", 303) - - if not shop_id: - return Response( - template.render( - title=f"{self.core_config.server.name} | Arcade", - sesh=vars(usr_sesh), - ), - media_type="text/html; charset=utf-8", - ) - - sinfo = await self.data.arcade.get_arcade(shop_id) - if not sinfo: - return Response( - template.render( - title=f"{self.core_config.server.name} | Arcade", - sesh=vars(usr_sesh), - ), - media_type="text/html; charset=utf-8", - ) - - cabs = await self.data.arcade.get_arcade_machines(shop_id) - cablst = [] - if cabs: - for x in cabs: - cablst.append( - { - "id": x["id"], - "serial": x["serial"], - "game": x["game"], - } - ) - - return Response( - template.render( - title=f"{self.core_config.server.name} | Arcade", - sesh=vars(usr_sesh), - arcade={"name": sinfo["name"], "id": sinfo["id"], "cabs": cablst}, - ), - media_type="text/html; charset=utf-8", - ) + 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): - async def render_GET(self, request: Request): - template = self.environment.get_template("core/templates/machine/index.jinja") - cab_id = request.path_params.get("cab_id", None) - - usr_sesh = self.validate_session(request) - if not usr_sesh or not self.test_perm( - usr_sesh.permissions, PermissionOffset.ACMOD - ): - self.logger.warn( - f"User {usr_sesh.user_id} does not have permission to view shops!" - ) - return RedirectResponse("/gate/", 303) - - if not cab_id: - return Response( - template.render( - title=f"{self.core_config.server.name} | Machine", - sesh=vars(usr_sesh), - ), - media_type="text/html; charset=utf-8", - ) - - return Response( - template.render( - title=f"{self.core_config.server.name} | Machine", - sesh=vars(usr_sesh), - arcade={}, - ), - media_type="text/html; charset=utf-8", - ) - - -cfg_dir = environ.get("DIANA_CFG_DIR", "config") -cfg: CoreConfig = CoreConfig() -if path.exists(f"{cfg_dir}/core.yaml"): - cfg.update(yaml.safe_load(open(f"{cfg_dir}/core.yaml"))) - -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) - -fe = FrontendServlet(cfg, cfg_dir) -app = Starlette(cfg.server.is_develop, fe.get_routes(), on_startup=[fe.startup]) + 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/mucha.py b/core/mucha.py index e146486..7c6f0ab 100644 --- a/core/mucha.py +++ b/core/mucha.py @@ -1,24 +1,18 @@ -import logging -from datetime import datetime +from typing import Dict, Any, Optional, List +import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler -from typing import Any, Dict, Optional - -import coloredlogs -import pytz +from twisted.web import resource +from twisted.web.http import Request +from datetime import datetime from Crypto.Cipher import Blowfish -from starlette.requests import Request -from starlette.responses import PlainTextResponse +import pytz from .config import CoreConfig -from .const import * -from .data import Data -from .title import TitleServlet from .utils import Utils - +from .title import TitleServlet class MuchaServlet: - mucha_registry: Dict[str, Dict[str, str]] = {} - + mucha_registry: List[str] = [] def __init__(self, cfg: CoreConfig, cfg_dir: str) -> None: self.config = cfg self.config_dir = cfg_dir @@ -41,174 +35,91 @@ class MuchaServlet: self.logger.addHandler(consoleHandler) self.logger.setLevel(cfg.mucha.loglevel) - coloredlogs.install( - level=cfg.mucha.loglevel, logger=self.logger, fmt=log_fmt_str - ) - - self.data = Data(cfg) + coloredlogs.install(level=cfg.mucha.loglevel, logger=self.logger, fmt=log_fmt_str) for _, mod in TitleServlet.title_registry.items(): - enabled, game_cds, netids = mod.get_mucha_info(self.config, self.config_dir) - if enabled: - for x in range(len(game_cds)): - self.mucha_registry[game_cds[x]] = {"netid_prefix": netids[x]} + 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") - async def handle_boardauth(self, request: Request) -> bytes: - bod = await request.body() - req_dict = self.mucha_preprocess(bod) + 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 {bod}") - return PlainTextResponse("RESULTS=000") + self.logger.error( + f"Error processing mucha request {request.content.getvalue()}" + ) + return b"RESULTS=000" req = MuchaAuthRequest(req_dict) - self.logger.debug(f"Mucha request {vars(req)}") - - if ( - not req.gameCd - or not req.gameVer - or not req.sendDate - or not req.countryCd - or not req.serialNum - ): - self.logger.warn(f"Missing required fields - {vars(req)}") - return PlainTextResponse("RESULTS=000") - - minfo = self.mucha_registry.get(req.gameCd, {}) - - if not minfo: - self.logger.warning(f"Unknown gameCd {req.gameCd} from {client_ip}") - return PlainTextResponse("RESULTS=000") - - b_key = b"" - for x in range(8): - b_key += req.sendDate[(x - 1) & 7].encode() - - b_iv = b_key # what the fuck namco - - cipher = Blowfish.new(b_key, Blowfish.MODE_CBC, b_iv) - try: - sn_decrypt = cipher.decrypt(bytes.fromhex(req.serialNum))[:12].decode() - except Exception as e: - self.logger.error(f"Decrypt SN {req.serialNum} failed! - {e}") - return PlainTextResponse("RESULTS=000") - - self.logger.info( - f"Boardauth request from {sn_decrypt} ({client_ip}) for {req.gameVer}" - ) - - resp = MuchaAuthResponse( - f"{self.config.server.hostname}{':' + str(self.config.server.port) if not self.config.server.is_using_proxy else ''}" - ) - - netid = minfo.get("netid_prefix", "ABxN") + sn_decrypt[5:] - - cab = await self.data.arcade.get_machine(netid) - if cab: - arcade = await self.data.arcade.get_arcade(cab["id"]) - if not arcade: - self.logger.error(f"Failed to get arcade with id {cab['id']}") - return PlainTextResponse("RESULTS=000") - - resp.AREA_0 = arcade["region_id"] or AllnetJapanRegionId.AICHI.name - resp.AREA_0_EN = arcade["region_id"] or AllnetJapanRegionId.AICHI.name - resp.AREA_FULL_0 = arcade["region_id"] or AllnetJapanRegionId.AICHI.name - resp.AREA_FULL_0_EN = arcade["region_id"] or AllnetJapanRegionId.AICHI.name - - resp.AREA_1 = ( - arcade["country"] or cab["country"] or AllnetCountryCode.JAPAN.value - ) - resp.AREA_1_EN = ( - arcade["country"] or cab["country"] or AllnetCountryCode.JAPAN.value - ) - resp.AREA_FULL_1 = ( - arcade["country"] or cab["country"] or AllnetCountryCode.JAPAN.value - ) - resp.AREA_FULL_1_EN = ( - arcade["country"] or cab["country"] or AllnetCountryCode.JAPAN.value - ) - - resp.AREA_2 = arcade["city"] if arcade["city"] else "" - resp.AREA_2_EN = arcade["city"] if arcade["city"] else "" - resp.AREA_FULL_2 = arcade["city"] if arcade["city"] else "" - resp.AREA_FULL_2_EN = arcade["city"] if arcade["city"] else "" - - resp.AREA_3 = "" - resp.AREA_3_EN = "" - resp.AREA_FULL_3 = "" - resp.AREA_FULL_3_EN = "" - - resp.PREFECTURE_ID = arcade["region_id"] - resp.COUNTRY_CD = ( - arcade["country"] or cab["country"] or AllnetCountryCode.JAPAN.value - ) - resp.PLACE_ID = ( - req.placeId - if req.placeId - else f"{arcade['country'] or cab['country'] or AllnetCountryCode.JAPAN.value}{arcade['id']:04X}" - ) - resp.SHOP_NAME = arcade["name"] - resp.SHOP_NAME_EN = arcade["name"] - resp.SHOP_NICKNAME = arcade["nickname"] - resp.SHOP_NICKNAME_EN = arcade["nickname"] - - elif self.config.server.allow_unregistered_serials: - self.logger.info(f"Allow unknown serial {netid} ({sn_decrypt}) to auth") - - else: - self.logger.warn(f"Auth failed for NetID {netid}") - return PlainTextResponse("RESULTS=000") - - self.logger.debug(f"Mucha response {vars(resp)}") - - return PlainTextResponse(self.mucha_postprocess(vars(resp))) - - async def handle_updatecheck(self, request: Request) -> bytes: - bod = await request.body() - req_dict = self.mucha_preprocess(bod) - client_ip = Utils.get_ip_addr(request) - - if req_dict is None: - self.logger.error(f"Error processing mucha request {bod}") - return PlainTextResponse("RESULTS=000") - - req = MuchaUpdateRequest(req_dict) - self.logger.info( - f"Updatecheck request from {req.serialNum} ({client_ip}) for {req.gameVer}" - ) + 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 PlainTextResponse("RESULTS=000") + return b"RESULTS=000" - resp = MuchaUpdateResponse( - req.gameVer, - f"{self.config.server.hostname}{':' + str(self.config.server.port) if not self.config.server.is_using_proxy else ''}", + # 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 PlainTextResponse(self.mucha_postprocess(vars(resp))) + return self.mucha_postprocess(vars(resp)) - async def handle_dlstate(self, request: Request) -> bytes: - bod = await request.body() - req_dict = self.mucha_preprocess(bod) + 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 {bod}") - return PlainTextResponse("RESULTS=000") + self.logger.error( + f"Error processing mucha request {request.content.getvalue()}" + ) + return b"RESULTS=000" + req = MuchaUpdateRequest(req_dict) + 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 {req.serialNum} ({client_ip}) for {req.gameCd} -> {req.updateVer}" - ) + self.logger.info(f"DownloadState request from {client_ip} for {req.gameCd} -> {req.updateVer}") self.logger.debug(f"request {vars(req)}") - return PlainTextResponse("RESULTS=001") + return b"RESULTS=001" def mucha_preprocess(self, data: bytes) -> Optional[Dict]: try: @@ -258,7 +169,7 @@ class MuchaAuthResponse: self.RESULTS = "001" self.AUTH_INTERVAL = "86400" self.SERVER_TIME = datetime.strftime(datetime.now(), "%Y%m%d%H%M") - self.SERVER_TIME_UTC = datetime.strftime(datetime.now(pytz.UTC), "%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.FILE_URL = f"https://{mucha_url}/file/" @@ -310,7 +221,7 @@ class MuchaUpdateRequest: class MuchaUpdateResponse: def __init__(self, game_ver: str, mucha_url: str) -> None: - self.RESULTS = "001" + self.RESULTS = "001" self.EXE_VER = game_ver self.UPDATE_VER_1 = game_ver @@ -329,7 +240,6 @@ class MuchaUpdateResponse: self.USER_ID = "" self.PASSWORD = "" - """ RESULTS EXE_VER @@ -350,16 +260,13 @@ 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: + def __init__(self, request: Dict) -> None: self.gameCd = request.get("gameCd", "") self.updateVer = request.get("updateVer", "") self.serialNum = request.get("serialNum", "") @@ -369,9 +276,8 @@ class MuchaDownloadStateRequest: self.placeId = request.get("placeId", "") self.storeRouterIp = request.get("storeRouterIp", "") - class MuchaDownloadErrorRequest: - def __init__(self, request: Dict) -> None: + def __init__(self, request: Dict) -> None: self.gameCd = request.get("gameCd", "") self.updateVer = request.get("updateVer", "") self.serialNum = request.get("serialNum", "") @@ -382,11 +288,10 @@ class MuchaDownloadErrorRequest: self.placeId = request.get("placeId", "") self.storeRouterIp = request.get("storeRouterIp", "") - class MuchaRegiAuthRequest: - def __init__(self, request: Dict) -> None: + def __init__(self, request: Dict) -> None: self.gameCd = request.get("gameCd", "") - self.serialNum = request.get("serialNum", "") # Encrypted + self.serialNum = request.get("serialNum", "") # Encrypted self.countryCd = request.get("countryCd", "") self.registrationCd = request.get("registrationCd", "") self.sendDate = request.get("sendDate", "") @@ -395,16 +300,14 @@ class MuchaRegiAuthRequest: 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 - + 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: + def __init__(self, request: Dict) -> None: self.gameCd = request.get("gameCd", "") self.serialNum = request.get("serialNum", "") self.countryCd = request.get("countryCd", "") @@ -413,14 +316,12 @@ class MuchaTokenStateRequest: 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: + def __init__(self, request: Dict) -> None: self.gameCd = request.get("gameCd", "") self.serialNum = request.get("serialNum", "") self.countryCd = request.get("countryCd", "") @@ -429,7 +330,6 @@ class MuchaTokenMarginStateRequest: self.limitUpperToken = request.get("limitUpperToken", 0) self.settlementMonth = request.get("settlementMonth", 0) - class MuchaTokenMarginStateResponse: def __init__(self) -> None: self.RESULTS = "001" diff --git a/core/templates/arcade/index.jinja b/core/templates/arcade/index.jinja deleted file mode 100644 index 1de4301..0000000 --- a/core/templates/arcade/index.jinja +++ /dev/null @@ -1,19 +0,0 @@ -{% extends "core/templates/index.jinja" %} -{% block content %} -{% if arcade is defined %} -

{{ arcade.name }}

-

PCBs assigned to this arcade

-{% if success is defined and success == 3 %} -
-Cab added successfully -
-{% endif %} - -{% else %} -

Arcade Not Found

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

Create User

-
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-

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

Gate

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

{{ server_name }}

- {% endblock content %} - - \ No newline at end of file diff --git a/core/templates/machine/index.jinja b/core/templates/machine/index.jinja deleted file mode 100644 index 3e122f3..0000000 --- a/core/templates/machine/index.jinja +++ /dev/null @@ -1,4 +0,0 @@ -{% extends "core/templates/index.jinja" %} -{% block content %} -

Machine Management

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

System Management

-{% if error is defined %} -{% include "core/templates/widgets/err_banner.jinja" %} -{% endif %} -
- {% if "{:08b}".format(sesh.permissions)[6] == "1" %} -
-
-

User Search

-
- - -
- OR -
- - -
- OR -
- - -
-
- -
-
- {% endif %} - {% if "{:08b}".format(sesh.permissions)[5] == "1" %} -
-
-

Shop search

-
- - -
- OR -
- - -
-
- -
-
- {% endif %} -
-
- {% if "{:08b}".format(sesh.permissions)[6] == "1" %} -
- {% for usr in usrlist %} -
{{ usr.username if usr.username is not none else "No Name Set"}}
- {% endfor %} -
- {% endif %} - {% if "{:08b}".format(sesh.permissions)[5] == "1" %} -
- {% for shop in shoplist %} -
{{ shop.name if shop.name else "No Name Set"}}
- {% endfor %} -
- {% endif %} -
-
- -
-{% endblock content %} \ No newline at end of file diff --git a/core/templates/user/index.jinja b/core/templates/user/index.jinja deleted file mode 100644 index 1b6ec1d..0000000 --- a/core/templates/user/index.jinja +++ /dev/null @@ -1,175 +0,0 @@ -{% extends "core/templates/index.jinja" %} -{% block content %} - -

Management for {{ username }} 

-{% if error is defined %} -{% include "core/templates/widgets/err_banner.jinja" %} -{% endif %} -{% if success is defined and success == 2 %} -
-Update successful -
-{% endif %} - -

-

Cards

-{% if success is defined and success == 3 %} -
-Card added successfully -
-{% endif %} - -
    -{% for c in cards %} -
  • {{ c.access_code }} ({{ c.type}}): {{ c.status }}  {% if c.status == 'Active'%}{% elif c.status == 'Locked' %}{% endif %} 
  • -{% endfor %} -
- -

Reset Password

-{% if success is defined and success == 1 %} -
-Update successful -
-{% endif %} -
-
- - -
-
- - -
Password must be at least 10 characters long, contain an upper and lowercase character, number, and special character
-
-
- - -
- -
- -{% if arcades is defined and arcades|length > 0 %} -

Arcades

-
    - {% for a in arcades %} -
  • {{ a.name }}

    - {% if a.machines|length > 0 %} - - - {% for m in a.machines %} - - {% endfor %} -
    SerialGameLast Seen
    {{ m.serial }}{{ m.game }}{{ m.last_seen }}
    - {% endif %} -
  • - {% endfor %} -
-{% endif %} - - - -{% endblock content %} \ No newline at end of file diff --git a/core/templates/widgets/err_banner.jinja b/core/templates/widgets/err_banner.jinja deleted file mode 100644 index eec204a..0000000 --- a/core/templates/widgets/err_banner.jinja +++ /dev/null @@ -1,28 +0,0 @@ -{% 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 %} -Required field not filled or invalid -{% elif error == 5 %} -Incorrect old password -{% elif error == 6 %} -Passwords don't match -{% elif error == 7 %} -New password not acceptable -{% elif error == 8 %} -New Nickname too long -{% elif error == 9 %} -You must be logged in to preform this action -{% elif error == 10 %} -Invalid serial number -{% else %} -An unknown error occoured -{% endif %} -
-{% endif %} \ No newline at end of file diff --git a/core/templates/widgets/topbar.jinja b/core/templates/widgets/topbar.jinja deleted file mode 100644 index 332173d..0000000 --- a/core/templates/widgets/topbar.jinja +++ /dev/null @@ -1,22 +0,0 @@ -
- Navigation -
-
-   - {% for game, data in game_list|items %} -   - {% endfor %} -
- -
- {% if sesh is defined and sesh["permissions"] >= 2 %} - - {% endif %} - {% if sesh is defined and sesh["user_id"] > 0 %} - - - {% else %} - - {% endif %} - -
\ No newline at end of file diff --git a/core/title.py b/core/title.py index c162664..3fdb30c 100644 --- a/core/title.py +++ b/core/title.py @@ -1,28 +1,12 @@ -import json -import logging +from typing import Dict, List, Tuple +import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler -from typing import Any, Dict, List, Tuple - -import coloredlogs -from starlette.requests import Request -from starlette.responses import Response -from starlette.routing import Route +from twisted.web.http import Request from core.config import CoreConfig from core.data import Data from core.utils import Utils - -class JSONResponseNoASCII(Response): - media_type = "application/json" - - def render(self, content: Any) -> bytes: - return json.dumps( - content, - ensure_ascii=False, - ).encode("utf-8") - - class BaseServlet: def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: self.core_cfg = core_cfg @@ -30,9 +14,7 @@ class BaseServlet: self.logger = logging.getLogger("title") @classmethod - def is_game_enabled( - cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str - ) -> bool: + 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: @@ -42,28 +24,29 @@ class BaseServlet: Returns: bool: True if the game is enabled and set to run, False otherwise - + """ return False - - def get_routes(self) -> List[Route]: + + 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: - List[Route]: A list of Routes, WebSocketRoutes, or similar classes + 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 [ - Route("/{game}/{version}/{endpoint}", self.render_POST, methods=["POST"]), - Route("/{game}/{version}/{endpoint}", self.render_GET, methods=["GET"]), - ] - + 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""" + """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]: + 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: @@ -74,20 +57,12 @@ class BaseServlet: 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.server.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_code}/{game_ver}/", - "", - ) + 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.server.hostname}/{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, List[str], List[str]]: + 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: @@ -97,20 +72,18 @@ class BaseServlet: 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, [], []) + return (False, "") - async def render_POST(self, request: Request) -> bytes: - self.logger.warn(f"Game Does not dispatch POST") - return Response() - - async def render_GET(self, request: Request) -> bytes: - self.logger.warn(f"Game Does not dispatch GET") - return Response() + 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 @@ -144,19 +117,13 @@ class TitleServlet: plugins = Utils.get_all_titles() for folder, mod in plugins.items(): - if ( - hasattr(mod, "game_codes") - and hasattr(mod, "index") - and hasattr(mod.index, "is_game_enabled") - ): + 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 game_codes: - if game_servlet.is_game_enabled( - code, self.config, self.config_folder - ): + 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: @@ -166,18 +133,16 @@ class TitleServlet: self.title_registry[code] = handler_cls else: - self.logger.error( - f"{folder} missing game_code or index in __init__.py, or is_game_enabled in index" - ) + 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.server.port) if core_cfg.server.port > 0 else ''}" + 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["title"] - subaction = endpoints["subaction"] - + subaction = endpoints['subaction'] + if code not in self.title_registry: self.logger.warning(f"Unknown game code {code}") request.setResponseCode(404) @@ -186,9 +151,7 @@ class TitleServlet: 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}" - ) + self.logger.error(f"{code} does not have handler for GET subaction {subaction}") request.setResponseCode(500) return b"" @@ -196,7 +159,7 @@ class TitleServlet: def render_POST(self, request: Request, endpoints: dict) -> bytes: code = endpoints["title"] - subaction = endpoints["subaction"] + subaction = endpoints['subaction'] if code not in self.title_registry: self.logger.warning(f"Unknown game code {code}") @@ -206,9 +169,7 @@ class TitleServlet: 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}" - ) + self.logger.error(f"{code} does not have handler for POST subaction {subaction}") request.setResponseCode(500) return b"" diff --git a/core/utils.py b/core/utils.py index 7099e16..8264213 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,21 +1,18 @@ -import importlib +from typing import Dict, Any, Optional +from types import ModuleType +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 os import walk -from types import ModuleType -from typing import Any, Dict, Optional - -import jwt -from starlette.requests import Request 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] = {} @@ -37,57 +34,40 @@ class Utils: @classmethod def get_ip_addr(cls, req: Request) -> str: - return req.headers.get("x-forwarded-for", req.client.host) - + 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 - - cls.real_title_port = ( - cfg.server.proxy_port - if cfg.server.is_using_proxy and cfg.server.proxy_port - else cfg.server.port - ) + 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 - - cls.real_title_port_ssl = ( - cfg.server.proxy_port_ssl - if cfg.server.is_using_proxy and cfg.server.proxy_port_ssl - else Utils.get_title_port(cfg) - ) + 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]: +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", - ) + 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 @@ -95,19 +75,10 @@ def create_sega_auth_key( 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]: +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}, - ) + 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 @@ -123,3 +94,4 @@ def decode_sega_auth_key( 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 541b4d3..85b18a0 100644 --- a/dbutils.py +++ b/dbutils.py @@ -1,12 +1,9 @@ -#!/usr/bin/env python3 -import argparse -import asyncio -import logging -from os import W_OK, access, mkdir, path - 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__": parser = argparse.ArgumentParser(description="Database utilities") @@ -19,24 +16,19 @@ if __name__ == "__main__": 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( - "--access_code", - "-a", - type=str, - help="Access code for new/transfer user", - default="00000000000000000000", + "action", type=str, help="DB Action, create, recreate, upgrade, or rollback" ) - parser.add_argument("--message", "-m", type=str, help="Revision message") -<<<<<<< Updated upstream - parser.add_argument("action", type=str, help="create, upgrade, downgrade, create-owner, migrate, create-revision, create-autorevision") -======= - parser.add_argument( - "action", - type=str, - help="create, upgrade, create-owner, migrate, create-revision", - ) ->>>>>>> Stashed changes args = parser.parse_args() cfg = CoreConfig() @@ -59,32 +51,41 @@ if __name__ == "__main__": if args.action == "create": data.create_database() - elif args.action == "upgrade": - data.schema_upgrade(args.version) + elif args.action == "recreate": + data.recreate_database() - elif args.action == "downgrade": - if not args.version: - logging.getLogger("database").error( - f"Version argument required for downgrade" + elif args.action == "upgrade" or args.action == "rollback": + if args.version is None: + data.logger.warning("No version set, upgrading to latest") + + if args.game is None: + 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, ) - exit(1) - data.schema_downgrade(args.version) + + else: + 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": - loop = asyncio.get_event_loop() - loop.run_until_complete(data.create_owner(args.email, args.access_code)) + data.create_owner(args.email) - elif args.action == "migrate": - loop = asyncio.get_event_loop() - loop.run_until_complete(data.migrate()) + elif args.action == "migrate-card": + data.migrate_card(args.old_ac, args.new_ac, args.force) - elif args.action == "create-revision": - loop = asyncio.get_event_loop() - loop.run_until_complete(data.create_revision(args.message)) + elif args.action == "cleanup": + data.delete_hanging_users() + + elif args.action == "version": + data.show_versions() - elif args.action == "create-autorevision": - loop = asyncio.get_event_loop() - loop.run_until_complete(data.create_revision_auto(args.message)) - - else: - logging.getLogger("database").info(f"Unknown action {args.action}") + data.logger.info("Done") diff --git a/docs/INSTALL_LINUX.md b/docs/INSTALL_LINUX.md deleted file mode 100644 index 08054e1..0000000 --- a/docs/INSTALL_LINUX.md +++ /dev/null @@ -1,107 +0,0 @@ -# Installing ARTEMiS on Linux -This guide assumes a fresh install of Debian 12 or Rasperry Pi OS. If you're using a different distrubution, your package manager commands and package names may be different then what's listed below. Please check with your repository's package manager for package names. - -## Install prerequisits -### Python -Some installs may come with python already installed. You can verify this by trying the following commands: -- `python --version` -- `python3 --version` -- `python3. --version` where `` is a python 3 release (eg 11, 10) - -If your python version is at least 3.7, you can move to the next step - -### Libraries and other software -ARTEMiS depends on mysql and memcached. As stated above, package names may vary by distrubution, but this is generally what you should expect to install. -#### Rasperry Pi OS -`sudo apt install git mariadb-server python3-pip memcached libmemcached-dev ` - -#### Debian 12 -`sudo apt install git mariadb-server python3-pip memcached libmemcached-dev default-libmysqlclient-dev pkg-config` - -### Optional: Install proxy -If you intend to use a proxy (recomended for public-facing production setups), we recomend nginx -`sudo apt install nginx` - -## Database setup -### mysql_secure_installation -If you already have your database installed and configured, and are able to log in, skip down to the [Creating the database](#creating-the-database) section below. Otherwise, setup your newly installed database. - -`sudo mysql_secure_installation` - -Leave the root password blank, do not switch to unix socket, do reset the root password to something secure, and answer yes to the rest of the prompts. You can then log into your database with `sudo mysql` - -### Creating the database -Once you're logged in, run the following commands, as root, to set up our database. Make sure you note down whatever you decide to make the password for the aime account, as you will need it to configure artemis. - -```sql -CREATE USER 'aime'@'localhost' IDENTIFIED BY ''; -CREATE DATABASE aime; -GRANT Alter,Create,Delete,Drop,Index,Insert,References,Select,Update ON aime.* TO 'aime'@'localhost'; -quit -``` -We have now set up our new user, `aime`, created a database called `aime` and given our user all the permissions it needs on every table of that database. - -### Configure memcached -Under the file /etc/memcached.conf, please make sure the following parameters are set: - -``` -# Start with a cap of 64 megs of memory. It's reasonable, and the daemon default -# Note that the daemon will grow to this size, but does not start out holding this much -# memory - --I 128m --m 1024 -``` - -** This is mandatory to avoid memcached overload caused by Crossbeats or by massive profiles - -Restart memcached using: sudo systemctl restart memcached - -## Getting ARTEMiS -### Clone from gitea -use `git clone https://gitea.tendokyu.moe/Hay1tsme/artemis.git` to pull down ARTEMiS into a folder called `artemis` created at wherever your current working directory is. `cd` into `artemis`. - -### Optional: Create a venv -Python venvs are a way to install and manage packages on a per-project basis and are recomended on systems that will have multiple python scripts running on them to avoid dependancy issues. If this server will be running ARTEMiS and ONLY ARTEMiS, then it is possible to get away without creating one. If you do want to create one, you will have to install an additional package: - -`sudo apt install python3-venv` (like above, package name may vary depending on distro and python version) - -Now, simply run `python -m venv .venv` (may have to use python3 or python 3.11 instead of python) to create your virtual environment in the folder `.venv`. In order to install packages and run scripts in this environment, you have to 'activate' it by running `source .venv/bin/activate`. Your terminal should now have (venv) appended to it. - -### Optional: Use the develop branch -By default, pulling down ARTEMiS from gitea will pull the `master` branch. This branch is updated less frequently, but is considered stable and ready for production use. If you'd rather have more updates, but a possibility for instability or bugs, you can switch to the develop branch by running `git checkout develop`. You can run `git checkout master` to switch back to stable. - -## Install python libraries -Run `pip install -r requirements.txt` to install all of ARTEMiS' dependencies. If any installs fail, you may have missed a step in the [Install prerequisits](#install-prerequisits) section above. If you're absolutly sure you didn't, submit an issue on gitea. - -## Configuration -### Copy example configs -From the `artemis` directory, run `cp -r example_config config` to copy the example configuration files to a new folder called `config`. All of the config changes you make will be done in the `config` folder. - -### Optional: Generate AimeDB and Frontend JWT Secrets -AimeDB and the frontend utalize JSON Web Tokens (JWT) for card authentication and session cookies respectivly. While generating a secret for AimeDB is optional, if you intend to run the frontend, a secret is required. You can generate a secret easily by running: - -`openssl rand --base64 64` - -With 64 being the number of bytes. You shouldn't need to go higher then 64, but you can if desired. **NOTE: When pasting secrets into the config file, make sure you remove any newlines!** - -### Edit `core.yaml` -Before editing `core.yaml`, you should familiarize yourself with the name and function of each of the config options. You can find a full list in [config.md](config.md) - -Open `core.yaml` in the `config` folder in your prefered text editor. The only configuration option that it is absolutly mandatory to change is `aimedb`->`key`. This key must be set for the server to start, and the key must be correct, otherwise you will not be able to process aimedb requests. The correct key is floating around online, and finding it is left as an excersie to the reader. - -Another option that should be changed is `database`->`password` to be the password you set when you created your database user. You did write it down somewhere, right? - -Since you are presumably not running the games on the same computer you're installing this server on, you're going to want to change `server`->`hostname` to be whatever hostname or IP address other PCs can reach this server by. Note that some games reject IPs and require hostnames, so setting a hostname is always recomended over an IP. - -### Edit game configs -Every game has their own yaml file with settings that you may want to tweek. `InitialD Zero` and `Pokken` both have `hostname` fields in their config file that you should edit, and some games support encryption, if supplied with proper keys. - -### A note about IDZ -InitialD Zero is currently the only game where it is required to specify encryption information (the AES key and at least one RSA key) for the game to start. These keys are, like the aimedb key, floating around online and will not be provided. If you don't have the keys, and don't plan on anybody connecting to your server playing InitialD Zero, it's best to set `enabled` to `False` in idz.yaml to disable the game. - -## Create database tables -ARTEMiS uses alembic to manage datbase versioning. `dbutils.py` acts as a wrapper for alembic, and can execute some necessassary database functions. To create the database tables, run `python dbutils.py create`. Confirm that there are no errors, and you're good to go. If you intend to use the frontend, you may also want to run `python dbutils.py create-owner -a ` to create a superuser account to log in with. - -## Run ARTEMiS -Once you have everything configured properly, simply run `python index.py` to start ARTEMiS. Verify that clients can connect to all services (allnet, billing, aimedb, and game servers) and setup is complete. \ No newline at end of file diff --git a/docs/INSTALL_UBUNTU_old.md b/docs/INSTALL_UBUNTU_old.md deleted file mode 100644 index 710c757..0000000 --- a/docs/INSTALL_UBUNTU_old.md +++ /dev/null @@ -1,129 +0,0 @@ -# ARTEMiS - Ubuntu 20.04 LTS Guide -This step-by-step guide assumes that you are using a fresh install of Ubuntu 20.04 LTS, some of the steps can be skipped if you already have an installation with MySQL 5.7 or even some of the modules already present on your environment - -# Setup -## Install memcached module -1. sudo apt-get install memcached -2. Under the file /etc/memcached.conf, please make sure the following parameters are set: - -``` -# Start with a cap of 64 megs of memory. It's reasonable, and the daemon default -# Note that the daemon will grow to this size, but does not start out holding this much -# memory - --I 128m --m 1024 -``` - -** This is mandatory to avoid memcached overload caused by Crossbeats or by massive profiles - -3. Restart memcached using: sudo systemctl restart memcached - -## Install MySQL 5.7 -``` -sudo apt update -sudo apt install wget -y -wget https://dev.mysql.com/get/mysql-apt-config_0.8.12-1_all.deb -sudo dpkg -i mysql-apt-config_0.8.12-1_all.deb -``` - 1. During the first prompt, select Ubuntu Bionic - 2. Select the default option - 3. Select MySQL 5.7 - 4. Select the last option -``` -sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 467B942D3A79BD29 -sudo apt-get update -sudo apt-cache policy mysql-server -sudo apt install -f mysql-client=5.7* mysql-community-server=5.7* mysql-server=5.7* -``` - -## Default Configuration for MySQL Server -1. sudo mysql_secure_installation -> Make sure to follow the steps that will be prompted such as changing the mysql root password and such - -2. Test your MySQL Server login by doing the following command : -> mysql -u root -p - -## Create the default ARTEMiS database and user -1. mysql -u root -p -2. Please change the password indicated in the next line for a custom secure one and continue with the next commands - -``` -CREATE USER 'aime'@'localhost' IDENTIFIED BY 'MyStrongPass.'; -CREATE DATABASE aime; -GRANT Alter,Create,Delete,Drop,Index,Insert,References,Select,Update ON aime.* TO 'aime'@'localhost'; -FLUSH PRIVILEGES; -exit; -``` - -3. sudo systemctl restart mysql - -## Install Python modules -``` -sudo apt-get install python3-dev default-libmysqlclient-dev build-essential mysql-client libmysqlclient-dev libmemcached-dev -sudo apt install libpython3.8-dev -sudo apt-get install python3-software-properties -sudo apt install python3-pip -sudo pip3 install --upgrade pip testresources -sudo pip3 install --upgrade pip setuptools -sudo apt-get install python3-tk -``` -7. Change your work path to the ARTEMiS root folder using 'cd' and install the requirements: -> sudo python3 -m pip install -r requirements.txt - -## Copy/Rename the folder example_config to config - -## Adjust /config/core.yaml -1. Make sure to change the server listen_address to be set to your local machine IP (ex.: 192.168.1.xxx) -2. Adjust the proper MySQL information you created earlier -3. Add the AimeDB key at the bottom of the file - -## Create the database tables for ARTEMiS -1. sudo python3 dbutils.py create - -2. If you get "No module named Crypto", run the following command: -``` -sudo pip uninstall crypto -sudo pip uninstall pycrypto -sudo pip install pycrypto -``` - -## Firewall Adjustements -``` -sudo ufw allow 80 -sudo ufw allow 443 -sudo ufw allow 8443 -sudo ufw allow 22345 -sudo ufw allow 8090 -sudo ufw allow 8444 -sudo ufw allow 8080 -``` - -## Running the ARTEMiS instance -1. sudo python3 index.py - -# Troubleshooting - -## Game does not connect to ARTEMiS Allnet server -1. Double-check your core.yaml, the listen_address is most likely either not binded to the proper IP or the port is not opened - -## Game does not connect to Title Server -1. Verify that your core.yaml is setup properly for both the server listen_address and title hostname -2. Boot your game and verify that an AllNet response does show and if it does, attempt to open the URI that is shown under a browser such as Edge, Chrome & Firefox. -3. If a page is shown, the server is working properly and if it doesn't, double check your port forwarding and also that you have entered the proper local IP under the Title hostname in core.yaml. - -## Unhandled command under AimeDB -1. Double check your AimeDB key under core.yaml, it is incorrect. - -## Memcache failed, error 3 -1. Make sure memcached is properly installed and running. You can check the status of the service using the following command: -> sudo systemctl status memcached -2. If it is failing, double check the /etc/memcached.conf file, it may have duplicated arguments like the -I and -m -3. If it is still not working afterward, you can proceed with a workaround by manually editing the /core/data/cache.py file. -``` -# Make memcache optional -try: - has_mc = False -except ModuleNotFoundError: - has_mc = False -``` diff --git a/docs/INSTALL_WINDOWS.md b/docs/INSTALL_WINDOWS.md index 1cbe140..b976b26 100644 --- a/docs/INSTALL_WINDOWS.md +++ b/docs/INSTALL_WINDOWS.md @@ -1,77 +1,102 @@ -# Installing ARTEMiS on Windows -This guide assumes a fresh install of Windows 10. Please be aware that due to the lack of memcached and the general woes of running a server on Windows, this is only recommended for local setups or small hosting-for-the-homies type servers. +# ARTEMiS - Windows 10/11 Guide +This step-by-step guide assumes that you are using a fresh install of Windows 10/11 without MySQL installed, some of the steps can be skipped if you already have an installation with MySQL 8.0 or even some of the modules already present on your environment -## Install prerequisites -### Python -- Python versions from 3.8 to 3.11 work with ARTEMiS. We recommend 3.11. - - https://www.python.org/ftp/python/3.11.7/python-3.11.7-amd64.exe -- Install using whichever options best suit your environment, making sure that the Python executable is on path, such that you can open CMD, type `python --version` and see the version of Python you have installed. -- If you already have a working version of Python installed, skip this step. +# Setup +## Install Python Python 3.9 (recommended) or 3.10 +1. Download Python 3.9 : [Link](https://www.python.org/ftp/python/3.9.13/python-3.9.13-amd64.exe) +2. Install python-3.9.13-amd64.exe + 1. Select Customize installation + 2. Make sure that pip, tcl/tk, and the for all users are checked and hit Next + 3. Make sure that you enable "Create shortcuts for installed applications" and "Add Python to environment variables" and hit Install -### MariaDB -- It is always recommended to use MariaDB over MySQL because Oracle is a terrible company. -- While the latest release of v10 is recommended, as it is an LTS release, v11 should work fine. - - https://ftp.osuosl.org/pub/mariadb//mariadb-10.11.6/winx64-packages/mariadb-10.11.6-winx64.msi -- REMEMBER YOUR ROOT PASSWORD SO YOU CAN LOG IN IN FUTURE STEPS. +## Install MySQL 8.0 +1. Download MySQL 8.0 Server : [Link](https://dev.mysql.com/get/Downloads/MySQLInstaller/mysql-installer-community-8.0.34.0.msi) +2. Install mysql-installer-web-community-8.0.34.0.msi + 1. Click on "Add ..." on the side + 2. Click on the "+" next to MySQL Servers + 3. Make sure MySQL Server 8.0.34 - X64 is under the products to be installed. + 4. Hit Next and Next once installed + 5. Select the configuration type "Development Computer" + 6. Hit Next + 7. Select "Use Legacy Authentication Method (Retain MySQL 5.x compatibility)" and hit Next + 8. Enter a root password and then hit Next > + 9. Leave everything under Windows Service as default and hit Next > + 10. Click on Execute and for it to finish and hit Next> and then Finish +3. Open MySQL 8.0 Command Line Client and login as your root user +4. Change `` to a new password for the user aime, type those commands to create your user and the database -### Git -- While technically optional, it is strongly recommended to obtain ARTEMiS via git clone instead of just downloading it. - - https://git-scm.com/download/win -- It is recommended to use Notepad++ as the default editor (if you have it installed), other than that, the default settings should be fine. - -### Optional: GUI database viewer -- Having a GUI database editor is recommended but not required. -- MariaDB will try to install HeidiSQL, but we recommend DBeaver. - - https://dbeaver.io/download/ - -## Obtain ARTEMiS -### Via git (recommended) -- `git clone https://gitea.tendokyu.moe/Hay1tsme/artemis.git` via cmd in whatever folder you want to install ARTEMiS. - - You can switch to the develop branch for latest changes via `git checkout develop`. - -### Via http download -- Download [here](https://gitea.tendokyu.moe/Hay1tsme/artemis/archive/master.zip). - - Develop branch can be found [here](https://gitea.tendokyu.moe/Hay1tsme/artemis/archive/develop.zip). -- Extract the zip file somewhere. - -## Database setup -- Log into your server as root, either via GUI (recommended) or CMD -- Create the `aime` user, replace `` with a password you choose. Remember it! -``` -CREATE USER 'aime'@'localhost' IDENTIFIED BY ''; +```sql +CREATE USER 'aime'@'localhost' IDENTIFIED BY ''; CREATE DATABASE aime; GRANT Alter,Create,Delete,Drop,Index,Insert,References,Select,Update ON aime.* TO 'aime'@'localhost'; +FLUSH PRIVILEGES; +exit; ``` -- If you create the database via a GUI, make sure you grant all the above permissions. -## Create a venv -- Python virtual environments are a good way to manage packages and make dealing with python and pip easier. -- `python -m pip venv venv` -- `venv\Scripts\activate.bat` to activate the venv whenever you need to interact with ARTEMiS. -- All the rest of the steps assume your venv is activated. +## Install Python modules +1. Change your work path to the artemis-master folder using 'cd' and install the requirements: -## Install pip modules -- `pip install -r requirements.txt` +```shell +pip install -r requirements.txt +``` -## Setup configuration -- Create a new `config` folder and copy the files in `example_config` over. -- edit `core.yaml` - - Put the password you created for the aime user into the `database` section. - - Put in the aimedb key (YOU DO NOT GENERATE THIS KEY, FIND IT SOMEWHERE). - - Set your hostname to be whatever hostname or IP address games can reach your server at (many games reject localhost and 127.0.0.1). - - Optional: generate base64-encoded secrets for aimedb and frontend. - - See [config.md](docs/config.md) for a full list of options. -- edit `idz.yaml` - - If you don't plan on anyone using your server to play Initial D Zero, it is best to disable it to cut down on console spam on boot. -- Edit other game yamls - - Add keys, set hostnames, ports, etc. Specific settings will depend on the game. See [game_specific_info](docs/game_specific_info.md). +## Copy/Rename the folder `example_config` to `config` -## Create Database Tables -- `python dbutils.py create` +## Adjust `config/core.yaml` -## Firewall -- If you're planning on serving games not on your PC, open at least ports 80, 8443, and 22345 in windows firewall - - Also set `listen_address` to either your local IP to serve on your LAN, or `0.0.0.0` for all interfaces, to accept connections from other places. +1. Make sure to change the server `hostname` to be set to your local machine IP (ex.: 192.168.xxx.xxx) + - In case you want to run this only locally, set the following values: -## Start ARTEMiS -- `python index.py` +```yaml +server: + listen_address: 0.0.0.0 +title: + hostname: 192.168.xxx.xxx +``` + +1. Adjust the proper MySQL information you created earlier +```yaml +database: + host: "localhost" + username: "aime" + password: "" + name: "aime" +``` +3. Add the AimeDB key at the bottom of the file +4. If the webui is needed, change the flag from False to True + +## Create the database tables for ARTEMiS + +```shell +python dbutils.py create +``` + +## Firewall Adjustements +Make sure the following ports are open both on your router and local Windows firewall in case you want to use this for public use (NOT recommended): +> Port 80 (TCP), 443 (TCP), 8443 (TCP), 22345 (TCP), 8080 (TCP), 8090 (TCP) **webui, 8444 (TCP) **mucha + +## Running the ARTEMiS instance +```shell +python index.py +``` + +# Troubleshooting + +## Game does not connect to ARTEMiS Allnet server +1. Double-check your core.yaml, the listen_address is most likely either not binded to the proper IP or the port is not opened + +## Game does not connect to Title Server +1. Verify that your core.yaml is setup properly for both the server listen_address and title hostname +2. Boot your game and verify that an AllNet response does show and if it does, attempt to open the URI that is shown under a browser such as Edge, Chrome & Firefox. +3. If a page is shown, the server is working properly and if it doesn't, double check your port forwarding and also that you have entered the proper local IP under the Title hostname in core.yaml. + +## Unhandled command under AimeDB +1. Double check your AimeDB key under core.yaml, it is incorrect. + +## AttributeError: module 'collections' has no attribute 'Hashable' +1. This means the pyYAML module is obsolete, simply rerun pip with the -U (force update) flag, as shown below. + - Change your work path to the artemis-master (or artemis-develop) folder using 'cd' and run the following commands: + +```shell +pip install -r requirements.txt -U +``` diff --git a/docs/INSTALL_WINDOWS_old.md b/docs/INSTALL_WINDOWS_old.md deleted file mode 100644 index b976b26..0000000 --- a/docs/INSTALL_WINDOWS_old.md +++ /dev/null @@ -1,102 +0,0 @@ -# ARTEMiS - Windows 10/11 Guide -This step-by-step guide assumes that you are using a fresh install of Windows 10/11 without MySQL installed, some of the steps can be skipped if you already have an installation with MySQL 8.0 or even some of the modules already present on your environment - -# Setup -## Install Python Python 3.9 (recommended) or 3.10 -1. Download Python 3.9 : [Link](https://www.python.org/ftp/python/3.9.13/python-3.9.13-amd64.exe) -2. Install python-3.9.13-amd64.exe - 1. Select Customize installation - 2. Make sure that pip, tcl/tk, and the for all users are checked and hit Next - 3. Make sure that you enable "Create shortcuts for installed applications" and "Add Python to environment variables" and hit Install - -## Install MySQL 8.0 -1. Download MySQL 8.0 Server : [Link](https://dev.mysql.com/get/Downloads/MySQLInstaller/mysql-installer-community-8.0.34.0.msi) -2. Install mysql-installer-web-community-8.0.34.0.msi - 1. Click on "Add ..." on the side - 2. Click on the "+" next to MySQL Servers - 3. Make sure MySQL Server 8.0.34 - X64 is under the products to be installed. - 4. Hit Next and Next once installed - 5. Select the configuration type "Development Computer" - 6. Hit Next - 7. Select "Use Legacy Authentication Method (Retain MySQL 5.x compatibility)" and hit Next - 8. Enter a root password and then hit Next > - 9. Leave everything under Windows Service as default and hit Next > - 10. Click on Execute and for it to finish and hit Next> and then Finish -3. Open MySQL 8.0 Command Line Client and login as your root user -4. Change `` to a new password for the user aime, type those commands to create your user and the database - -```sql -CREATE USER 'aime'@'localhost' IDENTIFIED BY ''; -CREATE DATABASE aime; -GRANT Alter,Create,Delete,Drop,Index,Insert,References,Select,Update ON aime.* TO 'aime'@'localhost'; -FLUSH PRIVILEGES; -exit; -``` - -## Install Python modules -1. Change your work path to the artemis-master folder using 'cd' and install the requirements: - -```shell -pip install -r requirements.txt -``` - -## Copy/Rename the folder `example_config` to `config` - -## Adjust `config/core.yaml` - -1. Make sure to change the server `hostname` to be set to your local machine IP (ex.: 192.168.xxx.xxx) - - In case you want to run this only locally, set the following values: - -```yaml -server: - listen_address: 0.0.0.0 -title: - hostname: 192.168.xxx.xxx -``` - -1. Adjust the proper MySQL information you created earlier -```yaml -database: - host: "localhost" - username: "aime" - password: "" - name: "aime" -``` -3. Add the AimeDB key at the bottom of the file -4. If the webui is needed, change the flag from False to True - -## Create the database tables for ARTEMiS - -```shell -python dbutils.py create -``` - -## Firewall Adjustements -Make sure the following ports are open both on your router and local Windows firewall in case you want to use this for public use (NOT recommended): -> Port 80 (TCP), 443 (TCP), 8443 (TCP), 22345 (TCP), 8080 (TCP), 8090 (TCP) **webui, 8444 (TCP) **mucha - -## Running the ARTEMiS instance -```shell -python index.py -``` - -# Troubleshooting - -## Game does not connect to ARTEMiS Allnet server -1. Double-check your core.yaml, the listen_address is most likely either not binded to the proper IP or the port is not opened - -## Game does not connect to Title Server -1. Verify that your core.yaml is setup properly for both the server listen_address and title hostname -2. Boot your game and verify that an AllNet response does show and if it does, attempt to open the URI that is shown under a browser such as Edge, Chrome & Firefox. -3. If a page is shown, the server is working properly and if it doesn't, double check your port forwarding and also that you have entered the proper local IP under the Title hostname in core.yaml. - -## Unhandled command under AimeDB -1. Double check your AimeDB key under core.yaml, it is incorrect. - -## AttributeError: module 'collections' has no attribute 'Hashable' -1. This means the pyYAML module is obsolete, simply rerun pip with the -U (force update) flag, as shown below. - - Change your work path to the artemis-master (or artemis-develop) folder using 'cd' and run the following commands: - -```shell -pip install -r requirements.txt -U -``` diff --git a/docs/config.md b/docs/config.md index 39855dc..81fb43d 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1,24 +1,23 @@ # ARTEMiS Configuration ## Server - `listen_address`: IP Address or hostname that the server will listen for connections on. Set to 127.0.0.1 for local only, or 0.0.0.0 for all interfaces. Default `127.0.0.1` -- `hostname`: Hostname that gets sent to clients to tell them where to connect. Games must be able to connect to your server via the hostname or IP you spcify here. Note that most games will reject `localhost` or `127.0.0.1`. Default `localhost` -- `port`: Port that the server will listen for connections on. Default `80` -- `ssl_key`: Location of the ssl server key for the secure title server. Ignored if you don't use SSL. Default `cert/title.key` -- `ssl_cert`: Location of the ssl server certificate for the secure title server. Must not be a self-signed SSL. Ignored if you don't use SSL. Default `cert/title.pem` - `allow_user_registration`: Allows users to register in-game via the AimeDB `register` function. Disable to be able to control who can use cards on your server. Default `True` - `allow_unregistered_serials`: Allows games that do not have registered keychips to connect and authenticate. Disable to restrict who can connect to your server. Recomended to disable for production setups. Default `True` - `name`: Name for the server, used by some games in their default MOTDs. Default `ARTEMiS` -- `is_develop`: Flags that the server is a development instance, and enables some useful development features. Disable for production setups. Default `True`. -- `is_using_proxy`: Flags that you'll be using some other software, such as nginx, to proxy requests, and to send `proxy_port` or `proxy_port_ssl` to games instead of `port`. Default `False` -- `proxy_port`: Which port your front-facing proxy will be listening on. Ignored if `is_using_proxy` is `False` or if set to `0`. Default `0` -- `proxy_port`: Which port your front-facing proxy will be listening for ssl connections on. Ignored if `is_using_proxy` is `False` or if set to `0`. Default `0` -- `log_dir`: Directory to store logs. Server MUST have read and write permissions to this directory or you will have issues. Default `logs` +- `is_develop`: Flags that the server is a development instance without a proxy standing in front of it. Setting to `False` tells the server not to listen for SSL, because the proxy should be handling all SSL-related things, among other things. Default `True` +- `threading`: Flags that `reactor.run` should be called via the `Thread` standard library. May provide a speed boost, but removes the ability to kill the server via `Ctrl + C`. Default: `False` - `check_arcade_ip`: Checks IPs against the `arcade` table in the database, if one is defined. Default `False` -- `strict_ip_checking`: Rejects clients if there is no IP in the `arcade` table for the respective arcade. Default `False` +- `strict_ip_checking`: Rejects clients if there is no IP in the `arcade` table for the respective arcade +- `log_dir`: Directory to store logs. Server MUST have read and write permissions to this directory or you will have issues. Default `logs` ## Title - `loglevel`: Logging level for the title server. Default `info` -- `reboot_start_time`: 24 hour JST time that clients will see as the start of maintenance period, ex `04:00`. Leave blank for no maintenance time. Default: `""` -- `reboot_end_time`: 24 hour JST time that clients will see as the end of maintenance period, ex `05:00`. Leave blank for no maintenance time. Default: `""` +- `hostname`: Hostname that gets sent to clients to tell them where to connect. Games must be able to connect to your server via the hostname or IP you spcify here. Note that most games will reject `localhost` or `127.0.0.1`. Default `localhost` +- `port`: Port that the title server will listen for connections on. Set to 0 to use the Allnet handler to reduce the port footprint. Default `8080` +- `port_ssl`: Port that the secure title server will listen for connections on. Set to 0 to use the Allnet handler to reduce the port footprint. Default `0` +- `ssl_key`: Location of the ssl server key for the secure title server. Ignored if `port_ssl` is set to `0` or `is_develop` set to `False`. Default `cert/title.key` +- `ssl_cert`: Location of the ssl server certificate for the secure title server. Must not be a self-signed SSL. Ignored if `port_ssl` is set to `0` or `is_develop` is set to `False`. Default `cert/title.pem` +- `reboot_start_time`: 24 hour JST time that clients will see as the start of maintenance period. Leave blank for no maintenance time. Default: "" +- `reboot_end_time`: 24 hour JST time that clients will see as the end of maintenance period. Leave blank for no maintenance time. Default: "" ## Database - `host`: Host of the database. Default `localhost` - `username`: Username of the account the server should connect to the database with. Default `aime` @@ -26,32 +25,24 @@ - `name`: Name of the database the server should expect. Default `aime` - `port`: Port the database server is listening on. Default `3306` - `protocol`: Protocol used in the connection string, e.i `mysql` would result in `mysql://...`. Default `mysql` -- `sha2_password`: Whether or not the password in the connection string should be hashed via SHA2. Default `False` -- `loglevel`: Logging level for the database. Default `info` +- `sha2_password`: Weather or not the password in the connection string should be hashed via SHA2. Default `False` +- `loglevel`: Logging level for the database. Default `warn` +- `user_table_autoincrement_start`: What the `aime_user` table ID autoincrememnt should start with. Default `10000` - `memcached_host`: Host of the memcached server. Default `localhost` ## Frontend -- `enable`: Whether or not the frontend servlet should run. Frontend can still be run via `python -m uvicorn core.frontend:app` even if this is set to `False`. Default `False` -- `port`: Port the frontend should listen on. Default `8080` +- `enable`: Weather or not the frontend should be enabled. Default `False` +- `port`: Port the frontend should listen for connections on. Default `8090` - `loglevel`: Logging level for the frontend server. Default `info` -- `secret`: Base64-encoded JWT secret for session cookies, generated by you. Default `""` ## Allnet -- `standalone`: Whether allnet should launch it's own servlet on it's own port, or be part of the main servlet on the default port. Disable if you either have something proxying `naominet.jp` requests to port 80, or have port 80 set in `server` -> `port` -- `port`: Port the allnet server should listen for connections on if it's running standalone. Games are hardcoded to ask for port `80` so only change if you have a proxy redirecting properly. Ignored if `standalone` is `False`. Default `80` - `loglevel`: Logging level for the allnet server. Default `info` +- `port`: Port the allnet server should listen for connections on. Games are hardcoded to ask for port `80` so only change if you have a proxy redirecting properly. Default `80` - `allow_online_updates`: Allow allnet to distribute online updates via DownloadOrders. This system is currently non-functional, so leave it disabled. Default `False` -- `update_cfg_folder`: Folder where delivery INI files will be checked for. Ignored if `allow_online_updates` is `False`. Default `""` ## Billing -- `standalone`: Whether the billing server should launch it's own servlet on it's own port, or be part of the main servlet on the default port. Setting this to `True` requires that you have `ssl_key` and `ssl_cert` set. Default `False` -- `loglevel`: Logging level for the billing server. Default `info` -- `port`: Port the billing server should listen for connections on. Games are hardcoded to ask for port `8443` so only change if you have a proxy redirecting properly. Ignored if `standalone` is `False`. Default `8443` -- `ssl_key`: Location of the ssl server key for the billing server. Ignored if `standalone` is `False`. Default `cert/server.key` -- `ssl_cert`: Location of the ssl server certificate for the billing server. Ignored if `standalone` is `False`. Must match the CA distributed to users or the billing server will not connect. Default `cert/server.pem` +- `port`: Port the billing server should listen for connections on. Games are hardcoded to ask for port `8443` so only change if you have a proxy redirecting properly. Set to 0 to use the allnet handler to reduce the number of ports the server eats up. Default `8443` +- `ssl_key`: Location of the ssl server key for the billing server. Ignored if `port` is set to `0` or `is_develop` set to `False`. Default `cert/server.key` +- `ssl_cert`: Location of the ssl server certificate for the billing server. Must match the CA distributed to users or the billing server will not connect. Ignored if `port` is set to `0` or `is_develop` is set to `False`. Default `cert/server.pem` - `signing_key`: Location of the RSA Private key used to sign billing requests. Must match the public key distributed to users or the billing server will not connect. Default `cert/billing.key` ## Aimedb -- `enable`: Whether or not aimedb should run. Default `True` -- `listen_address`: IP Address or hostname that the aimedb server will listen for connections on. Leave this blank to use the listen address under `server`. Default `""` - `loglevel`: Logging level for the aimedb server. Default `info` - `port`: Port the aimedb server should listen for connections on. Games are hardcoded to ask for port `22345` so only change if you have a proxy redirecting properly. Default `22345` -- `key`: Key to encrypt/decrypt aimedb requests and responses. MUST be set or the server will not start. If set incorrectly, your server will not properly handle aimedb requests. Default `""` -- `id_secret`: Base64-encoded JWT secret for Sega Auth IDs. Leaving this blank disables this feature. Default `""` -- `id_lifetime_seconds`: Number of secons a JWT generated should be valid for. Default `86400` (1 day) +- `key`: Key to encrypt/decrypt aimedb requests and responses. MUST be set or the server will not start. If set incorrectly, your server will not properly handle aimedb requests. Default `""` \ No newline at end of file diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index ab2daab..01960f9 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -165,6 +165,15 @@ The importer for crossbeats REV. will import Music. Config file is located in `config/cxb.yaml`. +| Option | Info | +| --------------------- | ---------------------------------------------------------- | +| `hostname` | Requires a proper `hostname` (not localhost!) to run | +| `ssl_enable` | Enables/Disables the use of the `ssl_cert` and `ssl_key` | +| `port` | Set your unsecure port number | +| `port_secure` | Set your secure/SSL port number | +| `ssl_cert`, `ssl_key` | Enter your SSL certificate (requires not self signed cert) | + + ## maimai DX ### Versions @@ -192,6 +201,7 @@ Config file is located in `config/cxb.yaml`. | SDEZ | 18 | maimai DX UNiVERSE PLUS | | SDEZ | 19 | maimai DX FESTiVAL | | SDEZ | 20 | maimai DX FESTiVAL PLUS | +| SDEZ | 21 | maimai DX BUDDiES | ### Importer @@ -406,6 +416,7 @@ After that, on next login the present should be received (or whenever it suppose * UNiVERSE PLUS: Yes * FESTiVAL: Yes (added in A031) * FESTiVAL PLUS: Yes (added in A035) + * BUDDiES: Yes * O.N.G.E.K.I. bright MEMORY: Yes @@ -594,7 +605,7 @@ Below is a list of VIP rewards. Currently, VIP is not implemented, and thus thes In order to use the importer locate your game installation folder and execute: ```shell -python read.py --game SDEW --version 0 --binfolder /titles/sao/data/ +python read.py --game SDEW --version --binfolder /path/to/game/extractedassets ``` The importer for SAO will import all items, heroes, support skills and titles data. @@ -621,17 +632,15 @@ python dbutils.py --game SDEW upgrade ### Notes - Defrag Match will crash at loading - Co-Op Online is not supported -- Shop is displayed but cannot purchase heroes or items +- Shop is not functionnal - Player title is currently static and cannot be changed in-game - QR Card Scanning currently only load a static hero -- Ex-quests progression not supported yet -- Daily Missions not implemented -- EX TOWER 1,2 & 3 are not yet supported -- Daily Yui coin not yet fixed + +**Network hashing in GssSite.dll must be disabled** ### Credits for SAO support: -- Midorica - Network Support +- Midorica - Limited Network Support - Dniel97 - Helping with network base - tungnotpunk - Source diff --git a/docs/prod.md b/docs/prod.md index c398061..de79b99 100644 --- a/docs/prod.md +++ b/docs/prod.md @@ -1,34 +1,41 @@ # ARTEMiS Production mode -ARTEMiS is designed to run in one of two ways. Developmen/local mode, which assumes you're just trying to set up something to save your scores and make the games work, and have patched your games to disable SSL and cert checks and encryption and the like, and production mode. In production mode, artemis assumes you have a proxy server, such as nginx or apache, standing in front of artemis doing HTTPS and port management. This document will cover how to properly set up a production instance of ARTEMiS. - -## ARTEMiS configuration -Step 1 is to edit your artemis configuration. Some recomended changes: -### `server` -- `listen_address` -> `127.0.0.1` -- `is_develop` -> `False` -- `is_using_proxy` -> `True` -- `port` -> The port nginx will send proxied requests to. If you're using the example config, set this to 8080. -- `proxy_port` -> The port your proxy will be accepting title server connections on. If you're using the example config, set this to 80. -- `proxy_port_ssl` -> The port your proxy will be accepting secure title server connections on. If you're using the example config, set this to 443. -- `allow_unregistered_serials` -> `False` -### `billing` -- `standalone` -> `False` -### `allnet` -- `standalone` -> `False` -### `frontend` -- `enable` -> `True` if you want the frontend -- `port` -> `8090` if you're using the default nginx config, otherwise whatever port your proxy will be sending requests to -### `aimedb` -- `listen_address` -> `0.0.0.0` unless you're proxying aimedb requests (not recomended at this time), in which case, leave this option unchanged - -If you plan to serve artemis behind a VPN, these additional settings are also recomended -- `check_arcade_ip` -> `True` -- `strict_ip_checking` -> `True` +Production mode is a configuration option that changes how the server listens to be more friendly to a production environment. This mode assumes that a proxy (for this guide, nginx) is standing in front of the server to handle port mapping and TLS. In order to activate production mode, simply change `is_develop` to `False` in `core.yaml`. Next time you start the server, you should see "Starting server in production mode". ## Nginx Configuration -For most cases, the config in `example_config` will suffice. It makes the following assumptions -- ARTEMiS is running on port 8080 -- Billing is set to not be standalone -- You're not using cloudflare in front of your frontend +### Port forwarding +Artemis requires that the following ports be forwarded to allow internet traffic to access the server. This will not change regardless of what you set in the config, as many of these ports are hard-coded in the games. +`tcp:80` all.net, non-ssl titles +`tcp:8443` billing +`tcp:22345` aimedb +`tcp:443` frontend, SSL titles -If this describes you, your only configuration needs are to edit the `server_name` and `certificate_*` directives. Otherwise, please see nginx configuration documentation to configure it to best suit your setup. +### A note about external proxy services (cloudflare, etc) +Due to the way that artemis functions, it is currently not possible to put the server behind something like Cloudflare. Cloudflare only proxies web traffic on the standard ports (80, 443) and, as shown above, this does not work with artemis. Server administrators should seek other means to protect their network (VPS hosting, VPN, etc) + +### SSL Certificates +You will need to generate SSL certificates for some games. The certificates vary in security and validity requirements. Please see the general guide below +- General Title: The certificate for the general title server should be valid, not self-signed and match the CN that the game will be reaching out to (e.i if your games are reaching out to titles.hostname.here, your ssl certificate should be valid for titles.hostname.here, or *.hostname.here) +- CXB: Same requires as the title server. It must not be self-signed, and CN must match. Recomended to get a wildcard cert if possible, and use it for both Title and CXB +- Pokken: Pokken can be self-signed, and the CN doesn't have to match, but it MUST use 2048-bit RSA. Due to the games age, andthing stronger then that will be rejected. + +### Port mappings +An example config is provided in the `config` folder called `nginx_example.conf`. It is set up for the following: +`naominet.jp:tcp:80` -> `localhost:tcp:8000` for allnet +`ib.naominet.jp:ssl:8443` -> `localhost:tcp:8444` for the billing server +`your.hostname.here:ssl:443` -> `localhost:tcp:8080` for the SSL title server +`your.hostname.here:tcp:80` -> `localhost:tcp:8080` for the non-SSL title server +`cxb.hostname.here:ssl:443` -> `localhost:tcp:8080` for crossbeats (appends /SDCA/104/ to the request) +`pokken.hostname.here:ssl:443` -> `localhost:tcp:8080` for pokken +`frontend.hostname.here:ssl:443` -> `localhost:tcp:8090` for the frontend, includes https redirection + +If you're using this as a guide, be sure to replace your.hostname.here with the hostname you specified in core.yaml under `titles->hostname`. Do *not* change naominet.jp, or allnet/billing will fail. Also remember to specifiy certificate paths correctly, as in the example they are simply placeholders. + +### Multi-service ports +It is possible to use nginx to redirect billing and title server requests to the same port that all.net uses. By setting `port` to 0 under billing and title server, you can change the nginx config to serve the following (entries not shown here should be the same) +`ib.naominet.jp:ssl:8443` -> `localhost:tcp:8000` for the billing server +`your.hostname.here:ssl:443` -> `localhost:tcp:8000` for the SSL title server +`your.hostname.here:tcp:80` -> `localhost:tcp:8000` for the non-SSL title server +`cxb.hostname.here:ssl:443` -> `localhost:tcp:8000` for crossbeats (appends /SDCA/104/ to the request) +`pokken.hostname.here:ssl:443` -> `localhost:tcp:8000` for pokken + +This will allow you to only use 3 ports locally, but you will still need to forward the same internet-facing ports as before. \ No newline at end of file diff --git a/example_config/chuni.yaml b/example_config/chuni.yaml index 4855fa1..72a1d98 100644 --- a/example_config/chuni.yaml +++ b/example_config/chuni.yaml @@ -22,14 +22,6 @@ version: 14: rom: 2.15.00 data: 2.15.00 - 15: - rom: 2.20.00 - data: 2.20.00 crypto: - encrypted_only: False - -matching: - enable: False - match_time_limit: 60 - match_error_limit: 9999 + encrypted_only: False \ No newline at end of file diff --git a/example_config/core.yaml b/example_config/core.yaml index 758a089..21b1a9d 100644 --- a/example_config/core.yaml +++ b/example_config/core.yaml @@ -1,25 +1,26 @@ server: - listen_address: "127.0.0.1" - hostname: "localhost" - port: 80 - ssl_key: "cert/title.key" - ssl_cert: "cert/title.crt" + listen_address: "127.0.0.1" allow_user_registration: True allow_unregistered_serials: True name: "ARTEMiS" is_develop: True is_using_proxy: False - proxy_port: 0 - proxy_port_ssl: 0 + threading: False log_dir: "logs" check_arcade_ip: False strict_ip_checking: False title: loglevel: "info" + hostname: "localhost" + port: 8080 + port_ssl: 0 + ssl_cert: "cert/title.crt" + ssl_key: "cert/title.key" reboot_start_time: "04:00" reboot_end_time: "05:00" + database: host: "localhost" username: "aime" @@ -28,34 +29,30 @@ database: port: 3306 protocol: "mysql" sha2_password: False - loglevel: "info" + loglevel: "warn" + user_table_autoincrement_start: 10000 enable_memcached: True memcached_host: "localhost" frontend: enable: False - port: 8080 + port: 8090 loglevel: "info" - secret: "" allnet: - standalone: False - port: 80 loglevel: "info" + port: 80 + ip_check: False allow_online_updates: False update_cfg_folder: "" billing: - standalone: True - loglevel: "info" port: 8443 ssl_key: "cert/server.key" ssl_cert: "cert/server.pem" signing_key: "cert/billing.key" aimedb: - enable: True - listen_address: "" loglevel: "info" port: 22345 key: "" @@ -63,4 +60,6 @@ aimedb: id_lifetime_seconds: 86400 mucha: + enable: False + hostname: "localhost" loglevel: "info" diff --git a/example_config/cxb.yaml b/example_config/cxb.yaml index 5cc4f90..7723ff4 100644 --- a/example_config/cxb.yaml +++ b/example_config/cxb.yaml @@ -1,4 +1,3 @@ server: enable: True - loglevel: "info" - use:https: True \ No newline at end of file + loglevel: "info" \ No newline at end of file diff --git a/example_config/nginx_example.conf b/example_config/nginx_example.conf index b01a822..ef3b7d4 100644 --- a/example_config/nginx_example.conf +++ b/example_config/nginx_example.conf @@ -6,7 +6,7 @@ server { location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass_request_headers on; - proxy_pass http://127.0.0.1:8080/; + proxy_pass http://localhost:8000/; } } @@ -18,7 +18,7 @@ server { location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass_request_headers on; - proxy_pass http://127.0.0.1:8080/; + proxy_pass http://localhost:8080/; } } @@ -38,13 +38,11 @@ server { ssl_prefer_server_ciphers off; location / { - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_pass_request_headers on; - proxy_pass http://127.0.0.1:8080/; + proxy_pass http://localhost:8080/; } } -# Billing, comment this out if running billing standalone +# Billing server { listen 8443 ssl; server_name ib.naominet.jp; @@ -59,10 +57,30 @@ server { ssl_ciphers "ALL:@SECLEVEL=0"; ssl_prefer_server_ciphers off; + location / { + proxy_pass http://localhost:8444/; + } +} + +# Pokken, comment this out if you don't plan on serving pokken. +server { + listen 443 ssl; + server_name pokken.hostname.here; + + ssl_certificate /path/to/cert/pokken.pem; + ssl_certificate_key /path/to/cert/pokken.key; + ssl_session_timeout 1d; + ssl_session_cache shared:MozSSL:10m; + ssl_session_tickets off; + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; + ssl_ciphers "ALL:@SECLEVEL=0"; + ssl_prefer_server_ciphers off; + location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass_request_headers on; - proxy_pass http://127.0.0.1:8080/; + proxy_pass http://localhost:8080/; } } @@ -73,12 +91,12 @@ server { location / { return 301 https://$host$request_uri; - # If you don't want https redirection, or are using something like cloudflare to manage HTTPS, comment out the line above and uncomment the line below - # proxy_pass http://127.0.0.1:8090/; + # If you don't want https redirection, comment the line above and uncomment the line below + # proxy_pass http://localhost:8090/; } } -# Frontend HTTPS. Comment out if you on't intend to use the frontend, or have cloudflare or something managing https for you. +# Frontend HTTPS. Comment out if you on't intend to use the frontend server { listen 443 ssl; server_name frontend.hostname.here; @@ -100,6 +118,6 @@ server { location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass_request_headers on; - proxy_pass http://127.0.0.1:8090/; + proxy_pass http://localhost:8090/; } } diff --git a/index.py b/index.py index 08117d5..798519c 100644 --- a/index.py +++ b/index.py @@ -1,118 +1,335 @@ #!/usr/bin/env python3 import argparse -import asyncio -import logging -from os import environ, path - -import uvicorn +import logging, coloredlogs +from logging.handlers import TimedRotatingFileHandler +from typing import Dict import yaml -from core import AimedbServlette, CoreConfig +from os import path, mkdir, access, W_OK +from core import * +from twisted.web import server, resource +from twisted.internet import reactor, endpoints +from twisted.web.http import Request +from routes import Mapper +from threading import Thread -async def launch_main(cfg: CoreConfig, ssl: bool) -> None: - if ssl: - server_cfg = uvicorn.Config( - "core.app:app", - host=cfg.server.listen_address, - port=cfg.server.port if args.port == 0 else args.port, - reload=cfg.server.is_develop, - log_level="info" if cfg.server.is_develop else "critical", - ssl_version=3, - ssl_certfile=cfg.server.ssl_cert, - ssl_keyfile=cfg.server.ssl_key, +class HttpDispatcher(resource.Resource): + def __init__(self, cfg: CoreConfig, config_dir: str): + super().__init__() + self.config = cfg + self.isLeaf = True + self.map_get = Mapper() + self.map_post = Mapper() + self.logger = logging.getLogger("core") + + self.title = TitleServlet(cfg, config_dir) + self.allnet = AllnetServlet(cfg, config_dir) + self.mucha = MuchaServlet(cfg, config_dir) + + self.map_get.connect( + "allnet_downloadorder_ini", + "/dl/ini/{file}", + controller="allnet", + action="handle_dlorder_ini", + conditions=dict(method=["GET"]), ) - else: - server_cfg = uvicorn.Config( - "core.app:app", - host=cfg.server.listen_address, - port=cfg.server.port if args.port == 0 else args.port, - reload=cfg.server.is_develop, - log_level="info" if cfg.server.is_develop else "critical", + + self.map_post.connect( + "allnet_downloadorder_report", + "/report-api/Report", + controller="allnet", + action="handle_dlorder_report", + conditions=dict(method=["POST"]), ) - server = uvicorn.Server(server_cfg) - await server.serve() + self.map_get.connect( + "allnet_ping", + "/naomitest.html", + controller="allnet", + action="handle_naomitest", + conditions=dict(method=["GET"]), + ) + self.map_post.connect( + "allnet_poweron", + "/sys/servlet/PowerOn", + controller="allnet", + action="handle_poweron", + conditions=dict(method=["POST"]), + ) + self.map_post.connect( + "allnet_downloadorder", + "/sys/servlet/DownloadOrder", + controller="allnet", + action="handle_dlorder", + conditions=dict(method=["POST"]), + ) + self.map_post.connect( + "allnet_loaderstaterecorder", + "/sys/servlet/LoaderStateRecorder", + controller="allnet", + action="handle_loaderstaterecorder", + conditions=dict(method=["POST"]), + ) + self.map_post.connect( + "allnet_alive", + "/sys/servlet/Alive", + controller="allnet", + action="handle_alive", + conditions=dict(method=["POST"]), + ) + self.map_get.connect( + "allnet_alive", + "/sys/servlet/Alive", + controller="allnet", + action="handle_alive", + conditions=dict(method=["GET"]), + ) + self.map_post.connect( + "allnet_billing", + "/request", + controller="allnet", + action="handle_billing_request", + conditions=dict(method=["POST"]), + ) + self.map_post.connect( + "allnet_billing", + "/request/", + controller="allnet", + action="handle_billing_request", + conditions=dict(method=["POST"]), + ) -async def launch_billing(cfg: CoreConfig) -> None: - server_cfg = uvicorn.Config( - "core.allnet:app_billing", - host=cfg.server.listen_address, - port=cfg.billing.port, - reload=cfg.server.is_develop, - log_level="info" if cfg.server.is_develop else "critical", - ssl_version=3, - ssl_certfile=cfg.billing.ssl_cert, - ssl_keyfile=cfg.billing.ssl_key, - ssl_ciphers="DEFAULT:!aNULL:!eNULL:!MD5:!3DES:!DES:!RC4:!IDEA:!SEED:!aDSS:!SRP:!PSK", - ) - server = uvicorn.Server(server_cfg) - await server.serve() + # Maintain compatability + self.map_post.connect( + "mucha_boardauth", + "/mucha/boardauth.do", + controller="mucha", + action="handle_boardauth", + conditions=dict(method=["POST"]), + ) + self.map_post.connect( + "mucha_updatacheck", + "/mucha/updatacheck.do", + controller="mucha", + action="handle_updatecheck", + conditions=dict(method=["POST"]), + ) + self.map_post.connect( + "mucha_dlstate", + "/mucha/downloadstate.do", + controller="mucha", + action="handle_dlstate", + conditions=dict(method=["POST"]), + ) + self.map_post.connect( + "mucha_boardauth", + "/mucha_front/boardauth.do", + controller="mucha", + action="handle_boardauth", + conditions=dict(method=["POST"]), + ) + self.map_post.connect( + "mucha_updatacheck", + "/mucha_front/updatacheck.do", + controller="mucha", + action="handle_updatecheck", + conditions=dict(method=["POST"]), + ) + self.map_post.connect( + "mucha_dlstate", + "/mucha_front/downloadstate.do", + controller="mucha", + action="handle_dlstate", + conditions=dict(method=["POST"]), + ) -async def launch_frontend(cfg: CoreConfig) -> None: - server_cfg = uvicorn.Config( - "core.frontend:app", - host=cfg.server.listen_address, - port=cfg.frontend.port, - reload=cfg.server.is_develop, - log_level="info" if cfg.server.is_develop else "critical", - ) - server = uvicorn.Server(server_cfg) - await server.serve() + for code, game in self.title.title_registry.items(): + get_matchers, post_matchers = game.get_endpoint_matchers() + + for m in get_matchers: + self.map_get.connect( + "title_get", + m[1], + controller="title", + action="render_GET", + title=code, + subaction=m[0], + conditions=dict(method=["GET"]), + requirements=m[2], + ) + + for m in post_matchers: + self.map_post.connect( + "title_post", + m[1], + controller="title", + action="render_POST", + title=code, + subaction=m[0], + conditions=dict(method=["POST"]), + requirements=m[2], + ) + def render_GET(self, request: Request) -> bytes: + test = self.map_get.match(request.uri.decode()) + client_ip = Utils.get_ip_addr(request) -async def launch_allnet(cfg: CoreConfig) -> None: - server_cfg = uvicorn.Config( - "core.allnet:app_allnet", - host=cfg.server.listen_address, - port=cfg.allnet.port, - reload=cfg.server.is_develop, - log_level="info" if cfg.server.is_develop else "critical", - ) - server = uvicorn.Server(server_cfg) - await server.serve() + if test is None: + self.logger.debug( + f"Unknown GET endpoint {request.uri.decode()} from {client_ip} to port {request.getHost().port}" + ) + request.setResponseCode(404) + return b"Endpoint not found." + return self.dispatch(test, request) -async def launcher(cfg: CoreConfig, ssl: bool) -> None: - task_list = [asyncio.create_task(launch_main(cfg, ssl))] + def render_POST(self, request: Request) -> bytes: + test = self.map_post.match(request.uri.decode()) + client_ip = Utils.get_ip_addr(request) - if cfg.billing.standalone: - task_list.append(asyncio.create_task(launch_billing(cfg))) - if cfg.frontend.enable: - task_list.append(asyncio.create_task(launch_frontend(cfg))) - if cfg.allnet.standalone: - task_list.append(asyncio.create_task(launch_allnet(cfg))) - if cfg.aimedb.enable: - AimedbServlette(cfg).start() + if test is None: + self.logger.debug( + f"Unknown POST endpoint {request.uri.decode()} from {client_ip} to port {request.getHost().port}" + ) + request.setResponseCode(404) + return b"Endpoint not found." - done, pending = await asyncio.wait( - task_list, - return_when=asyncio.FIRST_COMPLETED, - ) + return self.dispatch(test, request) - logging.getLogger("core").info("Shutdown") - for pending_task in pending: - pending_task.cancel("Another service died, server is shutting down") + def dispatch(self, matcher: Dict, request: Request) -> bytes: + controller = getattr(self, matcher["controller"], None) + if controller is None: + self.logger.error( + f"Controller {matcher['controller']} not found via endpoint {request.uri.decode()}" + ) + request.setResponseCode(404) + return b"Endpoint not found." + + handler = getattr(controller, matcher["action"], None) + if handler is None: + self.logger.error( + f"Action {matcher['action']} not found in controller {matcher['controller']} via endpoint {request.uri.decode()}" + ) + request.setResponseCode(404) + return b"Endpoint not found." + + url_vars = matcher + url_vars.pop("controller") + url_vars.pop("action") + ret = handler(request, url_vars) + + if type(ret) == str: + return ret.encode() + + elif type(ret) == bytes or type(ret) == tuple: # allow for bytes or tuple (data, response code) responses + return ret + + elif ret is None: + self.logger.warning(f"None returned by controller for {request.uri.decode()} endpoint") + return b"" + + else: + self.logger.warning(f"Unknown data type returned by controller for {request.uri.decode()} endpoint") + return b"" if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Artemis main entry point") + parser = argparse.ArgumentParser(description="ARTEMiS main entry point") parser.add_argument( "--config", "-c", type=str, default="config", help="Configuration folder" ) - parser.add_argument("--port", "-p", type=int, default=0, help="Port override") - parser.add_argument("--ssl", "-s", type=bool, help="Launch with SSL") args = parser.parse_args() if not path.exists(f"{args.config}/core.yaml"): print( - f"The config folder you specified ({args.config}) does not exist or does not contain core.yaml. Defaults will be used.\nDid you copy the example folder?" + f"The config folder you specified ({args.config}) does not exist or does not contain core.yaml.\nDid you copy the example folder?" ) + exit(1) cfg: CoreConfig = CoreConfig() if path.exists(f"{args.config}/core.yaml"): cfg.update(yaml.safe_load(open(f"{args.config}/core.yaml"))) - environ["ARTEMIS_CFG_DIR"] = args.config + if not path.exists(cfg.server.log_dir): + mkdir(cfg.server.log_dir) - asyncio.run(launcher(cfg, args.ssl)) + if not access(cfg.server.log_dir, W_OK): + print( + f"Log directory {cfg.server.log_dir} NOT writable, please check permissions" + ) + exit(1) + + logger = logging.getLogger("core") + log_fmt_str = "[%(asctime)s] Core | %(levelname)s | %(message)s" + log_fmt = logging.Formatter(log_fmt_str) + + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(cfg.server.log_dir, "core"), when="d", backupCount=10 + ) + fileHandler.setFormatter(log_fmt) + + consoleHandler = logging.StreamHandler() + consoleHandler.setFormatter(log_fmt) + + logger.addHandler(fileHandler) + logger.addHandler(consoleHandler) + + log_lv = logging.DEBUG if cfg.server.is_develop else logging.INFO + logger.setLevel(log_lv) + coloredlogs.install(level=log_lv, logger=logger, fmt=log_fmt_str) + + if not cfg.aimedb.key: + logger.error("!!AIMEDB KEY BLANK, SET KEY IN CORE.YAML!!") + exit(1) + + logger.info( + f"ARTEMiS starting in {'develop' if cfg.server.is_develop else 'production'} mode" + ) + + allnet_server_str = f"tcp:{cfg.allnet.port}:interface={cfg.server.listen_address}" + title_server_str = f"tcp:{cfg.title.port}:interface={cfg.server.listen_address}" + title_https_server_str = f"ssl:{cfg.title.port_ssl}:interface={cfg.server.listen_address}:privateKey={cfg.title.ssl_key}:certKey={cfg.title.ssl_cert}" + adb_server_str = f"tcp:{cfg.aimedb.port}:interface={cfg.server.listen_address}" + frontend_server_str = ( + f"tcp:{cfg.frontend.port}:interface={cfg.server.listen_address}" + ) + + billing_server_str = f"tcp:{cfg.billing.port}:interface={cfg.server.listen_address}" + if cfg.server.is_develop: + billing_server_str = ( + f"ssl:{cfg.billing.port}:interface={cfg.server.listen_address}" + f":privateKey={cfg.billing.ssl_key}:certKey={cfg.billing.ssl_cert}" + ) + + dispatcher = HttpDispatcher(cfg, args.config) + + endpoints.serverFromString(reactor, allnet_server_str).listen( + server.Site(dispatcher) + ) + endpoints.serverFromString(reactor, adb_server_str).listen(AimedbFactory(cfg)) + + if cfg.frontend.enable: + endpoints.serverFromString(reactor, frontend_server_str).listen( + server.Site(FrontendServlet(cfg, args.config)) + ) + + if cfg.billing.port > 0: + endpoints.serverFromString(reactor, billing_server_str).listen( + server.Site(dispatcher) + ) + + if cfg.title.port > 0: + endpoints.serverFromString(reactor, title_server_str).listen( + server.Site(dispatcher) + ) + + if cfg.title.port_ssl > 0: + endpoints.serverFromString(reactor, title_https_server_str).listen( + server.Site(dispatcher) + ) + + if cfg.server.threading: + Thread(target=reactor.run, args=(False,)).start() + else: + reactor.run() diff --git a/read.py b/read.py index 3c3606a..fa34314 100644 --- a/read.py +++ b/read.py @@ -1,15 +1,15 @@ -#!/usr/bin/env python3 +# vim: set fileencoding=utf-8 import argparse -import asyncio -import logging -import os import re -from logging.handlers import TimedRotatingFileHandler +import os +import yaml from os import path +import logging +import coloredlogs + +from logging.handlers import TimedRotatingFileHandler from typing import List, Optional -import coloredlogs -import yaml from core import CoreConfig, Utils @@ -39,9 +39,6 @@ class BaseReader: return ret - async def read(self) -> None: - pass - if __name__ == "__main__": parser = argparse.ArgumentParser(description="Import Game Information") @@ -139,7 +136,6 @@ if __name__ == "__main__": for dir, mod in titles.items(): if args.game in mod.game_codes: handler = mod.reader(config, args.version, bin_arg, opt_arg, args.extra) - loop = asyncio.get_event_loop() - loop.run_until_complete(handler.read()) + handler.read() logger.info("Done") diff --git a/readme.md b/readme.md index 1226784..8d0e04e 100644 --- a/readme.md +++ b/readme.md @@ -4,68 +4,38 @@ A network service emulator for games running SEGA'S ALL.NET service, and similar # Supported games Games listed below have been tested and confirmed working. Only game versions older then the version currently active in arcades, or games versions that have not recieved a major update in over one year, are supported. ++ CHUNITHM + + All versions up to SUN PLUS + ++ crossbeats REV. + + All versions + omnimix + ++ maimai DX + + All versions up to BUDDiES + ++ Hatsune Miku: Project DIVA Arcade + + All versions + + Card Maker + 1.30 + 1.35 -+ CHUNITHM INTL - + SUPERSTAR - + NEW - + NEW PLUS - + SUN - + SUN PLUS - -+ CHUNITHM JP - + AIR - + AIR PLUS - + AMAZON - + AMAZON PLUS - + CRYSTAL - + CRYSTAL PLUS - + PARADISE - + PARADISE LOST - + NEW - + NEW PLUS - + SUN - + SUN PLUS - -+ crossbeats REV. - + Crossbeats REV. - + Crossbeats REV. SUNRiSE S1 - + Crossbeats REV. SUNRiSE S2 + omnimix - -+ Hatsune Miku: Project DIVA Arcade - + Future Tone Arcade - All versions - -+ Initial D THE ARCADE - + Season 2 - -+ maimai DX - + Splash - + Splash Plus - + UNiVERSE - + UNiVERSE PLUS - + FESTiVAL - + FESTiVAL PLUS - + O.N.G.E.K.I. - + SUMMER - + SUMMER PLUS - + R.E.D. - + R.E.D. PLUS - + bright - + bright MEMORY - -+ POKKÉN TOURNAMENT - + Final Online - -+ Sword Art Online Arcade - + Final (Single player only) + + All versions up to bright MEMORY + WACCA + Lily R + Reverse ++ POKKÉN TOURNAMENT + + Final Online + ++ Sword Art Online Arcade (partial support) + + Final + ++ Initial D THE ARCADE + + Season 2 + ## Requirements - python 3 (tested working with 3.9 and 3.10, other versions YMMV) - pip @@ -73,7 +43,7 @@ Games listed below have been tested and confirmed working. Only game versions ol - mysql/mariadb server ## Setup guides -Follow the platform-specific guides for [windows](docs/INSTALL_WINDOWS.md), [linux (Debian 12 or Rasperry Pi OS recomended, but anything works)](docs/INSTALL_LINUX.md) or [docker](docs/INSTALL_DOCKER.md) to setup and run the server. +Follow the platform-specific guides for [windows](docs/INSTALL_WINDOWS.md), [ubuntu](docs/INSTALL_UBUNTU.md) or [docker](docs/INSTALL_DOCKER.md) to setup and run the server. ## Game specific information Read [Games specific info](docs/game_specific_info.md) for all supported games, importer settings, configuration option and database upgrades. diff --git a/requirements.txt b/requirements.txt index fe5b4ef..c399e1f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,24 +1,21 @@ -mypy -wheel -pytz -pyyaml -sqlalchemy==1.4.46 -mysqlclient -pyopenssl -service_identity -PyCryptodome -inflection -coloredlogs -pylibmc; platform_system != "Windows" -wacky -bcrypt -jinja2 -protobuf -pillow -pyjwt==2.8.0 -websockets -starlette -asyncio -uvicorn -alembic -python-multipart \ No newline at end of file +mypy +wheel +twisted +pytz +pyyaml +sqlalchemy==1.4.46 +mysqlclient +pyopenssl +service_identity +PyCryptodome +inflection +coloredlogs +pylibmc; platform_system != "Windows" +wacky +Routes +bcrypt +jinja2 +protobuf +autobahn +pillow +pyjwt diff --git a/titles/chuni/__init__.py b/titles/chuni/__init__.py index e64604e..dc0e2f4 100644 --- a/titles/chuni/__init__.py +++ b/titles/chuni/__init__.py @@ -1,15 +1,10 @@ -from .const import ChuniConstants -from .database import ChuniData -from .frontend import ChuniFrontend -from .index import ChuniServlet -from .read import ChuniReader +from titles.chuni.index import ChuniServlet +from titles.chuni.const import ChuniConstants +from titles.chuni.database import ChuniData +from titles.chuni.read import ChuniReader index = ChuniServlet database = ChuniData reader = ChuniReader -frontend = ChuniFrontend -game_codes = [ - ChuniConstants.GAME_CODE, - ChuniConstants.GAME_CODE_NEW, - ChuniConstants.GAME_CODE_INT, -] +game_codes = [ChuniConstants.GAME_CODE, ChuniConstants.GAME_CODE_NEW, ChuniConstants.GAME_CODE_INT] +current_schema_version = 5 \ No newline at end of file diff --git a/titles/chuni/air.py b/titles/chuni/air.py index fbc3b7e..b9bc1d3 100644 --- a/titles/chuni/air.py +++ b/titles/chuni/air.py @@ -2,8 +2,8 @@ from typing import Dict from core.config import CoreConfig from titles.chuni.base import ChuniBase -from titles.chuni.config import ChuniConfig from titles.chuni.const import ChuniConstants +from titles.chuni.config import ChuniConfig class ChuniAir(ChuniBase): @@ -11,7 +11,7 @@ class ChuniAir(ChuniBase): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_AIR - async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = await super().handle_get_game_setting_api_request(data) + 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" return ret diff --git a/titles/chuni/airplus.py b/titles/chuni/airplus.py index 14ad9ba..f0d8224 100644 --- a/titles/chuni/airplus.py +++ b/titles/chuni/airplus.py @@ -2,8 +2,8 @@ from typing import Dict from core.config import CoreConfig from titles.chuni.base import ChuniBase -from titles.chuni.config import ChuniConfig from titles.chuni.const import ChuniConstants +from titles.chuni.config import ChuniConfig class ChuniAirPlus(ChuniBase): @@ -11,7 +11,7 @@ class ChuniAirPlus(ChuniBase): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_AIR_PLUS - async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = await super().handle_get_game_setting_api_request(data) + 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" return ret diff --git a/titles/chuni/amazon.py b/titles/chuni/amazon.py index d4625a8..b765c2f 100644 --- a/titles/chuni/amazon.py +++ b/titles/chuni/amazon.py @@ -1,9 +1,11 @@ -from typing import Dict +from datetime import datetime, timedelta +from typing import Dict, Any +import pytz from core.config import CoreConfig from titles.chuni.base import ChuniBase -from titles.chuni.config import ChuniConfig from titles.chuni.const import ChuniConstants +from titles.chuni.config import ChuniConfig class ChuniAmazon(ChuniBase): @@ -11,7 +13,7 @@ class ChuniAmazon(ChuniBase): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_AMAZON - async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = await super().handle_get_game_setting_api_request(data) + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.30.00" return ret diff --git a/titles/chuni/amazonplus.py b/titles/chuni/amazonplus.py index aa3108e..ea8d704 100644 --- a/titles/chuni/amazonplus.py +++ b/titles/chuni/amazonplus.py @@ -1,9 +1,11 @@ -from typing import Dict +from datetime import datetime, timedelta +from typing import Dict, Any +import pytz from core.config import CoreConfig from titles.chuni.base import ChuniBase -from titles.chuni.config import ChuniConfig from titles.chuni.const import ChuniConstants +from titles.chuni.config import ChuniConfig class ChuniAmazonPlus(ChuniBase): @@ -11,7 +13,7 @@ class ChuniAmazonPlus(ChuniBase): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_AMAZON_PLUS - async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = await super().handle_get_game_setting_api_request(data) + 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" return ret diff --git a/titles/chuni/base.py b/titles/chuni/base.py index 0224db0..674fb9c 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -1,16 +1,17 @@ import logging +import json from datetime import datetime, timedelta -from typing import Any, Dict, List +from time import strftime import pytz +from typing import Dict, Any, List + from core.config import CoreConfig -from titles.chuni.config import ChuniConfig from titles.chuni.const import ChuniConstants from titles.chuni.database import ChuniData - +from titles.chuni.config import ChuniConfig SCORE_BUFFER = {} - class ChuniBase: def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None: self.core_cfg = core_cfg @@ -21,7 +22,7 @@ class ChuniBase: self.game = ChuniConstants.GAME_CODE self.version = ChuniConstants.VER_CHUNITHM - async def handle_game_login_api_request(self, data: Dict) -> Dict: + def handle_game_login_api_request(self, data: Dict) -> Dict: """ Handles the login bonus logic, required for the game because getUserLoginBonus gets called after getUserItem and therefore the @@ -37,22 +38,20 @@ class ChuniBase: return {"returnCode": 1} user_id = data["userId"] - login_bonus_presets = await self.data.static.get_login_bonus_presets( - self.version - ) + login_bonus_presets = self.data.static.get_login_bonus_presets(self.version) for preset in login_bonus_presets: # check if a user already has some pogress and if not add the # login bonus entry - user_login_bonus = await self.data.item.get_login_bonus( + user_login_bonus = self.data.item.get_login_bonus( user_id, self.version, preset["presetId"] ) if user_login_bonus is None: - await self.data.item.put_login_bonus( + self.data.item.put_login_bonus( user_id, self.version, preset["presetId"] ) # yeah i'm lazy - user_login_bonus = await self.data.item.get_login_bonus( + user_login_bonus = self.data.item.get_login_bonus( user_id, self.version, preset["presetId"] ) @@ -68,7 +67,7 @@ class ChuniBase: bonus_count = user_login_bonus["bonusCount"] + 1 last_update_date = datetime.now() - all_login_boni = await self.data.static.get_login_bonus( + all_login_boni = self.data.static.get_login_bonus( self.version, preset["presetId"] ) @@ -92,13 +91,13 @@ class ChuniBase: is_finished = True # grab the item for the corresponding day - login_item = await self.data.static.get_login_bonus_by_required_days( + login_item = self.data.static.get_login_bonus_by_required_days( self.version, preset["presetId"], bonus_count ) if login_item is not None: # now add the present to the database so the # handle_get_user_item_api_request can grab them - await self.data.item.put_item( + self.data.item.put_item( user_id, { "itemId": login_item["presentId"], @@ -108,7 +107,7 @@ class ChuniBase: }, ) - await self.data.item.put_login_bonus( + self.data.item.put_login_bonus( user_id, self.version, preset["presetId"], @@ -120,12 +119,12 @@ class ChuniBase: return {"returnCode": 1} - async def handle_game_logout_api_request(self, data: Dict) -> Dict: + def handle_game_logout_api_request(self, data: Dict) -> Dict: # self.data.base.log_event("chuni", "logout", logging.INFO, {"version": self.version, "user": data["userId"]}) return {"returnCode": 1} - async def handle_get_game_charge_api_request(self, data: Dict) -> Dict: - game_charge_list = await self.data.static.get_enabled_charges(self.version) + def handle_get_game_charge_api_request(self, data: Dict) -> Dict: + game_charge_list = self.data.static.get_enabled_charges(self.version) if game_charge_list is None or len(game_charge_list) == 0: return {"length": 0, "gameChargeList": []} @@ -146,8 +145,8 @@ class ChuniBase: ) return {"length": len(charges), "gameChargeList": charges} - async def handle_get_game_event_api_request(self, data: Dict) -> Dict: - game_events = await self.data.static.get_enabled_events(self.version) + def handle_get_game_event_api_request(self, data: Dict) -> Dict: + game_events = self.data.static.get_enabled_events(self.version) if game_events is None or len(game_events) == 0: self.logger.warning("No enabled events, did you run the reader?") @@ -178,39 +177,32 @@ class ChuniBase: "gameEventList": event_list, } - async def handle_get_game_idlist_api_request(self, data: Dict) -> Dict: + def handle_get_game_idlist_api_request(self, data: Dict) -> Dict: return {"type": data["type"], "length": 0, "gameIdlistList": []} - async def handle_get_game_message_api_request(self, data: Dict) -> Dict: + def handle_get_game_message_api_request(self, data: Dict) -> Dict: return { - "type": data["type"], - "length": 1, - "gameMessageList": [ - { - "id": 1, - "type": 1, - "message": f"Welcome to {self.core_cfg.server.name} network!" - if not self.game_cfg.server.news_msg - else self.game_cfg.server.news_msg, - "startDate": "2017-12-05 07:00:00.0", - "endDate": "2099-12-31 00:00:00.0", - } - ], + "type": data["type"], + "length": 1, + "gameMessageList": [{ + "id": 1, + "type": 1, + "message": f"Welcome to {self.core_cfg.server.name} network!" if not self.game_cfg.server.news_msg else self.game_cfg.server.news_msg, + "startDate": "2017-12-05 07:00:00.0", + "endDate": "2099-12-31 00:00:00.0" + }] } - async def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: - rankings = await self.data.score.get_rankings(self.version) + def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: + rankings = self.data.score.get_rankings(self.version) return {"type": data["type"], "gameRankingList": rankings} - async def handle_get_game_sale_api_request(self, data: Dict) -> Dict: + def handle_get_game_sale_api_request(self, data: Dict) -> Dict: return {"type": data["type"], "length": 0, "gameSaleList": []} - async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: # 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 == "" - ): + 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 ) @@ -219,29 +211,15 @@ class ChuniBase: ) else: # get current datetime in JST - current_jst = datetime.now(pytz.timezone("Asia/Tokyo")).date() + 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" - ) + 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"), - ) + 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) @@ -262,9 +240,8 @@ class ChuniBase: "isDumpUpload": "false", "isAou": "false", } - - async def handle_get_user_activity_api_request(self, data: Dict) -> Dict: - user_activity_list = await self.data.profile.get_profile_activity( + def handle_get_user_activity_api_request(self, data: Dict) -> Dict: + user_activity_list = self.data.profile.get_profile_activity( data["userId"], data["kind"] ) @@ -284,8 +261,8 @@ class ChuniBase: "userActivityList": activity_list, } - async def handle_get_user_character_api_request(self, data: Dict) -> Dict: - characters = await self.data.item.get_characters(data["userId"]) + def handle_get_user_character_api_request(self, data: Dict) -> Dict: + characters = self.data.item.get_characters(data["userId"]) if characters is None: return { "userId": data["userId"], @@ -319,8 +296,8 @@ class ChuniBase: "userCharacterList": character_list, } - async def handle_get_user_charge_api_request(self, data: Dict) -> Dict: - user_charge_list = await self.data.profile.get_profile_charge(data["userId"]) + def handle_get_user_charge_api_request(self, data: Dict) -> Dict: + user_charge_list = self.data.profile.get_profile_charge(data["userId"]) charge_list = [] for charge in user_charge_list: @@ -335,15 +312,15 @@ class ChuniBase: "userChargeList": charge_list, } - async def handle_get_user_recent_player_api_request(self, data: Dict) -> Dict: + def handle_get_user_recent_player_api_request(self, data: Dict) -> Dict: return { "userId": data["userId"], "length": 0, - "userRecentPlayerList": [], # playUserId, playUserName, playDate, friendPoint + "userRecentPlayerList": [], # playUserId, playUserName, playDate, friendPoint } - async def handle_get_user_course_api_request(self, data: Dict) -> Dict: - user_course_list = await self.data.score.get_courses(data["userId"]) + def handle_get_user_course_api_request(self, data: Dict) -> Dict: + user_course_list = self.data.score.get_courses(data["userId"]) if user_course_list is None: return { "userId": data["userId"], @@ -377,8 +354,8 @@ class ChuniBase: "userCourseList": course_list, } - async def handle_get_user_data_api_request(self, data: Dict) -> Dict: - p = await self.data.profile.get_profile_data(data["userId"], self.version) + 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 {} @@ -389,8 +366,8 @@ class ChuniBase: return {"userId": data["userId"], "userData": profile} - async def handle_get_user_data_ex_api_request(self, data: Dict) -> Dict: - p = await self.data.profile.get_profile_data_ex(data["userId"], self.version) + def handle_get_user_data_ex_api_request(self, data: Dict) -> Dict: + p = self.data.profile.get_profile_data_ex(data["userId"], self.version) if p is None: return {} @@ -401,8 +378,8 @@ class ChuniBase: return {"userId": data["userId"], "userDataEx": profile} - async def handle_get_user_duel_api_request(self, data: Dict) -> Dict: - user_duel_list = await self.data.item.get_duels(data["userId"]) + def handle_get_user_duel_api_request(self, data: Dict) -> Dict: + user_duel_list = self.data.item.get_duels(data["userId"]) if user_duel_list is None: return {} @@ -419,21 +396,27 @@ class ChuniBase: "userDuelList": duel_list, } - async def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict: - p = await self.data.profile.get_rival(data["rivalId"]) + def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict: + p = self.data.profile.get_rival(data["rivalId"]) if p is None: return {} - userRivalData = {"rivalId": p.user, "rivalName": p.userName} - return {"userId": data["userId"], "userRivalData": userRivalData} - - async def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict: + userRivalData = { + "rivalId": p.user, + "rivalName": p.userName + } + return { + "userId": data["userId"], + "userRivalData": userRivalData + } + + def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict: rival_id = data["rivalId"] next_index = int(data["nextIndex"]) max_count = int(data["maxCount"]) user_rival_music_list = [] # Fetch all the rival music entries for the user - all_entries = await self.data.score.get_rival_music(rival_id) + all_entries = self.data.score.get_rival_music(rival_id) # Process the entries based on max_count and nextIndex for music in all_entries: @@ -443,33 +426,23 @@ class ChuniBase: rank = music["scoreRank"] # Create a music entry for the current music_id if it's unique - music_entry = next( - ( - entry - for entry in user_rival_music_list - if entry["musicId"] == music_id - ), - None, - ) + music_entry = next((entry for entry in user_rival_music_list if entry["musicId"] == music_id), None) if music_entry is None: music_entry = { "musicId": music_id, "length": 0, - "userRivalMusicDetailList": [], + "userRivalMusicDetailList": [] } user_rival_music_list.append(music_entry) # Create a level entry for the current level if it's unique or has a higher score - level_entry = next( - ( - entry - for entry in music_entry["userRivalMusicDetailList"] - if entry["level"] == level - ), - None, - ) + level_entry = next((entry for entry in music_entry["userRivalMusicDetailList"] if entry["level"] == level), None) if level_entry is None: - level_entry = {"level": level, "scoreMax": score, "scoreRank": rank} + level_entry = { + "level": level, + "scoreMax": score, + "scoreRank": rank + } music_entry["userRivalMusicDetailList"].append(level_entry) elif score > level_entry["scoreMax"]: level_entry["scoreMax"] = score @@ -483,25 +456,18 @@ class ChuniBase: result = { "userId": data["userId"], "rivalId": data["rivalId"], - "nextIndex": str( - next_index - + len(user_rival_music_list[next_index : next_index + max_count]) - if max_count - <= len(user_rival_music_list[next_index : next_index + max_count]) - else -1 - ), - "userRivalMusicList": user_rival_music_list[ - next_index : next_index + max_count - ], + "nextIndex": str(next_index + len(user_rival_music_list[next_index: next_index + max_count]) if max_count <= len(user_rival_music_list[next_index: next_index + max_count]) else -1), + "userRivalMusicList": user_rival_music_list[next_index: next_index + max_count] } return result - async def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict: + + def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict: user_fav_item_list = [] # still needs to be implemented on WebUI # 1: Music, 2: User, 3: Character - fav_list = await self.data.item.get_all_favorites( + fav_list = self.data.item.get_all_favorites( data["userId"], self.version, fav_kind=int(data["kind"]) ) if fav_list is not None: @@ -516,17 +482,17 @@ class ChuniBase: "userFavoriteItemList": user_fav_item_list, } - async def handle_get_user_favorite_music_api_request(self, data: Dict) -> Dict: + def handle_get_user_favorite_music_api_request(self, data: Dict) -> Dict: """ This is handled via the webui, which we don't have right now """ return {"userId": data["userId"], "length": 0, "userFavoriteMusicList": []} - async def handle_get_user_item_api_request(self, data: Dict) -> Dict: + def handle_get_user_item_api_request(self, data: Dict) -> Dict: kind = int(int(data["nextIndex"]) / 10000000000) next_idx = int(int(data["nextIndex"]) % 10000000000) - user_item_list = await self.data.item.get_items(data["userId"], kind) + user_item_list = self.data.item.get_items(data["userId"], kind) if user_item_list is None or len(user_item_list) == 0: return { @@ -560,11 +526,9 @@ class ChuniBase: "userItemList": items, } - async def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: + def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: user_id = data["userId"] - user_login_bonus = await self.data.item.get_all_login_bonus( - user_id, self.version - ) + user_login_bonus = self.data.item.get_all_login_bonus(user_id, self.version) # ignore the loginBonus request if its disabled in config if user_login_bonus is None or not self.game_cfg.mods.use_login_bonus: return {"userId": user_id, "length": 0, "userLoginBonusList": []} @@ -588,8 +552,8 @@ class ChuniBase: "userLoginBonusList": user_login_list, } - async def handle_get_user_map_api_request(self, data: Dict) -> Dict: - user_map_list = await self.data.item.get_maps(data["userId"]) + def handle_get_user_map_api_request(self, data: Dict) -> Dict: + user_map_list = self.data.item.get_maps(data["userId"]) if user_map_list is None: return {} @@ -606,8 +570,8 @@ class ChuniBase: "userMapList": map_list, } - async def handle_get_user_music_api_request(self, data: Dict) -> Dict: - music_detail = await self.data.score.get_scores(data["userId"]) + def handle_get_user_music_api_request(self, data: Dict) -> Dict: + music_detail = self.data.score.get_scores(data["userId"]) if music_detail is None: return { "userId": data["userId"], @@ -643,19 +607,18 @@ class ChuniBase: if len(song_list) >= max_ct: break - - for songIdx in range(len(song_list)): - for recordIdx in range(x + 1, len(music_detail)): - if ( - song_list[songIdx]["userMusicDetailList"][0]["musicId"] - == music_detail[recordIdx]["musicId"] - ): - music = music_detail[recordIdx]._asdict() - music.pop("user") - music.pop("id") - song_list[songIdx]["userMusicDetailList"].append(music) - song_list[songIdx]["length"] += 1 - + + try: + while song_list[-1]["userMusicDetailList"][0]["musicId"] == music_detail[x + 1]["musicId"]: + music = music_detail[x + 1]._asdict() + music.pop("user") + music.pop("id") + song_list[-1]["userMusicDetailList"].append(music) + song_list[-1]["length"] += 1 + x += 1 + except IndexError: + pass + if len(song_list) >= max_ct: next_idx += len(song_list) else: @@ -668,8 +631,8 @@ class ChuniBase: "userMusicList": song_list, # 240 } - async def handle_get_user_option_api_request(self, data: Dict) -> Dict: - p = await self.data.profile.get_profile_option(data["userId"]) + def handle_get_user_option_api_request(self, data: Dict) -> Dict: + p = self.data.profile.get_profile_option(data["userId"]) option = p._asdict() option.pop("id") @@ -677,8 +640,8 @@ class ChuniBase: return {"userId": data["userId"], "userGameOption": option} - async def handle_get_user_option_ex_api_request(self, data: Dict) -> Dict: - p = await self.data.profile.get_profile_option_ex(data["userId"]) + def handle_get_user_option_ex_api_request(self, data: Dict) -> Dict: + p = self.data.profile.get_profile_option_ex(data["userId"]) option = p._asdict() option.pop("id") @@ -689,13 +652,11 @@ class ChuniBase: def read_wtf8(self, src): return bytes([ord(c) for c in src]).decode("utf-8") - async def handle_get_user_preview_api_request(self, data: Dict) -> Dict: - profile = await self.data.profile.get_profile_preview( - data["userId"], self.version - ) + 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: return None - profile_character = await self.data.item.get_character( + profile_character = self.data.item.get_character( data["userId"], profile["characterId"] ) @@ -733,10 +694,8 @@ class ChuniBase: "userNameEx": profile["userName"], } - async def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict: - recent_rating_list = await self.data.profile.get_profile_recent_rating( - data["userId"] - ) + def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict: + recent_rating_list = self.data.profile.get_profile_recent_rating(data["userId"]) if recent_rating_list is None: return { "userId": data["userId"], @@ -750,7 +709,7 @@ class ChuniBase: "userRecentRatingList": recent_rating_list["recentRating"], } - async def handle_get_user_region_api_request(self, data: Dict) -> Dict: + def handle_get_user_region_api_request(self, data: Dict) -> Dict: # TODO: Region return { "userId": data["userId"], @@ -758,22 +717,22 @@ class ChuniBase: "userRegionList": [], } - async def handle_get_user_team_api_request(self, data: Dict) -> Dict: + def handle_get_user_team_api_request(self, data: Dict) -> Dict: # Default values team_id = 65535 team_name = self.game_cfg.team.team_name team_rank = 0 # Get user profile - profile = await self.data.profile.get_profile_data(data["userId"], self.version) + profile = self.data.profile.get_profile_data(data["userId"], self.version) if profile and profile["teamId"]: # Get team by id - team = await self.data.profile.get_team_by_id(profile["teamId"]) + team = self.data.profile.get_team_by_id(profile["teamId"]) if team: team_id = team["id"] team_name = team["teamName"] - team_rank = await self.data.profile.get_team_rank(team["id"]) + team_rank = self.data.profile.get_team_rank(team["id"]) # Don't return anything if no team name has been defined for defaults and there is no team set for the player if not profile["teamId"] and team_name == "": @@ -792,8 +751,8 @@ class ChuniBase: "aggrDate": data["playDate"], }, } - - async def handle_get_team_course_setting_api_request(self, data: Dict) -> Dict: + + def handle_get_team_course_setting_api_request(self, data: Dict) -> Dict: return { "userId": data["userId"], "length": 0, @@ -801,9 +760,7 @@ class ChuniBase: "teamCourseSettingList": [], } - async def handle_get_team_course_setting_api_request_proto( - self, data: Dict - ) -> Dict: + def handle_get_team_course_setting_api_request_proto(self, data: Dict) -> Dict: return { "userId": data["userId"], "length": 1, @@ -818,24 +775,24 @@ class ChuniBase: "teamCourseMusicList": [ {"track": 184, "type": 1, "level": 3, "selectLevel": -1}, {"track": 184, "type": 1, "level": 3, "selectLevel": -1}, - {"track": 184, "type": 1, "level": 3, "selectLevel": -1}, + {"track": 184, "type": 1, "level": 3, "selectLevel": -1} ], "teamCourseRankingInfoList": [], "recodeDate": "2099-12-31 11:59:99.0", - "isPlayed": False, + "isPlayed": False } ], } - async def handle_get_team_course_rule_api_request(self, data: Dict) -> Dict: + def handle_get_team_course_rule_api_request(self, data: Dict) -> Dict: return { "userId": data["userId"], "length": 0, "nextIndex": -1, - "teamCourseRuleList": [], + "teamCourseRuleList": [] } - async def handle_get_team_course_rule_api_request_proto(self, data: Dict) -> Dict: + def handle_get_team_course_rule_api_request_proto(self, data: Dict) -> Dict: return { "userId": data["userId"], "length": 1, @@ -847,12 +804,12 @@ class ChuniBase: "damageMiss": 1, "damageAttack": 1, "damageJustice": 1, - "damageJusticeC": 1, + "damageJusticeC": 1 } ], } - async def handle_upsert_user_all_api_request(self, data: Dict) -> Dict: + def handle_upsert_user_all_api_request(self, data: Dict) -> Dict: upsert = data["upsertUserAll"] user_id = data["userId"] @@ -864,189 +821,137 @@ class ChuniBase: except Exception: pass - await self.data.profile.put_profile_data( + self.data.profile.put_profile_data( user_id, self.version, upsert["userData"][0] ) if "userDataEx" in upsert: - await self.data.profile.put_profile_data_ex( + self.data.profile.put_profile_data_ex( user_id, self.version, upsert["userDataEx"][0] ) if "userGameOption" in upsert: - await self.data.profile.put_profile_option( - user_id, upsert["userGameOption"][0] - ) + self.data.profile.put_profile_option(user_id, upsert["userGameOption"][0]) if "userGameOptionEx" in upsert: - await self.data.profile.put_profile_option_ex( + self.data.profile.put_profile_option_ex( user_id, upsert["userGameOptionEx"][0] ) if "userRecentRatingList" in upsert: - await self.data.profile.put_profile_recent_rating( + self.data.profile.put_profile_recent_rating( user_id, upsert["userRecentRatingList"] ) if "userCharacterList" in upsert: for character in upsert["userCharacterList"]: - await self.data.item.put_character(user_id, character) + self.data.item.put_character(user_id, character) if "userMapList" in upsert: for map in upsert["userMapList"]: - await self.data.item.put_map(user_id, map) + self.data.item.put_map(user_id, map) if "userCourseList" in upsert: for course in upsert["userCourseList"]: - await self.data.score.put_course(user_id, course) + self.data.score.put_course(user_id, course) if "userDuelList" in upsert: for duel in upsert["userDuelList"]: - await self.data.item.put_duel(user_id, duel) + self.data.item.put_duel(user_id, duel) if "userItemList" in upsert: for item in upsert["userItemList"]: - await self.data.item.put_item(user_id, item) + self.data.item.put_item(user_id, item) if "userActivityList" in upsert: for activity in upsert["userActivityList"]: - await self.data.profile.put_profile_activity(user_id, activity) + self.data.profile.put_profile_activity(user_id, activity) if "userChargeList" in upsert: for charge in upsert["userChargeList"]: - await self.data.profile.put_profile_charge(user_id, charge) + self.data.profile.put_profile_charge(user_id, charge) if "userMusicDetailList" in upsert: for song in upsert["userMusicDetailList"]: - await self.data.score.put_score(user_id, song) + self.data.score.put_score(user_id, song) if "userPlaylogList" in upsert: for playlog in upsert["userPlaylogList"]: # convert the player names to utf-8 if playlog["playedUserName1"] is not None: - playlog["playedUserName1"] = self.read_wtf8( - playlog["playedUserName1"] - ) + playlog["playedUserName1"] = self.read_wtf8(playlog["playedUserName1"]) if playlog["playedUserName2"] is not None: - playlog["playedUserName2"] = self.read_wtf8( - playlog["playedUserName2"] - ) + playlog["playedUserName2"] = self.read_wtf8(playlog["playedUserName2"]) if playlog["playedUserName3"] is not None: - playlog["playedUserName3"] = self.read_wtf8( - playlog["playedUserName3"] - ) - await self.data.score.put_playlog(user_id, playlog, self.version) + playlog["playedUserName3"] = self.read_wtf8(playlog["playedUserName3"]) + self.data.score.put_playlog(user_id, playlog, self.version) if "userTeamPoint" in upsert: team_points = upsert["userTeamPoint"] try: for tp in team_points: - if tp["teamId"] != "65535": + if tp["teamId"] != '65535': # Fetch the current team data - current_team = await self.data.profile.get_team_by_id( - tp["teamId"] - ) + current_team = self.data.profile.get_team_by_id(tp["teamId"]) # Calculate the new teamPoint - new_team_point = ( - int(tp["teamPoint"]) + current_team["teamPoint"] - ) + new_team_point = int(tp["teamPoint"]) + current_team["teamPoint"] # Prepare the data to update - team_data = {"teamPoint": new_team_point} + team_data = { + "teamPoint": new_team_point + } # Update the team data - await self.data.profile.update_team(tp["teamId"], team_data) + self.data.profile.update_team(tp["teamId"], team_data) except: - pass # Probably a better way to catch if the team is not set yet (new profiles), but let's just pass + pass # Probably a better way to catch if the team is not set yet (new profiles), but let's just pass if "userMapAreaList" in upsert: for map_area in upsert["userMapAreaList"]: - await self.data.item.put_map_area(user_id, map_area) + self.data.item.put_map_area(user_id, map_area) if "userOverPowerList" in upsert: for overpower in upsert["userOverPowerList"]: - await self.data.profile.put_profile_overpower(user_id, overpower) + self.data.profile.put_profile_overpower(user_id, overpower) if "userEmoneyList" in upsert: for emoney in upsert["userEmoneyList"]: - await self.data.profile.put_profile_emoney(user_id, emoney) + self.data.profile.put_profile_emoney(user_id, emoney) if "userLoginBonusList" in upsert: for login in upsert["userLoginBonusList"]: - await self.data.item.put_login_bonus( + self.data.item.put_login_bonus( user_id, self.version, login["presetId"], isWatched=True ) - - if ( - "userRecentPlayerList" in upsert - ): # TODO: Seen in Air, maybe implement sometime + + if "userRecentPlayerList" in upsert: # TODO: Seen in Air, maybe implement sometime for rp in upsert["userRecentPlayerList"]: pass -<<<<<<< Updated upstream - for rating_type in {"userRatingBaseList", "userRatingBaseHotList", "userRatingBaseNextList"}: - if rating_type not in upsert: - continue - - await self.data.profile.put_profile_rating( - user_id, - self.version, - rating_type, - upsert[rating_type], - ) -======= - # added in LUMINOUS - if "userCMissionList" in upsert: - for cmission in upsert["userCMissionList"]: - mission_id = cmission["missionId"] - - await self.data.item.put_cmission( - user_id, - { - "missionId": mission_id, - "point": cmission["point"], - }, - ) - - for progress in cmission["userCMissionProgressList"]: - await self.data.item.put_cmission_progress( - user_id, mission_id, progress - ) - - if "userNetBattleData" in upsert: - net_battle = upsert["userNetBattleData"][0] - - # fix the boolean - net_battle["isRankUpChallengeFailed"] = ( - False if net_battle["isRankUpChallengeFailed"] == "false" else True - ) - await self.data.profile.put_net_battle(user_id, net_battle) ->>>>>>> Stashed changes - return {"returnCode": "1"} - async def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: + def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: # add tickets after they got bought, this makes sure the tickets are # still valid after an unsuccessful logout - await self.data.profile.put_profile_charge(data["userId"], data["userCharge"]) + self.data.profile.put_profile_charge(data["userId"], data["userCharge"]) return {"returnCode": "1"} - async def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: + def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: return {"returnCode": "1"} - async def handle_upsert_client_develop_api_request(self, data: Dict) -> Dict: + def handle_upsert_client_develop_api_request(self, data: Dict) -> Dict: return {"returnCode": "1"} - async def handle_upsert_client_error_api_request(self, data: Dict) -> Dict: + def handle_upsert_client_error_api_request(self, data: Dict) -> Dict: return {"returnCode": "1"} - async def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: + def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: return {"returnCode": "1"} - async def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict: + def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict: return {"returnCode": "1"} - async def handle_get_user_net_battle_data_api_request(self, data: Dict) -> Dict: + def handle_get_user_net_battle_data_api_request(self, data: Dict) -> Dict: return { "userId": data["userId"], "userNetBattleData": {"recentNBSelectMusicList": []}, - } + } \ No newline at end of file diff --git a/titles/chuni/config.py b/titles/chuni/config.py index 2527fdc..05cc8ad 100644 --- a/titles/chuni/config.py +++ b/titles/chuni/config.py @@ -1,6 +1,5 @@ -from typing import Dict - from core.config import CoreConfig +from typing import Dict class ChuniServerConfig: @@ -20,7 +19,7 @@ class ChuniServerConfig: self.__config, "chuni", "server", "loglevel", default="info" ) ) - + @property def news_msg(self) -> str: return CoreConfig.get_config_field( @@ -37,7 +36,6 @@ class ChuniTeamConfig: return CoreConfig.get_config_field( self.__config, "chuni", "team", "name", default="" ) - @property def rank_scale(self) -> str: return CoreConfig.get_config_field( @@ -92,29 +90,6 @@ class ChuniCryptoConfig: ) -class ChuniMatchingConfig: - def __init__(self, parent_config: "ChuniConfig") -> None: - self.__config = parent_config - - @property - def enable(self) -> bool: - return CoreConfig.get_config_field( - self.__config, "chuni", "matching", "enable", default=False - ) - - @property - def match_time_limit(self) -> int: - return CoreConfig.get_config_field( - self.__config, "chuni", "matching", "match_time_limit", default=60 - ) - - @property - def match_error_limit(self) -> int: - return CoreConfig.get_config_field( - self.__config, "chuni", "matching", "match_error_limit", default=9999 - ) - - class ChuniConfig(dict): def __init__(self) -> None: self.server = ChuniServerConfig(self) @@ -122,4 +97,3 @@ class ChuniConfig(dict): self.mods = ChuniModsConfig(self) self.version = ChuniVersionConfig(self) self.crypto = ChuniCryptoConfig(self) - self.matching = ChuniMatchingConfig(self) diff --git a/titles/chuni/const.py b/titles/chuni/const.py index 99c7057..3e83378 100644 --- a/titles/chuni/const.py +++ b/titles/chuni/const.py @@ -20,7 +20,6 @@ class ChuniConstants: VER_CHUNITHM_NEW_PLUS = 12 VER_CHUNITHM_SUN = 13 VER_CHUNITHM_SUN_PLUS = 14 - VER_CHUNITHM_LUMINOUS = 15 VERSION_NAMES = [ "CHUNITHM", "CHUNITHM PLUS", @@ -36,10 +35,9 @@ class ChuniConstants: "CHUNITHM NEW!!", "CHUNITHM NEW PLUS!!", "CHUNITHM SUN", - "CHUNITHM SUN PLUS", - "CHUNITHM LUMINOUS", + "CHUNITHM SUN PLUS" ] @classmethod def game_ver_to_string(cls, ver: int): - return cls.VERSION_NAMES[ver] + return cls.VERSION_NAMES[ver] \ No newline at end of file diff --git a/titles/chuni/crystal.py b/titles/chuni/crystal.py index bf11b4b..a727ac3 100644 --- a/titles/chuni/crystal.py +++ b/titles/chuni/crystal.py @@ -1,9 +1,11 @@ -from typing import Dict +from datetime import datetime, timedelta +from typing import Dict, Any +import pytz from core.config import CoreConfig from titles.chuni.base import ChuniBase -from titles.chuni.config import ChuniConfig from titles.chuni.const import ChuniConstants +from titles.chuni.config import ChuniConfig class ChuniCrystal(ChuniBase): @@ -11,7 +13,7 @@ class ChuniCrystal(ChuniBase): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_CRYSTAL - async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = await super().handle_get_game_setting_api_request(data) + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.40.00" return ret diff --git a/titles/chuni/crystalplus.py b/titles/chuni/crystalplus.py index 445bd83..fbb3969 100644 --- a/titles/chuni/crystalplus.py +++ b/titles/chuni/crystalplus.py @@ -1,9 +1,11 @@ -from typing import Dict +from datetime import datetime, timedelta +from typing import Dict, Any +import pytz from core.config import CoreConfig from titles.chuni.base import ChuniBase -from titles.chuni.config import ChuniConfig from titles.chuni.const import ChuniConstants +from titles.chuni.config import ChuniConfig class ChuniCrystalPlus(ChuniBase): @@ -11,7 +13,7 @@ class ChuniCrystalPlus(ChuniBase): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS - async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = await super().handle_get_game_setting_api_request(data) + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.45.00" return ret diff --git a/titles/chuni/database.py b/titles/chuni/database.py index 652029d..eeb588c 100644 --- a/titles/chuni/database.py +++ b/titles/chuni/database.py @@ -1,5 +1,5 @@ -from core.config import CoreConfig from core.data import Data +from core.config import CoreConfig from titles.chuni.schema import * diff --git a/titles/chuni/frontend.py b/titles/chuni/frontend.py deleted file mode 100644 index 55c3d7e..0000000 --- a/titles/chuni/frontend.py +++ /dev/null @@ -1,89 +0,0 @@ -from os import path -from typing import List - -import jinja2 -import yaml -from core.config import CoreConfig -from core.frontend import FE_Base, UserSession -from starlette.requests import Request -from starlette.responses import RedirectResponse, Response -from starlette.routing import Route - -from .config import ChuniConfig -from .const import ChuniConstants -from .database import ChuniData - - -class ChuniFrontend(FE_Base): - def __init__( - self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str - ) -> None: - super().__init__(cfg, environment) - self.data = ChuniData(cfg) - self.game_cfg = ChuniConfig() - if path.exists(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"): - self.game_cfg.update( - yaml.safe_load(open(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}")) - ) - self.nav_name = "Chunithm" - - def get_routes(self) -> List[Route]: - return [ - Route("/", self.render_GET, methods=["GET"]), - Route("/update.name", self.update_name, methods=["POST"]), - ] - - async def render_GET(self, request: Request) -> bytes: - template = self.environment.get_template( - "titles/chuni/templates/chuni_index.jinja" - ) - usr_sesh = self.validate_session(request) - if not usr_sesh: - usr_sesh = UserSession() - - return Response( - template.render( - title=f"{self.core_config.server.name} | {self.nav_name}", - game_list=self.environment.globals["game_list"], - sesh=vars(usr_sesh), - ), - media_type="text/html; charset=utf-8", - ) - - async def update_name(self, request: Request) -> bytes: - usr_sesh = self.validate_session(request) - if not usr_sesh: - return RedirectResponse("/gate/", 303) - - new_name: str = request.query_params.get("new_name", "") - new_name_full = "" - - if not new_name: - return RedirectResponse("/gate/?e=4", 303) - - if len(new_name) > 8: - return RedirectResponse("/gate/?e=8", 303) - - for x in new_name: # FIXME: This will let some invalid characters through atm - o = ord(x) - try: - if o == 0x20: - new_name_full += chr(0x3000) - elif o < 0x7F and o > 0x20: - new_name_full += chr(o + 0xFEE0) - elif o <= 0x7F: - self.logger.warn(f"Invalid ascii character {o:02X}") - return RedirectResponse("/gate/?e=4", 303) - else: - new_name_full += x - - except Exception as e: - self.logger.error( - f"Something went wrong parsing character {o:04X} - {e}" - ) - return RedirectResponse("/gate/?e=4", 303) - - if not await self.data.profile.update_name(usr_sesh, new_name_full): - return RedirectResponse("/gate/?e=999", 303) - - return RedirectResponse("/gate/?s=1", 303) diff --git a/titles/chuni/index.py b/titles/chuni/index.py index b4b21ec..fa8a394 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -1,40 +1,35 @@ -import json -import logging -import string -import zlib +from twisted.web.http import Request +import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler -from os import path -from typing import Dict, List, Tuple - -import coloredlogs -import inflection +import zlib import yaml +import json +import inflection +import string +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 import CoreConfig, Utils from core.title import BaseServlet -from Crypto.Cipher import AES -from Crypto.Hash import SHA1 -from Crypto.Protocol.KDF import PBKDF2 -from Crypto.Util.Padding import pad -from starlette.requests import Request -from starlette.responses import Response -from starlette.routing import Route - -from .air import ChuniAir -from .airplus import ChuniAirPlus -from .amazon import ChuniAmazon -from .amazonplus import ChuniAmazonPlus -from .base import ChuniBase from .config import ChuniConfig from .const import ChuniConstants -from .crystal import ChuniCrystal -from .crystalplus import ChuniCrystalPlus -from .luminous import ChuniLuminous -from .new import ChuniNew -from .newplus import ChuniNewPlus -from .paradise import ChuniParadise +from .base import ChuniBase from .plus import ChuniPlus +from .air import ChuniAir +from .airplus import ChuniAirPlus from .star import ChuniStar from .starplus import ChuniStarPlus +from .amazon import ChuniAmazon +from .amazonplus import ChuniAmazonPlus +from .crystal import ChuniCrystal +from .crystalplus import ChuniCrystalPlus +from .paradise import ChuniParadise +from .new import ChuniNew +from .newplus import ChuniNewPlus from .sun import ChuniSun from .sunplus import ChuniSunPlus @@ -65,7 +60,6 @@ class ChuniServlet(BaseServlet): ChuniNewPlus, ChuniSun, ChuniSunPlus, - ChuniLuminous, ] self.logger = logging.getLogger("chuni") @@ -108,15 +102,13 @@ class ChuniServlet(BaseServlet): for method in method_list: method_fixed = inflection.camelize(method)[6:-7] # number of iterations was changed to 70 in SUN and then to 36 - if version == ChuniConstants.VER_CHUNITHM_LUMINOUS: - iter_count = 8 - elif version == ChuniConstants.VER_CHUNITHM_SUN_PLUS: + if version == ChuniConstants.VER_CHUNITHM_SUN_PLUS: iter_count = 36 elif version == ChuniConstants.VER_CHUNITHM_SUN: iter_count = 70 else: iter_count = 44 - + hash = PBKDF2( method_fixed, bytes.fromhex(keys[2]), @@ -125,15 +117,22 @@ class ChuniServlet(BaseServlet): hmac_hash_module=SHA1, ) - hashed_name = hash.hex()[ - :32 - ] # truncate unused bytes like the game does + 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()}" ) + def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: + return ( + [], + [ + ("render_POST", "/{game}/{version}/ChuniServlet/{endpoint}", {}), + ("render_POST", "/{game}/{version}/ChuniServlet/MatchingServer/{endpoint}", {}) + ] + ) + @classmethod def is_game_enabled( cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str @@ -149,119 +148,88 @@ class ChuniServlet(BaseServlet): return True - 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.server.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_code}/{game_ver}/", - self.core_cfg.server.hostname, - ) + 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_code}/{game_ver}/", self.core_cfg.title.hostname) - return ( - f"http://{self.core_cfg.server.hostname}/{game_code}/{game_ver}/", - self.core_cfg.server.hostname, - ) + return (f"http://{self.core_cfg.title.hostname}/{game_code}/{game_ver}/", self.core_cfg.title.hostname) - def get_routes(self) -> List[Route]: - return [ - Route( - "/{game:str}/{version:int}/ChuniServlet/{endpoint:str}", - self.render_POST, - methods=["POST"], - ), - Route( - "/{game:str}/{version:int}/ChuniServlet/MatchingServer/{endpoint:str}", - self.render_POST, - methods=["POST"], - ), - ] - - async def render_POST(self, request: Request) -> bytes: - endpoint: str = request.path_params.get("endpoint") - version: int = request.path_params.get("version") - game_code: str = request.path_params.get("game") + def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes: + endpoint = matchers['endpoint'] + version = int(matchers['version']) + game_code = matchers['game'] if endpoint.lower() == "ping": - return Response(zlib.compress(b'{"returnCode": "1"}')) + return zlib.compress(b'{"returnCode": "1"}') - req_raw = await request.body() + req_raw = request.content.getvalue() encrtped = False internal_ver = 0 client_ip = Utils.get_ip_addr(request) - if game_code == "SDHD" or game_code == "SDBT": # JP - if version < 105: # 1.0 - internal_ver = ChuniConstants.VER_CHUNITHM - elif version >= 105 and version < 110: # PLUS - internal_ver = ChuniConstants.VER_CHUNITHM_PLUS - elif version >= 110 and version < 115: # AIR - internal_ver = ChuniConstants.VER_CHUNITHM_AIR - elif version >= 115 and version < 120: # AIR PLUS - internal_ver = ChuniConstants.VER_CHUNITHM_AIR_PLUS - elif version >= 120 and version < 125: # STAR - internal_ver = ChuniConstants.VER_CHUNITHM_STAR - elif version >= 125 and version < 130: # STAR PLUS - internal_ver = ChuniConstants.VER_CHUNITHM_STAR_PLUS - elif version >= 130 and version < 135: # AMAZON - internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON - elif version >= 135 and version < 140: # AMAZON PLUS - internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON_PLUS - elif version >= 140 and version < 145: # CRYSTAL - internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL - elif version >= 145 and version < 150: # CRYSTAL PLUS - internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS - elif version >= 150 and version < 200: # PARADISE - internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE - elif version >= 200 and version < 205: # NEW!! - internal_ver = ChuniConstants.VER_CHUNITHM_NEW - elif version >= 205 and version < 210: # NEW PLUS!! - internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS - elif version >= 210 and version < 215: # SUN - internal_ver = ChuniConstants.VER_CHUNITHM_SUN - elif 215 <= version < 220: # SUN - internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS - elif version >= 220: # LUMINOUS - internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS - elif game_code == "SDGS": # Int - if version < 110: # SUPERSTAR - internal_ver = ( - ChuniConstants.VER_CHUNITHM_PARADISE - ) # FIXME: Not sure what was intended to go here? was just "PARADISE" - elif version >= 110 and version < 115: # NEW - internal_ver = ChuniConstants.VER_CHUNITHM_NEW - elif version >= 115 and version < 120: # NEW PLUS!! - internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS - elif version >= 120 and version < 125: # SUN - internal_ver = ChuniConstants.VER_CHUNITHM_SUN - elif 125 <= version < 130: # SUN PLUS - internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS - elif version >= 130: # LUMINOUS - internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS + if game_code == "SDHD" or game_code == "SDBT": # JP + if version < 105: # 1.0 + internal_ver = ChuniConstants.VER_CHUNITHM + elif version >= 105 and version < 110: # PLUS + internal_ver = ChuniConstants.VER_CHUNITHM_PLUS + elif version >= 110 and version < 115: # AIR + internal_ver = ChuniConstants.VER_CHUNITHM_AIR + elif version >= 115 and version < 120: # AIR PLUS + internal_ver = ChuniConstants.VER_CHUNITHM_AIR_PLUS + elif version >= 120 and version < 125: # STAR + internal_ver = ChuniConstants.VER_CHUNITHM_STAR + elif version >= 125 and version < 130: # STAR PLUS + internal_ver = ChuniConstants.VER_CHUNITHM_STAR_PLUS + elif version >= 130 and version < 135: # AMAZON + internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON + elif version >= 135 and version < 140: # AMAZON PLUS + internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON_PLUS + elif version >= 140 and version < 145: # CRYSTAL + internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL + elif version >= 145 and version < 150: # CRYSTAL PLUS + internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS + elif version >= 150 and version < 200: # PARADISE + internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE + elif version >= 200 and version < 205: # NEW!! + internal_ver = ChuniConstants.VER_CHUNITHM_NEW + elif version >= 205 and version < 210: # NEW PLUS!! + internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS + elif version >= 210 and version < 215: # SUN + internal_ver = ChuniConstants.VER_CHUNITHM_SUN + elif version >= 215: # SUN + internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS + elif game_code == "SDGS": # Int + if version < 110: # SUPERSTAR + internal_ver = ChuniConstants.PARADISE + elif version >= 110 and version < 115: # NEW + internal_ver = ChuniConstants.VER_CHUNITHM_NEW + elif version >= 115 and version < 120: # NEW PLUS!! + internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS + elif version >= 120 and version < 125: # SUN + internal_ver = ChuniConstants.VER_CHUNITHM_SUN + elif version >= 125: # SUN PLUS + internal_ver = ChuniConstants.VER_CHUNITHM_SUN_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 if internal_ver < ChuniConstants.VER_CHUNITHM_NEW: - endpoint = request.headers.get("User-Agent").split("#")[0] + endpoint = request.getHeader("User-Agent").split("#")[0] else: if internal_ver not in self.hash_table: self.logger.error( f"v{version} does not support encryption or no keys entered" ) - return Response(zlib.compress(b'{"stat": "0"}')) + return zlib.compress(b'{"stat": "0"}') elif endpoint.lower() not in self.hash_table[internal_ver]: self.logger.error( f"No hash found for v{version} endpoint {endpoint}" ) - return Response(zlib.compress(b'{"stat": "0"}')) + return zlib.compress(b'{"stat": "0"}') endpoint = self.hash_table[internal_ver][endpoint.lower()] @@ -278,7 +246,7 @@ class ChuniServlet(BaseServlet): self.logger.error( f"Failed to decrypt v{version} request to {endpoint} -> {e}" ) - return Response(zlib.compress(b'{"stat": "0"}')) + return zlib.compress(b'{"stat": "0"}') encrtped = True @@ -290,7 +258,7 @@ class ChuniServlet(BaseServlet): self.logger.error( f"Unencrypted v{version} {endpoint} request, but config is set to encrypted only: {req_raw}" ) - return Response(zlib.compress(b'{"stat": "0"}')) + return zlib.compress(b'{"stat": "0"}') try: unzip = zlib.decompress(req_raw) @@ -299,7 +267,7 @@ class ChuniServlet(BaseServlet): self.logger.error( f"Failed to decompress v{version} {endpoint} request -> {e}" ) - return Response(zlib.compress(b'{"stat": "0"}')) + return b"" req_data = json.loads(unzip) @@ -317,13 +285,13 @@ class ChuniServlet(BaseServlet): else: try: handler = getattr(handler_cls, func_to_find) - resp = await handler(req_data) + resp = handler(req_data) except Exception as e: self.logger.error(f"Error handling v{version} method {endpoint} - {e}") - return Response(zlib.compress(b'{"stat": "0"}')) + return zlib.compress(b'{"stat": "0"}') - if resp is None: + if resp == None: resp = {"returnCode": 1} self.logger.debug(f"Response {resp}") @@ -331,7 +299,7 @@ class ChuniServlet(BaseServlet): zipped = zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) if not encrtped: - return Response(zipped) + return zipped padded = pad(zipped, 16) @@ -341,4 +309,4 @@ class ChuniServlet(BaseServlet): bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]), ) - return Response(crypt.encrypt(padded)) + return crypt.encrypt(padded) \ No newline at end of file diff --git a/titles/chuni/luminous.py b/titles/chuni/luminous.py deleted file mode 100644 index 5929bcb..0000000 --- a/titles/chuni/luminous.py +++ /dev/null @@ -1,72 +0,0 @@ -from typing import Dict - -from core.config import CoreConfig -from titles.chuni.config import ChuniConfig -from titles.chuni.const import ChuniConstants -from titles.chuni.sunplus import ChuniSunPlus - - -class ChuniLuminous(ChuniSunPlus): - def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None: - super().__init__(core_cfg, game_cfg) - self.version = ChuniConstants.VER_CHUNITHM_LUMINOUS - - async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: - user_data = await super().handle_cm_get_user_preview_api_request(data) - - user_data["lastDataVersion"] = "2.20.00" - return user_data - - async def handle_get_game_map_area_condition_api_request(self, data: Dict) -> Dict: - return {"length": 0, "gameMapAreaConditionList": []} - - async def handle_get_user_c_mission_api_request(self, data: Dict) -> Dict: - user_id = data["userId"] - mission_id = data["missionId"] - - progress_list = [] - point = 0 - - mission_data = await self.data.item.get_cmission(user_id, mission_id) - progress_data = await self.data.item.get_cmission_progress(user_id, mission_id) - - if mission_data and progress_data: - point = mission_data["point"] - - for progress in progress_data: - progress_list.append( - { - "order": progress["order"], - "stage": progress["stage"], - "progress": progress["progress"], - } - ) - - return { - "userId": user_id, - "missionId": mission_id, - "point": point, - "userCMissionProgressList": progress_list, - } - - async def handle_get_user_net_battle_ranking_info_api_request( - self, data: Dict - ) -> Dict: - user_id = data["userId"] - - net_battle = {} - net_battle_data = await self.data.profile.get_net_battle(user_id) - - if net_battle_data: - net_battle = { - "isRankUpChallengeFailed": net_battle_data["isRankUpChallengeFailed"], - "highestBattleRankId": net_battle_data["highestBattleRankId"], - "battleIconId": net_battle_data["battleIconId"], - "battleIconNum": net_battle_data["battleIconNum"], - "avatarEffectPoint": net_battle_data["avatarEffectPoint"], - } - - return { - "userId": user_id, - "userNetBattleData": net_battle, - } diff --git a/titles/chuni/new.py b/titles/chuni/new.py index a16f9db..8a658bf 100644 --- a/titles/chuni/new.py +++ b/titles/chuni/new.py @@ -5,11 +5,10 @@ from typing import Dict import pytz from core.config import CoreConfig -from core.utils import Utils -from titles.chuni.base import ChuniBase -from titles.chuni.config import ChuniConfig from titles.chuni.const import ChuniConstants from titles.chuni.database import ChuniData +from titles.chuni.base import ChuniBase +from titles.chuni.config import ChuniConfig class ChuniNew(ChuniBase): @@ -24,19 +23,7 @@ class ChuniNew(ChuniBase): self.game = ChuniConstants.GAME_CODE self.version = ChuniConstants.VER_CHUNITHM_NEW - def _interal_ver_to_intver(self) -> str: - if self.version == ChuniConstants.VER_CHUNITHM_NEW: - return "200" - if self.version == ChuniConstants.VER_CHUNITHM_NEW_PLUS: - return "205" - if self.version == ChuniConstants.VER_CHUNITHM_SUN: - return "210" - if self.version == ChuniConstants.VER_CHUNITHM_SUN_PLUS: - return "215" - if self.version == ChuniConstants.VER_CHUNITHM_LUMINOUS: - return "220" - - async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: # use UTC time and convert it to JST time by adding +9 # matching therefore starts one hour before and lasts for 8 hours match_start = datetime.strftime( @@ -46,10 +33,7 @@ class ChuniNew(ChuniBase): datetime.utcnow() + timedelta(hours=16), 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 == "" - ): + 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 ) @@ -58,44 +42,19 @@ class ChuniNew(ChuniBase): ) else: # get current datetime in JST - current_jst = datetime.now(pytz.timezone("Asia/Tokyo")).date() + 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" - ) + 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"), - ) + 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) - - t_port = ( - f":{self.core_cfg.server.port}" - if ( - not self.core_cfg.server.is_using_proxy - and Utils.get_title_port(self.core_cfg) != 80 - ) - else "" - ) - version = self.game_cfg.version.version(self.version) - return { "gameSetting": { "isMaintenance": False, @@ -108,31 +67,31 @@ class ChuniNew(ChuniBase): "maxCountMusic": 300, "matchStartTime": match_start, "matchEndTime": match_end, - "matchTimeLimit": self.game_cfg.matching.match_time_limit, - "matchErrorLimit": self.game_cfg.matching.match_error_limit, - "romVersion": version["rom"], - "dataVersion": version["data"], - "matchingUri": f"http://{self.core_cfg.server.hostname}{t_port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/", - "matchingUriX": f"http://{self.core_cfg.server.hostname}{t_port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/", + "matchTimeLimit": 60, + "matchErrorLimit": 9999, + "romVersion": self.game_cfg.version.version(self.version)["rom"], + "dataVersion": self.game_cfg.version.version(self.version)["data"], + "matchingUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", + "matchingUriX": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", # might be really important for online battle to connect the cabs via UDP port 50201 - "udpHolePunchUri": f"http://{self.core_cfg.server.hostname}{t_port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/", - "reflectorUri": f"http://{self.core_cfg.server.hostname}{t_port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/", + "udpHolePunchUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", + "reflectorUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", }, "isDumpUpload": False, "isAou": False, } - async def handle_remove_token_api_request(self, data: Dict) -> Dict: + def handle_remove_token_api_request(self, data: Dict) -> Dict: return {"returnCode": "1"} - async def handle_delete_token_api_request(self, data: Dict) -> Dict: + def handle_delete_token_api_request(self, data: Dict) -> Dict: return {"returnCode": "1"} - async def handle_create_token_api_request(self, data: Dict) -> Dict: + def handle_create_token_api_request(self, data: Dict) -> Dict: return {"returnCode": "1"} - async def handle_get_user_map_area_api_request(self, data: Dict) -> Dict: - user_map_areas = await self.data.item.get_map_areas(data["userId"]) + def handle_get_user_map_area_api_request(self, data: Dict) -> Dict: + user_map_areas = self.data.item.get_map_areas(data["userId"]) map_areas = [] for map_area in user_map_areas: @@ -143,16 +102,14 @@ class ChuniNew(ChuniBase): return {"userId": data["userId"], "userMapAreaList": map_areas} - async def handle_get_user_symbol_chat_setting_api_request(self, data: Dict) -> Dict: + def handle_get_user_symbol_chat_setting_api_request(self, data: Dict) -> Dict: return {"userId": data["userId"], "symbolCharInfoList": []} - async def handle_get_user_preview_api_request(self, data: Dict) -> Dict: - profile = await self.data.profile.get_profile_preview( - data["userId"], self.version - ) + 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: return None - profile_character = await self.data.item.get_character( + profile_character = self.data.item.get_character( data["userId"], profile["characterId"] ) @@ -196,8 +153,8 @@ class ChuniNew(ChuniBase): } return data1 - async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: - p = await self.data.profile.get_profile_data(data["userId"], self.version) + def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + p = self.data.profile.get_profile_data(data["userId"], self.version) if p is None: return {} @@ -209,17 +166,17 @@ class ChuniNew(ChuniBase): "isLogin": False, } - async def handle_printer_login_api_request(self, data: Dict) -> Dict: + def handle_printer_login_api_request(self, data: Dict) -> Dict: return {"returnCode": 1} - async def handle_printer_logout_api_request(self, data: Dict) -> Dict: + def handle_printer_logout_api_request(self, data: Dict) -> Dict: return {"returnCode": 1} - async def handle_get_game_gacha_api_request(self, data: Dict) -> Dict: + def handle_get_game_gacha_api_request(self, data: Dict) -> Dict: """ returns all current active banners (gachas) """ - game_gachas = await self.data.static.get_gachas(self.version) + game_gachas = self.data.static.get_gachas(self.version) # clean the database rows game_gacha_list = [] @@ -245,11 +202,11 @@ class ChuniNew(ChuniBase): "registIdList": [], } - async def handle_get_game_gacha_card_by_id_api_request(self, data: Dict) -> Dict: + def handle_get_game_gacha_card_by_id_api_request(self, data: Dict) -> Dict: """ returns all valid cards for a given gachaId """ - game_gacha_cards = await self.data.static.get_gacha_cards(data["gachaId"]) + game_gacha_cards = self.data.static.get_gacha_cards(data["gachaId"]) game_gacha_card_list = [] for gacha_card in game_gacha_cards: @@ -269,8 +226,8 @@ class ChuniNew(ChuniBase): "ssrBookCalcList": [], } - async def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: - p = await self.data.profile.get_profile_data(data["userId"], self.version) + def handle_cm_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 {} @@ -294,8 +251,8 @@ class ChuniNew(ChuniBase): ], } - async def handle_get_user_gacha_api_request(self, data: Dict) -> Dict: - user_gachas = await self.data.item.get_user_gachas(data["userId"]) + 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": []} @@ -313,8 +270,8 @@ class ChuniNew(ChuniBase): "userGachaList": user_gacha_list, } - async def handle_get_user_printed_card_api_request(self, data: Dict) -> Dict: - user_print_list = await self.data.item.get_user_print_states( + def handle_get_user_printed_card_api_request(self, data: Dict) -> Dict: + user_print_list = self.data.item.get_user_print_states( data["userId"], has_completed=True ) if user_print_list is None: @@ -348,10 +305,10 @@ class ChuniNew(ChuniBase): "userPrintedCardList": print_list, } - async def handle_get_user_card_print_error_api_request(self, data: Dict) -> Dict: + def handle_get_user_card_print_error_api_request(self, data: Dict) -> Dict: user_id = data["userId"] - user_print_states = await self.data.item.get_user_print_states( + user_print_states = self.data.item.get_user_print_states( user_id, has_completed=False ) @@ -370,13 +327,13 @@ class ChuniNew(ChuniBase): "userCardPrintStateList": card_print_state_list, } - async def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: - return await super().handle_get_user_character_api_request(data) + def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: + return super().handle_get_user_character_api_request(data) - async def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: - return await super().handle_get_user_item_api_request(data) + def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: + return super().handle_get_user_item_api_request(data) - async def handle_roll_gacha_api_request(self, data: Dict) -> Dict: + def handle_roll_gacha_api_request(self, data: Dict) -> Dict: """ Handle a gacha roll API request, with: gachaId: the gachaId where the cards should be pulled from @@ -394,16 +351,14 @@ class ChuniNew(ChuniBase): # characterId should be returned if chara_id != -1: # get the - card = await self.data.static.get_gacha_card_by_character( - gacha_id, chara_id - ) + card = self.data.static.get_gacha_card_by_character(gacha_id, chara_id) tmp = card._asdict() tmp.pop("id") rolled_cards.append(tmp) else: - gacha_cards = await self.data.static.get_gacha_cards(gacha_id) + gacha_cards = self.data.static.get_gacha_cards(gacha_id) # get the card id for each roll for _ in range(num_rolls): @@ -420,7 +375,7 @@ class ChuniNew(ChuniBase): return {"length": len(rolled_cards), "gameGachaCardList": rolled_cards} - async def handle_cm_upsert_user_gacha_api_request(self, data: Dict) -> Dict: + def handle_cm_upsert_user_gacha_api_request(self, data: Dict) -> Dict: upsert = data["cmUpsertUserGacha"] user_id = data["userId"] place_id = data["placeId"] @@ -430,7 +385,7 @@ class ChuniNew(ChuniBase): user_data.pop("rankUpChallengeResults") user_data.pop("userEmoney") - await self.data.profile.put_profile_data(user_id, self.version, user_data) + self.data.profile.put_profile_data(user_id, self.version, user_data) # save the user gacha user_gacha = upsert["userGacha"] @@ -438,16 +393,16 @@ class ChuniNew(ChuniBase): user_gacha.pop("gachaId") user_gacha.pop("dailyGachaDate") - await self.data.item.put_user_gacha(user_id, gacha_id, user_gacha) + self.data.item.put_user_gacha(user_id, gacha_id, user_gacha) # save all user items if "userItemList" in upsert: for item in upsert["userItemList"]: - await self.data.item.put_item(user_id, item) + self.data.item.put_item(user_id, item) # add every gamegachaCard to database for card in upsert["gameGachaCardList"]: - await self.data.item.put_user_print_state( + self.data.item.put_user_print_state( user_id, hasCompleted=False, placeId=place_id, @@ -457,7 +412,7 @@ class ChuniNew(ChuniBase): # retrieve every game gacha card which has been added in order to get # the orderId for the next request - user_print_states = await self.data.item.get_user_print_states_by_gacha( + user_print_states = self.data.item.get_user_print_states_by_gacha( user_id, gacha_id, has_completed=False ) card_print_state_list = [] @@ -475,7 +430,7 @@ class ChuniNew(ChuniBase): "userCardPrintStateList": card_print_state_list, } - async def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: + def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: return { "returnCode": 1, "orderId": 0, @@ -483,7 +438,7 @@ class ChuniNew(ChuniBase): "apiName": "CMUpsertUserPrintlogApi", } - async def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: + def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: user_print_detail = data["userPrintDetail"] user_id = data["userId"] @@ -499,9 +454,7 @@ class ChuniNew(ChuniBase): ) # add the entry to the user print table with the random serialId - await self.data.item.put_user_print_detail( - user_id, serial_id, user_print_detail - ) + self.data.item.put_user_print_detail(user_id, serial_id, user_print_detail) return { "returnCode": 1, @@ -510,9 +463,7 @@ class ChuniNew(ChuniBase): "apiName": "CMUpsertUserPrintApi", } - async def handle_cm_upsert_user_print_subtract_api_request( - self, data: Dict - ) -> Dict: + def handle_cm_upsert_user_print_subtract_api_request(self, data: Dict) -> Dict: upsert = data["userCardPrintState"] user_id = data["userId"] place_id = data["placeId"] @@ -520,39 +471,37 @@ class ChuniNew(ChuniBase): # save all user items if "userItemList" in data: for item in data["userItemList"]: - await self.data.item.put_item(user_id, item) + self.data.item.put_item(user_id, item) # set the card print state to success and use the orderId as the key - await self.data.item.put_user_print_state( + self.data.item.put_user_print_state( user_id, id=upsert["orderId"], hasCompleted=True ) return {"returnCode": "1", "apiName": "CMUpsertUserPrintSubtractApi"} - async def handle_cm_upsert_user_print_cancel_api_request(self, data: Dict) -> Dict: + def handle_cm_upsert_user_print_cancel_api_request(self, data: Dict) -> Dict: order_ids = data["orderIdList"] user_id = data["userId"] # set the card print state to success and use the orderId as the key for order_id in order_ids: - await self.data.item.put_user_print_state( - user_id, id=order_id, hasCompleted=True - ) + self.data.item.put_user_print_state(user_id, id=order_id, hasCompleted=True) return {"returnCode": "1", "apiName": "CMUpsertUserPrintCancelApi"} - async def handle_ping_request(self, data: Dict) -> Dict: + def handle_ping_request(self, data: Dict) -> Dict: # matchmaking ping request return {"returnCode": "1"} - async def handle_begin_matching_api_request(self, data: Dict) -> Dict: + def handle_begin_matching_api_request(self, data: Dict) -> Dict: room_id = 1 # check if there is a free matching room - matching_room = await self.data.item.get_oldest_free_matching(self.version) + matching_room = self.data.item.get_oldest_free_matching(self.version) if matching_room is None: # grab the latest roomId and add 1 for the new room - newest_matching = await self.data.item.get_newest_matching(self.version) + newest_matching = self.data.item.get_newest_matching(self.version) if newest_matching is not None: room_id = newest_matching["roomId"] + 1 @@ -562,12 +511,12 @@ class ChuniNew(ChuniBase): # create the new room with room_id and the current user id (host) # user id is required for the countdown later on - await self.data.item.put_matching( + self.data.item.put_matching( self.version, room_id, [new_member], user_id=new_member["userId"] ) # get the newly created matching room - matching_room = await self.data.item.get_matching(self.version, room_id) + matching_room = self.data.item.get_matching(self.version, room_id) else: # a room already exists, so just add the new member to it matching_member_list = matching_room["matchingMemberInfoList"] @@ -577,7 +526,7 @@ class ChuniNew(ChuniBase): matching_member_list.append(new_member) # add the updated room to the database, make sure to set isFull correctly! - await self.data.item.put_matching( + self.data.item.put_matching( self.version, matching_room["roomId"], matching_member_list, @@ -594,8 +543,8 @@ class ChuniNew(ChuniBase): return {"roomId": 1, "matchingWaitState": matching_wait} - async def handle_end_matching_api_request(self, data: Dict) -> Dict: - matching_room = await self.data.item.get_matching(self.version, data["roomId"]) + def handle_end_matching_api_request(self, data: Dict) -> Dict: + matching_room = self.data.item.get_matching(self.version, data["roomId"]) members = matching_room["matchingMemberInfoList"] # only set the host user to role 1 every other to 0? @@ -604,7 +553,7 @@ class ChuniNew(ChuniBase): for m in members ] - await self.data.item.put_matching( + self.data.item.put_matching( self.version, matching_room["roomId"], members, @@ -619,13 +568,13 @@ class ChuniNew(ChuniBase): # no idea, maybe to differentiate between CPUs and real players? "matchingMemberRoleList": role_list, # TCP/UDP connection? - "reflectorUri": f"{self.core_cfg.server.hostname}", + "reflectorUri": f"{self.core_cfg.title.hostname}", } - async def handle_remove_matching_member_api_request(self, data: Dict) -> Dict: + def handle_remove_matching_member_api_request(self, data: Dict) -> Dict: # get all matching rooms, because Chuni only returns the userId # not the actual roomId - matching_rooms = await self.data.item.get_all_matchings(self.version) + matching_rooms = self.data.item.get_all_matchings(self.version) if matching_rooms is None: return {"returnCode": "1"} @@ -639,10 +588,10 @@ class ChuniNew(ChuniBase): # if the last user got removed, delete the matching room if len(new_members) <= 0: - await self.data.item.delete_matching(self.version, room["roomId"]) + self.data.item.delete_matching(self.version, room["roomId"]) else: # remove the user from the room - await self.data.item.put_matching( + self.data.item.put_matching( self.version, room["roomId"], new_members, @@ -652,10 +601,10 @@ class ChuniNew(ChuniBase): return {"returnCode": "1"} - async def handle_get_matching_state_api_request(self, data: Dict) -> Dict: + def handle_get_matching_state_api_request(self, data: Dict) -> Dict: polling_interval = 1 # get the current active room - matching_room = await self.data.item.get_matching(self.version, data["roomId"]) + matching_room = self.data.item.get_matching(self.version, data["roomId"]) members = matching_room["matchingMemberInfoList"] rest_sec = matching_room["restMSec"] @@ -678,7 +627,7 @@ class ChuniNew(ChuniBase): current_member["userName"] = self.read_wtf8(current_member["userName"]) members[i] = current_member - await self.data.item.put_matching( + self.data.item.put_matching( self.version, data["roomId"], members, @@ -698,4 +647,4 @@ class ChuniNew(ChuniBase): "matchingMemberInfoList": [current_member] + diff_members, } - return {"roomId": data["roomId"], "matchingWaitState": matching_wait} + return {"matchingWaitState": matching_wait} diff --git a/titles/chuni/newplus.py b/titles/chuni/newplus.py index 1364caa..749173c 100644 --- a/titles/chuni/newplus.py +++ b/titles/chuni/newplus.py @@ -1,9 +1,9 @@ -from typing import Dict +from typing import Dict, Any from core.config import CoreConfig -from titles.chuni.config import ChuniConfig -from titles.chuni.const import ChuniConstants from titles.chuni.new import ChuniNew +from titles.chuni.const import ChuniConstants +from titles.chuni.config import ChuniConfig class ChuniNewPlus(ChuniNew): @@ -11,9 +11,31 @@ class ChuniNewPlus(ChuniNew): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_NEW_PLUS - async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: - user_data = await super().handle_cm_get_user_preview_api_request(data) + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = super().handle_get_game_setting_api_request(data) + ret["gameSetting"]["romVersion"] = self.game_cfg.version.version(self.version)[ + "rom" + ] + ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version(self.version)[ + "data" + ] + ret["gameSetting"][ + "matchingUri" + ] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/" + ret["gameSetting"][ + "matchingUriX" + ] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/" + ret["gameSetting"][ + "udpHolePunchUri" + ] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/" + ret["gameSetting"][ + "reflectorUri" + ] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/" + return ret + + def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = super().handle_cm_get_user_preview_api_request(data) # hardcode lastDataVersion for CardMaker 1.35 A028 user_data["lastDataVersion"] = "2.05.00" - return user_data + return user_data \ No newline at end of file diff --git a/titles/chuni/paradise.py b/titles/chuni/paradise.py index 0985303..19155d6 100644 --- a/titles/chuni/paradise.py +++ b/titles/chuni/paradise.py @@ -1,9 +1,11 @@ -from typing import Dict +from datetime import datetime, timedelta +from typing import Dict, Any +import pytz from core.config import CoreConfig from titles.chuni.base import ChuniBase -from titles.chuni.config import ChuniConfig from titles.chuni.const import ChuniConstants +from titles.chuni.config import ChuniConfig class ChuniParadise(ChuniBase): @@ -11,7 +13,7 @@ class ChuniParadise(ChuniBase): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_PARADISE - async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = await super().handle_get_game_setting_api_request(data) + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.50.00" return ret diff --git a/titles/chuni/plus.py b/titles/chuni/plus.py index c995636..62d9e0d 100644 --- a/titles/chuni/plus.py +++ b/titles/chuni/plus.py @@ -2,8 +2,8 @@ from typing import Dict from core.config import CoreConfig from titles.chuni.base import ChuniBase -from titles.chuni.config import ChuniConfig from titles.chuni.const import ChuniConstants +from titles.chuni.config import ChuniConfig class ChuniPlus(ChuniBase): @@ -11,7 +11,7 @@ class ChuniPlus(ChuniBase): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_PLUS - async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = await super().handle_get_game_setting_api_request(data) + 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" return ret diff --git a/titles/chuni/read.py b/titles/chuni/read.py index c8acbe4..7100ae6 100644 --- a/titles/chuni/read.py +++ b/titles/chuni/read.py @@ -1,11 +1,11 @@ -import xml.etree.ElementTree as ET -from os import path, walk from typing import Optional +from os import walk, path +import xml.etree.ElementTree as ET +from read import BaseReader from core.config import CoreConfig -from read import BaseReader -from titles.chuni.const import ChuniConstants from titles.chuni.database import ChuniData +from titles.chuni.const import ChuniConstants class ChuniReader(BaseReader): @@ -28,7 +28,7 @@ class ChuniReader(BaseReader): self.logger.error(f"Invalid chunithm version {version}") exit(1) - async def read(self) -> None: + def read(self) -> None: data_dirs = [] if self.bin_dir is not None: data_dirs += self.get_data_directories(self.bin_dir) @@ -38,13 +38,13 @@ class ChuniReader(BaseReader): for dir in data_dirs: self.logger.info(f"Read from {dir}") - await self.read_events(f"{dir}/event") - await self.read_music(f"{dir}/music") - await self.read_charges(f"{dir}/chargeItem") - await self.read_avatar(f"{dir}/avatarAccessory") - await self.read_login_bonus(f"{dir}/") + self.read_events(f"{dir}/event") + self.read_music(f"{dir}/music") + self.read_charges(f"{dir}/chargeItem") + self.read_avatar(f"{dir}/avatarAccessory") + self.read_login_bonus(f"{dir}/") - async def read_login_bonus(self, root_dir: str) -> None: + def read_login_bonus(self, root_dir: str) -> None: for root, dirs, files in walk(f"{root_dir}loginBonusPreset"): for dir in dirs: if path.exists(f"{root}/{dir}/LoginBonusPreset.xml"): @@ -60,7 +60,7 @@ class ChuniReader(BaseReader): True if xml_root.find("disableFlag").text == "false" else False ) - result = await self.data.static.put_login_bonus_preset( + result = self.data.static.put_login_bonus_preset( self.version, id, name, is_enabled ) @@ -98,7 +98,7 @@ class ChuniReader(BaseReader): bonus_root.find("loginBonusCategoryType").text ) - result = await self.data.static.put_login_bonus( + result = self.data.static.put_login_bonus( self.version, id, bonus_id, @@ -117,7 +117,7 @@ class ChuniReader(BaseReader): f"Failed to insert login bonus {bonus_id}" ) - async def read_events(self, evt_dir: str) -> None: + def read_events(self, evt_dir: str) -> None: for root, dirs, files in walk(evt_dir): for dir in dirs: if path.exists(f"{root}/{dir}/Event.xml"): @@ -132,7 +132,7 @@ class ChuniReader(BaseReader): for substances in xml_root.findall("substances"): event_type = substances.find("type").text - result = await self.data.static.put_event( + result = self.data.static.put_event( self.version, id, event_type, name ) if result is not None: @@ -140,7 +140,7 @@ class ChuniReader(BaseReader): else: self.logger.warning(f"Failed to insert event {id}") - async def read_music(self, music_dir: str) -> None: + def read_music(self, music_dir: str) -> None: for root, dirs, files in walk(music_dir): for dir in dirs: if path.exists(f"{root}/{dir}/Music.xml"): @@ -172,9 +172,7 @@ class ChuniReader(BaseReader): chart_type = MusicFumenData.find("type") chart_id = chart_type.find("id").text chart_diff = chart_type.find("str").text - if chart_diff == "WorldsEnd" and ( - chart_id == "4" or chart_id == "5" - ): # 4 in SDBT, 5 in SDHD + if chart_diff == "WorldsEnd" and (chart_id == "4" or chart_id == "5"): # 4 in SDBT, 5 in SDHD level = float(xml_root.find("starDifType").text) we_chara = ( xml_root.find("worldsEndTagName") @@ -187,7 +185,7 @@ class ChuniReader(BaseReader): ) we_chara = None - result = await self.data.static.put_music( + result = self.data.static.put_music( self.version, song_id, chart_id, @@ -208,7 +206,7 @@ class ChuniReader(BaseReader): f"Failed to insert music {song_id} chart {chart_id}" ) - async def read_charges(self, charge_dir: str) -> None: + def read_charges(self, charge_dir: str) -> None: for root, dirs, files in walk(charge_dir): for dir in dirs: if path.exists(f"{root}/{dir}/ChargeItem.xml"): @@ -224,7 +222,7 @@ class ChuniReader(BaseReader): consumeType = xml_root.find("consumeType").text sellingAppeal = bool(xml_root.find("sellingAppeal").text) - result = await self.data.static.put_charge( + result = self.data.static.put_charge( self.version, id, name, @@ -238,7 +236,7 @@ class ChuniReader(BaseReader): else: self.logger.warning(f"Failed to insert charge {id}") - async def read_avatar(self, avatar_dir: str) -> None: + def read_avatar(self, avatar_dir: str) -> None: for root, dirs, files in walk(avatar_dir): for dir in dirs: if path.exists(f"{root}/{dir}/AvatarAccessory.xml"): @@ -256,7 +254,7 @@ class ChuniReader(BaseReader): for texture in xml_root.findall("texture"): texturePath = texture.find("path").text - result = await self.data.static.put_avatar( + result = self.data.static.put_avatar( self.version, id, name, category, iconPath, texturePath ) diff --git a/titles/chuni/schema/__init__.py b/titles/chuni/schema/__init__.py index 5a4a6f4..51d950b 100644 --- a/titles/chuni/schema/__init__.py +++ b/titles/chuni/schema/__init__.py @@ -1,6 +1,6 @@ -from titles.chuni.schema.item import ChuniItemData from titles.chuni.schema.profile import ChuniProfileData from titles.chuni.schema.score import ChuniScoreData +from titles.chuni.schema.item import ChuniItemData from titles.chuni.schema.static import ChuniStaticData __all__ = ["ChuniProfileData", "ChuniScoreData", "ChuniItemData", "ChuniStaticData"] diff --git a/titles/chuni/schema/item.py b/titles/chuni/schema/item.py index 0a41b3f..dc2751d 100644 --- a/titles/chuni/schema/item.py +++ b/titles/chuni/schema/item.py @@ -1,19 +1,20 @@ from typing import Dict, List, Optional - -from core.data.schema import BaseData, metadata from sqlalchemy import ( - Column, - PrimaryKeyConstraint, Table, + Column, UniqueConstraint, + PrimaryKeyConstraint, and_, delete, ) -from sqlalchemy.dialects.mysql import insert -from sqlalchemy.engine import Row +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON +from sqlalchemy.engine.base import Connection from sqlalchemy.schema import ForeignKey from sqlalchemy.sql import func, select -from sqlalchemy.types import JSON, TIMESTAMP, Boolean, Integer, String +from sqlalchemy.dialects.mysql import insert +from sqlalchemy.engine import Row + +from core.data.schema import BaseData, metadata character = Table( "chuni_item_character", @@ -242,84 +243,63 @@ matching = Table( mysql_charset="utf8mb4", ) -cmission = Table( - "chuni_item_cmission", - metadata, - Column("id", Integer, primary_key=True, nullable=False), - Column( - "user", - ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), - nullable=False, - ), - Column("missionId", Integer, nullable=False), - Column("point", Integer), - UniqueConstraint("user", "missionId", name="chuni_item_cmission_uk"), - mysql_charset="utf8mb4", -) - -cmission_progress = Table( - "chuni_item_cmission_progress", - metadata, - Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade"), nullable=False), - Column("missionId", Integer, nullable=False), - Column("order", Integer), - Column("stage", Integer), - Column("progress", Integer), - UniqueConstraint( - "user", "missionId", "order", name="chuni_item_cmission_progress_uk" - ), - mysql_charset="utf8mb4", -) - class ChuniItemData(BaseData): - async def get_oldest_free_matching(self, version: int) -> Optional[Row]: + def get_oldest_free_matching(self, version: int) -> Optional[Row]: sql = matching.select( - and_(matching.c.version == version, matching.c.isFull == False) + and_( + matching.c.version == version, + matching.c.isFull == False + ) ).order_by(matching.c.roomId.asc()) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_newest_matching(self, version: int) -> Optional[Row]: - sql = matching.select(and_(matching.c.version == version)).order_by( - matching.c.roomId.desc() + def get_newest_matching(self, version: int) -> Optional[Row]: + sql = matching.select( + and_( + matching.c.version == version + ) + ).order_by(matching.c.roomId.desc()) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_all_matchings(self, version: int) -> Optional[List[Row]]: + sql = matching.select( + and_( + matching.c.version == version + ) ) - result = await self.execute(sql) - if result is None: - return None - return result.fetchone() - - async def get_all_matchings(self, version: int) -> Optional[List[Row]]: - sql = matching.select(and_(matching.c.version == version)) - - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_matching(self, version: int, room_id: int) -> Optional[Row]: + def get_matching(self, version: int, room_id: int) -> Optional[Row]: sql = matching.select( and_(matching.c.version == version, matching.c.roomId == room_id) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def put_matching( + def put_matching( self, version: int, room_id: int, matching_member_info_list: List, user_id: int = None, rest_sec: int = 60, - is_full: bool = False, + is_full: bool = False ) -> Optional[int]: sql = insert(matching).values( roomId=room_id, @@ -334,22 +314,22 @@ class ChuniItemData(BaseData): restMSec=rest_sec, matchingMemberInfoList=matching_member_info_list ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: return None return result.lastrowid - async def delete_matching(self, version: int, room_id: int): + def delete_matching(self, version: int, room_id: int): sql = delete(matching).where( and_(matching.c.roomId == room_id, matching.c.version == version) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.lastrowid - async def get_all_favorites( + def get_all_favorites( self, user_id: int, version: int, fav_kind: int = 1 ) -> Optional[List[Row]]: sql = favorite.select( @@ -360,12 +340,12 @@ class ChuniItemData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_login_bonus( + def put_login_bonus( self, user_id: int, version: int, preset_id: int, **login_bonus_data ) -> Optional[int]: sql = insert(login_bonus).values( @@ -374,12 +354,12 @@ class ChuniItemData(BaseData): conflict = sql.on_duplicate_key_update(presetId=preset_id, **login_bonus_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: return None return result.lastrowid - async def get_all_login_bonus( + def get_all_login_bonus( self, user_id: int, version: int, is_finished: bool = False ) -> Optional[List[Row]]: sql = login_bonus.select( @@ -390,12 +370,12 @@ class ChuniItemData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_login_bonus( + def get_login_bonus( self, user_id: int, version: int, preset_id: int ) -> Optional[Row]: sql = login_bonus.select( @@ -406,12 +386,12 @@ class ChuniItemData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def put_character(self, user_id: int, character_data: Dict) -> Optional[int]: + def put_character(self, user_id: int, character_data: Dict) -> Optional[int]: character_data["user"] = user_id character_data = self.fix_bools(character_data) @@ -419,30 +399,30 @@ class ChuniItemData(BaseData): sql = insert(character).values(**character_data) conflict = sql.on_duplicate_key_update(**character_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: return None return result.lastrowid - async def get_character(self, user_id: int, character_id: int) -> Optional[Dict]: + def get_character(self, user_id: int, character_id: int) -> Optional[Dict]: sql = select(character).where( and_(character.c.user == user_id, character.c.characterId == character_id) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_characters(self, user_id: int) -> Optional[List[Row]]: + def get_characters(self, user_id: int) -> Optional[List[Row]]: sql = select(character).where(character.c.user == user_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_item(self, user_id: int, item_data: Dict) -> Optional[int]: + def put_item(self, user_id: int, item_data: Dict) -> Optional[int]: item_data["user"] = user_id item_data = self.fix_bools(item_data) @@ -450,12 +430,12 @@ class ChuniItemData(BaseData): sql = insert(item).values(**item_data) conflict = sql.on_duplicate_key_update(**item_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: return None return result.lastrowid - async def get_items(self, user_id: int, kind: int = None) -> Optional[List[Row]]: + def get_items(self, user_id: int, kind: int = None) -> Optional[List[Row]]: if kind is None: sql = select(item).where(item.c.user == user_id) else: @@ -463,12 +443,12 @@ class ChuniItemData(BaseData): and_(item.c.user == user_id, item.c.itemKind == kind) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_duel(self, user_id: int, duel_data: Dict) -> Optional[int]: + def put_duel(self, user_id: int, duel_data: Dict) -> Optional[int]: duel_data["user"] = user_id duel_data = self.fix_bools(duel_data) @@ -476,20 +456,20 @@ class ChuniItemData(BaseData): sql = insert(duel).values(**duel_data) conflict = sql.on_duplicate_key_update(**duel_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: return None return result.lastrowid - async def get_duels(self, user_id: int) -> Optional[List[Row]]: + def get_duels(self, user_id: int) -> Optional[List[Row]]: sql = select(duel).where(duel.c.user == user_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_map(self, user_id: int, map_data: Dict) -> Optional[int]: + def put_map(self, user_id: int, map_data: Dict) -> Optional[int]: map_data["user"] = user_id map_data = self.fix_bools(map_data) @@ -497,20 +477,20 @@ class ChuniItemData(BaseData): sql = insert(map).values(**map_data) conflict = sql.on_duplicate_key_update(**map_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: return None return result.lastrowid - async def get_maps(self, user_id: int) -> Optional[List[Row]]: + def get_maps(self, user_id: int) -> Optional[List[Row]]: sql = select(map).where(map.c.user == user_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_map_area(self, user_id: int, map_area_data: Dict) -> Optional[int]: + def put_map_area(self, user_id: int, map_area_data: Dict) -> Optional[int]: map_area_data["user"] = user_id map_area_data = self.fix_bools(map_area_data) @@ -518,28 +498,28 @@ class ChuniItemData(BaseData): sql = insert(map_area).values(**map_area_data) conflict = sql.on_duplicate_key_update(**map_area_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: return None return result.lastrowid - async def get_map_areas(self, user_id: int) -> Optional[List[Row]]: + def get_map_areas(self, user_id: int) -> Optional[List[Row]]: sql = select(map_area).where(map_area.c.user == user_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_user_gachas(self, aime_id: int) -> Optional[List[Row]]: + def get_user_gachas(self, aime_id: int) -> Optional[List[Row]]: sql = gacha.select(gacha.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_user_gacha( + def put_user_gacha( self, aime_id: int, gacha_id: int, gacha_data: Dict ) -> Optional[int]: sql = insert(gacha).values(user=aime_id, gachaId=gacha_id, **gacha_data) @@ -547,14 +527,14 @@ class ChuniItemData(BaseData): conflict = sql.on_duplicate_key_update( user=aime_id, gachaId=gacha_id, **gacha_data ) - result = await self.execute(conflict) + 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 - async def get_user_print_states( + def get_user_print_states( self, aime_id: int, has_completed: bool = False ) -> Optional[List[Row]]: sql = print_state.select( @@ -564,12 +544,12 @@ class ChuniItemData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_user_print_states_by_gacha( + def get_user_print_states_by_gacha( self, aime_id: int, gacha_id: int, has_completed: bool = False ) -> Optional[List[Row]]: sql = print_state.select( @@ -580,16 +560,16 @@ class ChuniItemData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_user_print_state(self, aime_id: int, **print_data) -> Optional[int]: + def put_user_print_state(self, aime_id: int, **print_data) -> Optional[int]: sql = insert(print_state).values(user=aime_id, **print_data) conflict = sql.on_duplicate_key_update(user=aime_id, **print_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning( @@ -598,7 +578,7 @@ class ChuniItemData(BaseData): return None return result.lastrowid - async def put_user_print_detail( + def put_user_print_detail( self, aime_id: int, serial_id: str, user_print_data: Dict ) -> Optional[int]: sql = insert(print_detail).values( @@ -606,7 +586,7 @@ class ChuniItemData(BaseData): ) conflict = sql.on_duplicate_key_update(user=aime_id, **user_print_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning( @@ -614,61 +594,3 @@ class ChuniItemData(BaseData): ) return None return result.lastrowid - - async def put_cmission_progress( - self, user_id: int, mission_id: int, progress_data: Dict - ) -> Optional[int]: - progress_data["user"] = user_id - progress_data["missionId"] = mission_id - - sql = insert(cmission_progress).values(**progress_data) - conflict = sql.on_duplicate_key_update(**progress_data) - - result = await self.execute(conflict) - if result is None: - return None - return result.lastrowid - - async def get_cmission_progress( - self, user_id: int, mission_id: int - ) -> Optional[List[Row]]: - sql = cmission_progress.select( - and_( - cmission_progress.c.user == user_id, - cmission_progress.c.missionId == mission_id, - ) - ).order_by(cmission_progress.c.order.asc()) - - result = await self.execute(sql) - if result is None: - return None - return result.fetchall() - - async def put_cmission(self, user_id: int, mission_data: Dict) -> Optional[int]: - mission_data["user"] = user_id - - sql = insert(cmission).values(**mission_data) - conflict = sql.on_duplicate_key_update(**mission_data) - - result = await self.execute(conflict) - if result is None: - return None - return result.lastrowid - - async def get_cmissions(self, user_id: int) -> Optional[List[Row]]: - sql = cmission.select(cmission.c.user == user_id) - - result = await self.execute(sql) - if result is None: - return None - return result.fetchall() - - async def get_cmission(self, user_id: int, mission_id: int) -> Optional[Row]: - sql = cmission.select( - and_(cmission.c.user == user_id, cmission.c.missionId == mission_id) - ) - - result = await self.execute(sql) - if result is None: - return None - return result.fetchone() diff --git a/titles/chuni/schema/profile.py b/titles/chuni/schema/profile.py index 71691c2..ea70583 100644 --- a/titles/chuni/schema/profile.py +++ b/titles/chuni/schema/profile.py @@ -1,21 +1,13 @@ from typing import Dict, List, Optional -<<<<<<< Updated upstream -from sqlalchemy import Table, Column, UniqueConstraint, and_ -from sqlalchemy.types import Integer, String, Boolean, JSON, BigInteger +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.engine import Row -from sqlalchemy.sql import select, delete +from sqlalchemy.sql import func, select from sqlalchemy.dialects.mysql import insert -======= ->>>>>>> Stashed changes from core.data.schema import BaseData, metadata -from sqlalchemy import Column, Table, UniqueConstraint, and_ -from sqlalchemy.dialects.mysql import insert -from sqlalchemy.engine import Row -from sqlalchemy.schema import ForeignKey -from sqlalchemy.sql import select -from sqlalchemy.types import JSON, BigInteger, Boolean, Integer, String profile = Table( "chuni_profile_data", @@ -401,51 +393,9 @@ team = Table( mysql_charset="utf8mb4", ) -<<<<<<< Updated upstream -rating = Table( - "chuni_profile_rating", - 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("type", String(255), nullable=False), - Column("index", Integer, nullable=False), - Column("musicId", Integer), - Column("difficultId", Integer), - Column("romVersionCode", Integer), - Column("score", Integer), - UniqueConstraint("user", "version", "type", "index", name="chuni_profile_rating_best_uk"), -======= -net_battle = Table( - "chuni_profile_net_battle", - metadata, - Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), - Column("isRankUpChallengeFailed", Boolean), - Column("highestBattleRankId", Integer), - Column("battleIconId", Integer), - Column("battleIconNum", Integer), - Column("avatarEffectPoint", Integer), ->>>>>>> Stashed changes - mysql_charset="utf8mb4", -) - class ChuniProfileData(BaseData): - async def update_name(self, user_id: int, new_name: str) -> bool: - sql = profile.update(profile.c.user == user_id).values(userName=new_name) - result = await self.execute(sql) - - if result is None: - self.logger.warning(f"Failed to set user {user_id} name to {new_name}") - return False - return True - - async def put_profile_data( + def put_profile_data( self, aime_id: int, version: int, profile_data: Dict ) -> Optional[int]: profile_data["user"] = aime_id @@ -457,45 +407,39 @@ class ChuniProfileData(BaseData): sql = insert(profile).values(**profile_data) conflict = sql.on_duplicate_key_update(**profile_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: - self.logger.warning( - 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 - async def get_profile_preview(self, aime_id: int, version: int) -> Optional[Row]: + 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)) ).order_by(profile.c.version.desc()) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async 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, - ) + 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, ) - .order_by(profile.c.version.desc()) - ) + ).order_by(profile.c.version.desc()) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def put_profile_data_ex( + def put_profile_data_ex( self, aime_id: int, version: int, profile_ex_data: Dict ) -> Optional[int]: profile_ex_data["user"] = aime_id @@ -505,7 +449,7 @@ class ChuniProfileData(BaseData): sql = insert(profile_ex).values(**profile_ex_data) conflict = sql.on_duplicate_key_update(**profile_ex_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning( @@ -514,31 +458,25 @@ class ChuniProfileData(BaseData): return None return result.lastrowid - async def get_profile_data_ex(self, aime_id: int, version: int) -> Optional[Row]: - sql = ( - select(profile_ex) - .where( - and_( - profile_ex.c.user == aime_id, - profile_ex.c.version <= version, - ) + def get_profile_data_ex(self, aime_id: int, version: int) -> Optional[Row]: + sql = select(profile_ex).where( + and_( + profile_ex.c.user == aime_id, + profile_ex.c.version <= version, ) - .order_by(profile_ex.c.version.desc()) - ) + ).order_by(profile_ex.c.version.desc()) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def put_profile_option( - self, aime_id: int, option_data: Dict - ) -> Optional[int]: + def put_profile_option(self, aime_id: int, option_data: Dict) -> Optional[int]: option_data["user"] = aime_id sql = insert(option).values(**option_data) conflict = sql.on_duplicate_key_update(**option_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning( @@ -547,22 +485,22 @@ class ChuniProfileData(BaseData): return None return result.lastrowid - async def get_profile_option(self, aime_id: int) -> Optional[Row]: + def get_profile_option(self, aime_id: int) -> Optional[Row]: sql = select(option).where(option.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def put_profile_option_ex( + def put_profile_option_ex( self, aime_id: int, option_ex_data: Dict ) -> Optional[int]: option_ex_data["user"] = aime_id sql = insert(option_ex).values(**option_ex_data) conflict = sql.on_duplicate_key_update(**option_ex_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning( @@ -571,15 +509,15 @@ class ChuniProfileData(BaseData): return None return result.lastrowid - async def get_profile_option_ex(self, aime_id: int) -> Optional[Row]: + def get_profile_option_ex(self, aime_id: int) -> Optional[Row]: sql = select(option_ex).where(option_ex.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def put_profile_recent_rating( + def put_profile_recent_rating( self, aime_id: int, recent_rating_data: List[Dict] ) -> Optional[int]: sql = insert(recent_rating).values( @@ -587,7 +525,7 @@ class ChuniProfileData(BaseData): ) conflict = sql.on_duplicate_key_update(recentRating=recent_rating_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning( f"put_profile_recent_rating: Failed to update! aime_id: {aime_id}" @@ -595,17 +533,15 @@ class ChuniProfileData(BaseData): return None return result.lastrowid - async def get_profile_recent_rating(self, aime_id: int) -> Optional[Row]: + def get_profile_recent_rating(self, aime_id: int) -> Optional[Row]: sql = select(recent_rating).where(recent_rating.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def put_profile_activity( - self, aime_id: int, activity_data: Dict - ) -> Optional[int]: + def put_profile_activity(self, aime_id: int, activity_data: Dict) -> Optional[int]: # The game just uses "id" but we need to distinguish that from the db column "id" activity_data["user"] = aime_id activity_data["activityId"] = activity_data["id"] @@ -613,7 +549,7 @@ class ChuniProfileData(BaseData): sql = insert(activity).values(**activity_data) conflict = sql.on_duplicate_key_update(**activity_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning( @@ -622,28 +558,24 @@ class ChuniProfileData(BaseData): return None return result.lastrowid - async def get_profile_activity( - self, aime_id: int, kind: int - ) -> Optional[List[Row]]: + def get_profile_activity(self, aime_id: int, kind: int) -> Optional[List[Row]]: sql = ( select(activity) .where(and_(activity.c.user == aime_id, activity.c.kind == kind)) .order_by(activity.c.sortNumber.desc()) # to get the last played track ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_profile_charge( - self, aime_id: int, charge_data: Dict - ) -> Optional[int]: + def put_profile_charge(self, aime_id: int, charge_data: Dict) -> Optional[int]: charge_data["user"] = aime_id sql = insert(charge).values(**charge_data) conflict = sql.on_duplicate_key_update(**charge_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning( @@ -652,42 +584,40 @@ class ChuniProfileData(BaseData): return None return result.lastrowid - async def get_profile_charge(self, aime_id: int) -> Optional[List[Row]]: + def get_profile_charge(self, aime_id: int) -> Optional[List[Row]]: sql = select(charge).where(charge.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def add_profile_region(self, aime_id: int, region_id: int) -> Optional[int]: + def add_profile_region(self, aime_id: int, region_id: int) -> Optional[int]: pass - async def get_profile_regions(self, aime_id: int) -> Optional[List[Row]]: + def get_profile_regions(self, aime_id: int) -> Optional[List[Row]]: pass - async def put_profile_emoney( - self, aime_id: int, emoney_data: Dict - ) -> Optional[int]: + def put_profile_emoney(self, aime_id: int, emoney_data: Dict) -> Optional[int]: emoney_data["user"] = aime_id sql = insert(emoney).values(**emoney_data) conflict = sql.on_duplicate_key_update(**emoney_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: return None return result.lastrowid - async def get_profile_emoney(self, aime_id: int) -> Optional[List[Row]]: + def get_profile_emoney(self, aime_id: int) -> Optional[List[Row]]: sql = select(emoney).where(emoney.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_profile_overpower( + def put_profile_overpower( self, aime_id: int, overpower_data: Dict ) -> Optional[int]: overpower_data["user"] = aime_id @@ -695,31 +625,33 @@ class ChuniProfileData(BaseData): sql = insert(overpower).values(**overpower_data) conflict = sql.on_duplicate_key_update(**overpower_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: return None return result.lastrowid - async def get_profile_overpower(self, aime_id: int) -> Optional[List[Row]]: + def get_profile_overpower(self, aime_id: int) -> Optional[List[Row]]: sql = select(overpower).where(overpower.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_team_by_id(self, team_id: int) -> Optional[Row]: + def get_team_by_id(self, team_id: int) -> Optional[Row]: sql = select(team).where(team.c.id == team_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_team_rank(self, team_id: int) -> int: + def get_team_rank(self, team_id: int) -> int: # Normal ranking system, likely the one used in the real servers # Query all teams sorted by 'teamPoint' - result = await self.execute(select(team.c.id).order_by(team.c.teamPoint.desc())) + result = self.execute( + select(team.c.id).order_by(team.c.teamPoint.desc()) + ) # Get the rank of the team with the given team_id rank = None @@ -734,86 +666,40 @@ class ChuniProfileData(BaseData): # RIP scaled team ranking. Gone, but forgotten # def get_team_rank_scaled(self, team_id: int) -> int: - async def update_team(self, team_id: int, team_data: Dict) -> bool: + def update_team(self, team_id: int, team_data: Dict) -> bool: team_data["id"] = team_id sql = insert(team).values(**team_data) conflict = sql.on_duplicate_key_update(**team_data) - result = await self.execute(conflict) - - if result is None: - self.logger.warn(f"update_team: Failed to update team! team id: {team_id}") - return False - return True - - async def get_rival(self, rival_id: int) -> Optional[Row]: - sql = select(profile).where(profile.c.user == rival_id) - result = await self.execute(sql) - - if result is None: - return None - return result.fetchone() - - async def get_overview(self) -> Dict: - # Fetch and add up all the playcounts - playcount_sql = await self.execute(select(profile.c.playCount)) - - if playcount_sql is None: - self.logger.warn(f"get_overview: Couldn't pull playcounts") - return 0 - - total_play_count = 0 - for row in playcount_sql: - total_play_count += row[0] -<<<<<<< Updated upstream - return { - "total_play_count": total_play_count - } - - async def put_profile_rating( - self, - aime_id: int, - version: int, - rating_type: str, - rating_data: List[Dict], - ): - inserted_values = [ - {"user": aime_id, "version": version, "type": rating_type, "index": i, **x} - for (i, x) in enumerate(rating_data) - ] - sql = insert(rating).values(inserted_values) - update_dict = {x.name: x for x in sql.inserted if x.name != "id"} - sql = sql.on_duplicate_key_update(**update_dict) - result = await self.execute(sql) + result = self.execute(conflict) if result is None: self.logger.warn( - f"put_profile_rating: Could not insert {rating_type}, aime_id: {aime_id}", + f"update_team: Failed to update team! team id: {team_id}" ) - return - -======= - return {"total_play_count": total_play_count} - - async def get_net_battle(self, aime_id: int) -> Optional[Row]: - sql = select(net_battle).where(net_battle.c.user == aime_id) - result = await self.execute(sql) + return False + return True + def get_rival(self, rival_id: int) -> Optional[Row]: + sql = select(profile).where(profile.c.user == rival_id) + result = self.execute(sql) if result is None: return None return result.fetchone() + def get_overview(self) -> Dict: + # Fetch and add up all the playcounts + playcount_sql = self.execute(select(profile.c.playCount)) - async def put_net_battle( - self, aime_id: int, net_battle_data: Dict - ) -> Optional[int]: - net_battle_data["user"] = aime_id + if playcount_sql is None: + self.logger.warn( + f"get_overview: Couldn't pull playcounts" + ) + return 0 - sql = insert(net_battle).values(**net_battle_data) - conflict = sql.on_duplicate_key_update(**net_battle_data) - - result = await self.execute(conflict) - if result is None: - return None ->>>>>>> Stashed changes - return result.lastrowid + total_play_count = 0; + for row in playcount_sql: + total_play_count += row[0] + return { + "total_play_count": total_play_count + } diff --git a/titles/chuni/schema/score.py b/titles/chuni/schema/score.py index c7adf9c..7e41b8f 100644 --- a/titles/chuni/schema/score.py +++ b/titles/chuni/schema/score.py @@ -1,12 +1,13 @@ from typing import Dict, List, Optional - -from core.data.schema import BaseData, metadata -from sqlalchemy import Column, Table, UniqueConstraint -from sqlalchemy.dialects.mysql import insert -from sqlalchemy.engine import Row +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.engine import Row from sqlalchemy.sql import func, select -from sqlalchemy.types import Boolean, Integer, String +from sqlalchemy.dialects.mysql import insert +from sqlalchemy.sql.expression import exists +from core.data.schema import BaseData, metadata course = Table( "chuni_score_course", @@ -136,62 +137,60 @@ playlog = Table( Column("regionId", Integer), Column("machineType", Integer), Column("ticketId", Integer), - mysql_charset="utf8mb4", + mysql_charset="utf8mb4" ) class ChuniScoreData(BaseData): - async def get_courses(self, aime_id: int) -> Optional[Row]: + def get_courses(self, aime_id: int) -> Optional[Row]: sql = select(course).where(course.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_course(self, aime_id: int, course_data: Dict) -> Optional[int]: + def put_course(self, aime_id: int, course_data: Dict) -> Optional[int]: course_data["user"] = aime_id course_data = self.fix_bools(course_data) sql = insert(course).values(**course_data) conflict = sql.on_duplicate_key_update(**course_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: return None return result.lastrowid - async def get_scores(self, aime_id: int) -> Optional[Row]: + def get_scores(self, aime_id: int) -> Optional[Row]: sql = select(best_score).where(best_score.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_score(self, aime_id: int, score_data: Dict) -> Optional[int]: + def put_score(self, aime_id: int, score_data: Dict) -> Optional[int]: score_data["user"] = aime_id score_data = self.fix_bools(score_data) sql = insert(best_score).values(**score_data) conflict = sql.on_duplicate_key_update(**score_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: return None return result.lastrowid - async def get_playlogs(self, aime_id: int) -> Optional[Row]: + def get_playlogs(self, aime_id: int) -> Optional[Row]: sql = select(playlog).where(playlog.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_playlog( - self, aime_id: int, playlog_data: Dict, version: int - ) -> Optional[int]: + def put_playlog(self, aime_id: int, playlog_data: Dict, version: int) -> Optional[int]: # Calculate the ROM version that should be inserted into the DB, based on the version of the ggame being inserted # We only need from Version 10 (Plost) and back, as newer versions include romVersion in their upsert # This matters both for gameRankings, as well as a future DB update to keep version data separate @@ -206,7 +205,7 @@ class ChuniScoreData(BaseData): 3: "1.15.0", 2: "1.10.0", 1: "1.05.0", - 0: "1.00.0", + 0: "1.00.0" } playlog_data["user"] = aime_id @@ -217,12 +216,12 @@ class ChuniScoreData(BaseData): sql = insert(playlog).values(**playlog_data) conflict = sql.on_duplicate_key_update(**playlog_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: return None return result.lastrowid - async def get_rankings(self, version: int) -> Optional[List[Dict]]: + def get_rankings(self, version: int) -> Optional[List[Dict]]: # Calculates the ROM version that should be fetched for rankings, based on the game version being retrieved # This prevents tracks that are not accessible in your version from counting towards the 10 results romVer = { @@ -239,24 +238,10 @@ class ChuniScoreData(BaseData): 3: "1.15%", 2: "1.10%", 1: "1.05%", - 0: "1.00%", + 0: "1.00%" } - sql = ( - select( - [ - playlog.c.musicId.label("id"), - func.count(playlog.c.musicId).label("point"), - ] - ) - .where( - (playlog.c.level != 4) - & (playlog.c.romVersion.like(romVer.get(version, "%"))) - ) - .group_by(playlog.c.musicId) - .order_by(func.count(playlog.c.musicId).desc()) - .limit(10) - ) - result = await self.execute(sql) + sql = select([playlog.c.musicId.label('id'), func.count(playlog.c.musicId).label('point')]).where((playlog.c.level != 4) & (playlog.c.romVersion.like(romVer.get(version, "%")))).group_by(playlog.c.musicId).order_by(func.count(playlog.c.musicId).desc()).limit(10) + result = self.execute(sql) if result is None: return None @@ -264,10 +249,10 @@ class ChuniScoreData(BaseData): rows = result.fetchall() return [dict(row) for row in rows] - async def get_rival_music(self, rival_id: int) -> Optional[List[Dict]]: + def get_rival_music(self, rival_id: int) -> Optional[List[Dict]]: sql = select(best_score).where(best_score.c.user == rival_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() diff --git a/titles/chuni/schema/static.py b/titles/chuni/schema/static.py index 0c31b69..fe32d41 100644 --- a/titles/chuni/schema/static.py +++ b/titles/chuni/schema/static.py @@ -1,18 +1,21 @@ from typing import Dict, List, Optional - -from core.data.schema import BaseData, metadata from sqlalchemy import ( - Column, ForeignKeyConstraint, - PrimaryKeyConstraint, Table, + Column, UniqueConstraint, + PrimaryKeyConstraint, and_, ) -from sqlalchemy.dialects.mysql import insert +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.types import TIMESTAMP, Boolean, Float, Integer, String +from sqlalchemy.dialects.mysql import insert +from datetime import datetime + +from core.data.schema import BaseData, metadata events = Table( "chuni_static_events", @@ -172,7 +175,7 @@ login_bonus = Table( class ChuniStaticData(BaseData): - async def put_login_bonus( + def put_login_bonus( self, version: int, preset_id: int, @@ -204,12 +207,12 @@ class ChuniStaticData(BaseData): loginBonusCategoryType=login_bonus_category_type, ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: return None return result.lastrowid - async def get_login_bonus( + def get_login_bonus( self, version: int, preset_id: int, @@ -221,12 +224,12 @@ class ChuniStaticData(BaseData): ) ).order_by(login_bonus.c.needLoginDayCount.desc()) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_login_bonus_by_required_days( + def get_login_bonus_by_required_days( self, version: int, preset_id: int, need_login_day_count: int ) -> Optional[Row]: sql = login_bonus.select( @@ -237,12 +240,12 @@ class ChuniStaticData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def put_login_bonus_preset( + def put_login_bonus_preset( self, version: int, preset_id: int, preset_name: str, is_enabled: bool ) -> Optional[int]: sql = insert(login_bonus_preset).values( @@ -256,12 +259,12 @@ class ChuniStaticData(BaseData): presetName=preset_name, isEnabled=is_enabled ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: return None return result.lastrowid - async def get_login_bonus_presets( + def get_login_bonus_presets( self, version: int, is_enabled: bool = True ) -> Optional[List[Row]]: sql = login_bonus_preset.select( @@ -271,12 +274,12 @@ class ChuniStaticData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_event( + def put_event( self, version: int, event_id: int, type: int, name: str ) -> Optional[int]: sql = insert(events).values( @@ -285,19 +288,19 @@ class ChuniStaticData(BaseData): conflict = sql.on_duplicate_key_update(name=name) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: return None return result.lastrowid - async def update_event( + def update_event( self, version: int, event_id: int, enabled: bool ) -> Optional[bool]: sql = events.update( and_(events.c.version == version, events.c.eventId == event_id) ).values(enabled=enabled) - result = await self.execute(sql) + result = self.execute(sql) if result is None: self.logger.warning( f"update_event: failed to update event! version: {version}, event_id: {event_id}, enabled: {enabled}" @@ -312,35 +315,35 @@ class ChuniStaticData(BaseData): return None return event["enabled"] - async def get_event(self, version: int, event_id: int) -> Optional[Row]: + def get_event(self, version: int, event_id: int) -> Optional[Row]: sql = select(events).where( and_(events.c.version == version, events.c.eventId == event_id) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_enabled_events(self, version: int) -> Optional[List[Row]]: + def get_enabled_events(self, version: int) -> Optional[List[Row]]: sql = select(events).where( and_(events.c.version == version, events.c.enabled == True) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_events(self, version: int) -> Optional[List[Row]]: + def get_events(self, version: int) -> Optional[List[Row]]: sql = select(events).where(events.c.version == version) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_music( + def put_music( self, version: int, song_id: int, @@ -373,12 +376,12 @@ class ChuniStaticData(BaseData): worldsEndTag=we_tag, ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: return None return result.lastrowid - async def put_charge( + def put_charge( self, version: int, charge_id: int, @@ -403,38 +406,38 @@ class ChuniStaticData(BaseData): sellingAppeal=selling_appeal, ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: return None return result.lastrowid - async def get_enabled_charges(self, version: int) -> Optional[List[Row]]: + def get_enabled_charges(self, version: int) -> Optional[List[Row]]: sql = select(charge).where( and_(charge.c.version == version, charge.c.enabled == True) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_charges(self, version: int) -> Optional[List[Row]]: + def get_charges(self, version: int) -> Optional[List[Row]]: sql = select(charge).where(charge.c.version == version) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_music(self, version: int) -> Optional[List[Row]]: + def get_music(self, version: int) -> Optional[List[Row]]: sql = music.select(music.c.version <= version) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_music_chart( + def get_music_chart( self, version: int, song_id: int, chart_id: int ) -> Optional[List[Row]]: sql = select(music).where( @@ -445,20 +448,21 @@ class ChuniStaticData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_song(self, music_id: int) -> Optional[Row]: + def get_song(self, music_id: int) -> Optional[Row]: sql = music.select(music.c.id == music_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def put_avatar( + + def put_avatar( self, version: int, avatarAccessoryId: int, @@ -483,12 +487,12 @@ class ChuniStaticData(BaseData): texturePath=texturePath, ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: return None return result.lastrowid - async def put_gacha( + def put_gacha( self, version: int, gacha_id: int, @@ -509,33 +513,33 @@ class ChuniStaticData(BaseData): **gacha_data, ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning(f"Failed to insert gacha! gacha_id {gacha_id}") return None return result.lastrowid - async def get_gachas(self, version: int) -> Optional[List[Dict]]: + def get_gachas(self, version: int) -> Optional[List[Dict]]: sql = gachas.select(gachas.c.version <= version).order_by( gachas.c.gachaId.asc() ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_gacha(self, version: int, gacha_id: int) -> Optional[Dict]: + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def put_gacha_card( + 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) @@ -544,21 +548,21 @@ class ChuniStaticData(BaseData): gachaId=gacha_id, cardId=card_id, **gacha_card ) - result = await self.execute(conflict) + 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 - async def get_gacha_cards(self, gacha_id: int) -> Optional[List[Dict]]: + def get_gacha_cards(self, gacha_id: int) -> Optional[List[Dict]]: sql = gacha_cards.select(gacha_cards.c.gachaId == gacha_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_gacha_card_by_character( + def get_gacha_card_by_character( self, gacha_id: int, chara_id: int ) -> Optional[Dict]: sql_sub = ( @@ -570,26 +574,26 @@ class ChuniStaticData(BaseData): and_(gacha_cards.c.gachaId == gacha_id, gacha_cards.c.cardId == sql_sub) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def put_card(self, version: int, card_id: int, **card_data) -> Optional[int]: + 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 = await self.execute(conflict) + 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 - async def get_card(self, version: int, card_id: int) -> Optional[Dict]: + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: return None - return result.fetchone() + return result.fetchone() \ No newline at end of file diff --git a/titles/chuni/star.py b/titles/chuni/star.py index c9d13f7..4c071e8 100644 --- a/titles/chuni/star.py +++ b/titles/chuni/star.py @@ -2,8 +2,8 @@ from typing import Dict from core.config import CoreConfig from titles.chuni.base import ChuniBase -from titles.chuni.config import ChuniConfig from titles.chuni.const import ChuniConstants +from titles.chuni.config import ChuniConfig class ChuniStar(ChuniBase): @@ -11,7 +11,7 @@ class ChuniStar(ChuniBase): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_STAR - async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = await super().handle_get_game_setting_api_request(data) + 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" return ret diff --git a/titles/chuni/starplus.py b/titles/chuni/starplus.py index 411f981..8c24cc8 100644 --- a/titles/chuni/starplus.py +++ b/titles/chuni/starplus.py @@ -2,8 +2,8 @@ from typing import Dict from core.config import CoreConfig from titles.chuni.base import ChuniBase -from titles.chuni.config import ChuniConfig from titles.chuni.const import ChuniConstants +from titles.chuni.config import ChuniConfig class ChuniStarPlus(ChuniBase): @@ -11,7 +11,7 @@ class ChuniStarPlus(ChuniBase): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_STAR_PLUS - async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = await super().handle_get_game_setting_api_request(data) + 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" return ret diff --git a/titles/chuni/sun.py b/titles/chuni/sun.py index 75161d9..9a82a65 100644 --- a/titles/chuni/sun.py +++ b/titles/chuni/sun.py @@ -1,9 +1,9 @@ -from typing import Dict +from typing import Dict, Any from core.config import CoreConfig -from titles.chuni.config import ChuniConfig -from titles.chuni.const import ChuniConstants from titles.chuni.newplus import ChuniNewPlus +from titles.chuni.const import ChuniConstants +from titles.chuni.config import ChuniConfig class ChuniSun(ChuniNewPlus): @@ -11,9 +11,27 @@ class ChuniSun(ChuniNewPlus): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_SUN - async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: - user_data = await super().handle_cm_get_user_preview_api_request(data) + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = super().handle_get_game_setting_api_request(data) + ret["gameSetting"]["romVersion"] = self.game_cfg.version.version(self.version)["rom"] + ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version(self.version)["data"] + ret["gameSetting"][ + "matchingUri" + ] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/210/ChuniServlet/" + ret["gameSetting"][ + "matchingUriX" + ] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/210/ChuniServlet/" + ret["gameSetting"][ + "udpHolePunchUri" + ] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/210/ChuniServlet/" + ret["gameSetting"][ + "reflectorUri" + ] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/210/ChuniServlet/" + return ret + + def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = super().handle_cm_get_user_preview_api_request(data) # hardcode lastDataVersion for CardMaker 1.35 A032 user_data["lastDataVersion"] = "2.10.00" - return user_data + return user_data \ No newline at end of file diff --git a/titles/chuni/sunplus.py b/titles/chuni/sunplus.py index 8279cbb..66a7f3b 100644 --- a/titles/chuni/sunplus.py +++ b/titles/chuni/sunplus.py @@ -1,9 +1,9 @@ -from typing import Dict +from typing import Dict, Any from core.config import CoreConfig -from titles.chuni.config import ChuniConfig -from titles.chuni.const import ChuniConstants from titles.chuni.sun import ChuniSun +from titles.chuni.const import ChuniConstants +from titles.chuni.config import ChuniConfig class ChuniSunPlus(ChuniSun): @@ -11,8 +11,26 @@ class ChuniSunPlus(ChuniSun): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_SUN_PLUS - async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: - user_data = await super().handle_cm_get_user_preview_api_request(data) + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = super().handle_get_game_setting_api_request(data) + ret["gameSetting"]["romVersion"] = self.game_cfg.version.version(self.version)["rom"] + ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version(self.version)["data"] + ret["gameSetting"][ + "matchingUri" + ] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/215/ChuniServlet/" + ret["gameSetting"][ + "matchingUriX" + ] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/215/ChuniServlet/" + ret["gameSetting"][ + "udpHolePunchUri" + ] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/215/ChuniServlet/" + ret["gameSetting"][ + "reflectorUri" + ] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/215/ChuniServlet/" + return ret + + def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = super().handle_cm_get_user_preview_api_request(data) # I don't know if lastDataVersion is going to matter, I don't think CardMaker 1.35 works this far up user_data["lastDataVersion"] = "2.15.00" diff --git a/titles/chuni/templates/chuni_index.jinja b/titles/chuni/templates/chuni_index.jinja deleted file mode 100644 index e310054..0000000 --- a/titles/chuni/templates/chuni_index.jinja +++ /dev/null @@ -1,43 +0,0 @@ -{% extends "core/templates/index.jinja" %} -{% block content %} -

Chunithm

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

Profile for {{ profile.userName }} 

-{% if error is defined %} -{% include "core/templates/widgets/err_banner.jinja" %} -{% endif %} -{% if success is defined and success == 1 %} -
-Update successful -
-{% endif %} - -{% elif sesh is defined and sesh is not none and sesh.user_id > 0 %} -No profile information found for this account. -{% else %} -Login to view profile information. -{% endif %} -{% endblock content %} \ No newline at end of file diff --git a/titles/cm/__init__.py b/titles/cm/__init__.py index 99018d3..1115f96 100644 --- a/titles/cm/__init__.py +++ b/titles/cm/__init__.py @@ -1,9 +1,12 @@ -from titles.cm.const import CardMakerConstants -from titles.cm.database import CardMakerData from titles.cm.index import CardMakerServlet +from titles.cm.const import CardMakerConstants from titles.cm.read import CardMakerReader +from titles.cm.database import CardMakerData index = CardMakerServlet reader = CardMakerReader database = CardMakerData + game_codes = [CardMakerConstants.GAME_CODE] + +current_schema_version = 1 diff --git a/titles/cm/base.py b/titles/cm/base.py index fd4acc8..b911983 100644 --- a/titles/cm/base.py +++ b/titles/cm/base.py @@ -1,12 +1,15 @@ +from datetime import date, datetime, timedelta +from typing import Any, Dict, List +import json import logging -from datetime import datetime, timedelta -from typing import Dict +from enum import Enum import pytz from core.config import CoreConfig from core.utils import Utils -from titles.cm.config import CardMakerConfig +from core.data.cache import cached from titles.cm.const import CardMakerConstants +from titles.cm.config import CardMakerConfig class CardMakerBase: @@ -26,14 +29,11 @@ class CardMakerBase: def _parse_int_ver(version: str) -> str: return version.replace(".", "")[:3] - async def handle_get_game_connect_api_request(self, data: Dict) -> Dict: - if ( - not self.core_cfg.server.is_using_proxy - and Utils.get_title_port(self.core_cfg) != 80 - ): - uri = f"http://{self.core_cfg.server.hostname}:{Utils.get_title_port(self.core_cfg)}" + def handle_get_game_connect_api_request(self, data: Dict) -> Dict: + if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80: + uri = f"http://{self.core_cfg.title.hostname}:{Utils.get_title_port(self.core_cfg)}" else: - uri = f"http://{self.core_cfg.server.hostname}" + uri = f"http://{self.core_cfg.title.hostname}" # grab the dict with all games version numbers from user config games_ver = self.game_cfg.version.version(self.version) @@ -51,7 +51,7 @@ class CardMakerBase: { "modelKind": 1, "type": 1, - "titleUri": f"{uri}/SDEZ/{self._parse_int_ver(games_ver['maimai'])}/Maimai2Servlet/", + "titleUri": f"{uri}/{self._parse_int_ver(games_ver['maimai'])}/Maimai2Servlet/", }, # ONGEKI { @@ -62,12 +62,9 @@ class CardMakerBase: ], } - async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: # 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 == "" - ): + 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 ) @@ -76,29 +73,15 @@ class CardMakerBase: ) else: # get current datetime in JST - current_jst = datetime.now(pytz.timezone("Asia/Tokyo")).date() + 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" - ) + 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"), - ) + 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) @@ -127,11 +110,11 @@ class CardMakerBase: "isAou": False, } - async def handle_get_client_bookkeeping_api_request(self, data: Dict) -> Dict: + def handle_get_client_bookkeeping_api_request(self, data: Dict) -> Dict: return {"placeId": data["placeId"], "length": 0, "clientBookkeepingList": []} - async def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: + def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "UpsertClientSettingApi"} - async def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: + def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "UpsertClientBookkeepingApi"} diff --git a/titles/cm/cm135.py b/titles/cm/cm135.py index a0dc440..e134974 100644 --- a/titles/cm/cm135.py +++ b/titles/cm/cm135.py @@ -1,9 +1,10 @@ from typing import Dict from core.config import CoreConfig +from core.data.cache import cached from titles.cm.base import CardMakerBase -from titles.cm.config import CardMakerConfig from titles.cm.const import CardMakerConstants +from titles.cm.config import CardMakerConfig class CardMaker135(CardMakerBase): @@ -11,7 +12,7 @@ class CardMaker135(CardMakerBase): super().__init__(core_cfg, game_cfg) self.version = CardMakerConstants.VER_CARD_MAKER_135 - async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = await super().handle_get_game_setting_api_request(data) + 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" return ret diff --git a/titles/cm/config.py b/titles/cm/config.py index 63fd8bb..ad96a7d 100644 --- a/titles/cm/config.py +++ b/titles/cm/config.py @@ -1,5 +1,4 @@ from typing import Dict - from core.config import CoreConfig @@ -32,13 +31,18 @@ class CardMakerVersionConfig: 1: {"ongeki": 1.30.01, "chuni": 2.00.00, "maimai": 1.20.00} """ return CoreConfig.get_config_field( - self.__config, - "cardmaker", - "version", - default={ - 0: {"ongeki": "1.30.01", "chuni": "2.00.00", "maimai": "1.20.00"}, - 1: {"ongeki": "1.35.03", "chuni": "2.10.00", "maimai": "1.30.00"}, - }, + self.__config, "cardmaker", "version", default={ + 0: { + "ongeki": "1.30.01", + "chuni": "2.00.00", + "maimai": "1.20.00" + }, + 1: { + "ongeki": "1.35.03", + "chuni": "2.10.00", + "maimai": "1.30.00" + } + } )[version] diff --git a/titles/cm/database.py b/titles/cm/database.py index ad9e1c1..1d32109 100644 --- a/titles/cm/database.py +++ b/titles/cm/database.py @@ -1,5 +1,5 @@ -from core.config import CoreConfig from core.data import Data +from core.config import CoreConfig class CardMakerData(Data): diff --git a/titles/cm/index.py b/titles/cm/index.py index 96fba57..489b846 100644 --- a/titles/cm/index.py +++ b/titles/cm/index.py @@ -1,25 +1,23 @@ import json -import logging -import string -import zlib -from logging.handlers import TimedRotatingFileHandler -from os import path -from typing import List - -import coloredlogs import inflection import yaml -from core.config import CoreConfig -from core.title import BaseServlet -from core.utils import Utils -from starlette.requests import Request -from starlette.responses import Response -from starlette.routing import Route +import string +import logging +import coloredlogs +import zlib -from .base import CardMakerBase -from .cm135 import CardMaker135 +from os import path +from typing import Tuple, List, Dict +from twisted.web.http import Request +from logging.handlers import TimedRotatingFileHandler + +from core.config import CoreConfig +from core.utils import Utils +from core.title import BaseServlet from .config import CardMakerConfig from .const import CardMakerConstants +from .base import CardMakerBase +from .cm135 import CardMaker135 class CardMakerServlet(BaseServlet): @@ -33,7 +31,7 @@ class CardMakerServlet(BaseServlet): self.versions = [ CardMakerBase(core_cfg, self.game_cfg), - CardMaker135(core_cfg, self.game_cfg), + CardMaker135(core_cfg, self.game_cfg) ] self.logger = logging.getLogger("cardmaker") @@ -58,7 +56,7 @@ class CardMakerServlet(BaseServlet): 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 @@ -73,18 +71,17 @@ class CardMakerServlet(BaseServlet): return False return True + + def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: + return ( + [], + [("render_POST", "/SDED/{version}/{endpoint}", {})] + ) - def get_routes(self) -> List[Route]: - return [ - Route( - "/SDED/{version:int}/{endpoint:str}", self.render_POST, methods=["POST"] - ) - ] - - async def render_POST(self, request: Request) -> bytes: - version: int = request.path_params.get("version") - endpoint: str = request.path_params.get("endpoint") - req_raw = await request.body() + def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes: + version = int(matchers['version']) + endpoint = matchers['endpoint'] + req_raw = request.content.getvalue() internal_ver = 0 client_ip = Utils.get_ip_addr(request) @@ -106,7 +103,7 @@ class CardMakerServlet(BaseServlet): self.logger.error( f"Failed to decompress v{version} {endpoint} request -> {e}" ) - return Response(zlib.compress(b'{"stat": "0"}')) + return zlib.compress(b'{"stat": "0"}') req_data = json.loads(unzip) @@ -117,22 +114,20 @@ class CardMakerServlet(BaseServlet): if not hasattr(self.versions[internal_ver], func_to_find): self.logger.warning(f"Unhandled v{version} request {endpoint}") - return Response(zlib.compress(b'{"returnCode": 1}')) + return zlib.compress(b'{"returnCode": 1}') try: handler = getattr(self.versions[internal_ver], func_to_find) - resp = await handler(req_data) + resp = handler(req_data) except Exception as e: self.logger.error(f"Error handling v{version} method {endpoint} - {e}") raise - return Response(zlib.compress(b'{"stat": "0"}')) + return zlib.compress(b'{"stat": "0"}') if resp is None: resp = {"returnCode": 1} self.logger.debug(f"Response {resp}") - return Response( - zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) - ) + return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) diff --git a/titles/cm/read.py b/titles/cm/read.py index f36e25d..80684bd 100644 --- a/titles/cm/read.py +++ b/titles/cm/read.py @@ -1,18 +1,21 @@ -import csv +from decimal import Decimal +import logging import os import re +import csv import xml.etree.ElementTree as ET -from typing import Optional +from typing import Any, Dict, List, Optional -from core.config import CoreConfig from read import BaseReader -from titles.chuni.const import ChuniConstants -from titles.chuni.database import ChuniData -from titles.cm.const import CardMakerConstants -from titles.mai2.const import Mai2Constants -from titles.mai2.database import Mai2Data -from titles.ongeki.const import OngekiConstants +from core.config import CoreConfig from titles.ongeki.database import OngekiData +from titles.cm.const import CardMakerConstants +from titles.ongeki.const import OngekiConstants +from titles.ongeki.config import OngekiConfig +from titles.mai2.database import Mai2Data +from titles.mai2.const import Mai2Constants +from titles.chuni.database import ChuniData +from titles.chuni.const import ChuniConstants class CardMakerReader(BaseReader): @@ -47,7 +50,7 @@ class CardMakerReader(BaseReader): ): return f"{root}/{dir}" - async def read(self) -> None: + def read(self) -> None: static_datas = { "static_gachas.csv": "read_ongeki_gacha_csv", "static_gacha_cards.csv": "read_ongeki_gacha_card_csv", @@ -56,14 +59,14 @@ class CardMakerReader(BaseReader): if self.bin_dir is not None: data_dir = self._get_card_maker_directory(self.bin_dir) - await self.read_chuni_card(f"{data_dir}/CHU/Data/A000/card") - await self.read_chuni_gacha(f"{data_dir}/CHU/Data/A000/gacha") + self.read_chuni_card(f"{data_dir}/CHU/Data/A000/card") + self.read_chuni_gacha(f"{data_dir}/CHU/Data/A000/gacha") - await self.read_mai2_card(f"{data_dir}/MAI/Data/A000/card") + self.read_mai2_card(f"{data_dir}/MAI/Data/A000/card") for file, func in static_datas.items(): if os.path.exists(f"{self.bin_dir}/MU3/{file}"): read_csv = getattr(CardMakerReader, func) - await read_csv(self, f"{self.bin_dir}/MU3/{file}") + read_csv(self, f"{self.bin_dir}/MU3/{file}") else: self.logger.warning( f"Couldn't find {file} file in {self.bin_dir}, skipping" @@ -75,12 +78,12 @@ class CardMakerReader(BaseReader): # ONGEKI (MU3) cnnot easily access the bin data(A000.pac) # so only opt_dir will work for now for dir in data_dirs: - await self.read_chuni_card(f"{dir}/CHU/card") - await self.read_chuni_gacha(f"{dir}/CHU/gacha") - await self.read_mai2_card(f"{dir}/MAI/card") - await self.read_ongeki_gacha(f"{dir}/MU3/gacha") + self.read_chuni_card(f"{dir}/CHU/card") + self.read_chuni_gacha(f"{dir}/CHU/gacha") + self.read_mai2_card(f"{dir}/MAI/card") + self.read_ongeki_gacha(f"{dir}/MU3/gacha") - async def read_chuni_card(self, base_dir: str) -> None: + def read_chuni_card(self, base_dir: str) -> None: self.logger.info(f"Reading cards from {base_dir}...") version_ids = { @@ -111,7 +114,7 @@ class CardMakerReader(BaseReader): chain = int(troot.find("chain").text) skill_name = troot.find("skillName").text - await self.chuni_data.static.put_card( + self.chuni_data.static.put_card( version, card_id, charaName=chara_name, @@ -128,7 +131,7 @@ class CardMakerReader(BaseReader): self.logger.info(f"Added chuni card {card_id}") - async def read_chuni_gacha(self, base_dir: str) -> None: + def read_chuni_gacha(self, base_dir: str) -> None: self.logger.info(f"Reading gachas from {base_dir}...") version_ids = { @@ -155,7 +158,7 @@ class CardMakerReader(BaseReader): True if troot.find("ceilingType").text == "1" else False ) - await self.chuni_data.static.put_gacha( + self.chuni_data.static.put_gacha( version, gacha_id, name, @@ -178,7 +181,7 @@ class CardMakerReader(BaseReader): True if gacha_card.find("pickup").text == "1" else False ) - await self.chuni_data.static.put_gacha_card( + self.chuni_data.static.put_gacha_card( gacha_id, card_id, weight=weight, @@ -190,7 +193,7 @@ class CardMakerReader(BaseReader): f"Added chuni card {card_id} to gacha {gacha_id}" ) - async def read_mai2_card(self, base_dir: str) -> None: + def read_mai2_card(self, base_dir: str) -> None: self.logger.info(f"Reading cards from {base_dir}...") version_ids = { @@ -229,18 +232,18 @@ class CardMakerReader(BaseReader): False if re.search(r"\d{2}/\d{2}/\d{2}", name) else enabled ) - await self.mai2_data.static.put_card( + self.mai2_data.static.put_card( version, card_id, name, enabled=enabled ) self.logger.info(f"Added mai2 card {card_id}") - async def read_ongeki_gacha_csv(self, file_path: str) -> None: + def read_ongeki_gacha_csv(self, file_path: str) -> None: self.logger.info(f"Reading gachas from {file_path}...") with open(file_path, encoding="utf-8") as f: reader = csv.DictReader(f) for row in reader: - await self.ongeki_data.static.put_gacha( + self.ongeki_data.static.put_gacha( row["version"], row["gachaId"], row["gachaName"], @@ -252,13 +255,13 @@ class CardMakerReader(BaseReader): self.logger.info(f"Added ongeki gacha {row['gachaId']}") - async def read_ongeki_gacha_card_csv(self, file_path: str) -> None: + def read_ongeki_gacha_card_csv(self, file_path: str) -> None: self.logger.info(f"Reading gacha cards from {file_path}...") with open(file_path, encoding="utf-8") as f: reader = csv.DictReader(f) for row in reader: - await self.ongeki_data.static.put_gacha_card( + self.ongeki_data.static.put_gacha_card( row["gachaId"], row["cardId"], rarity=row["rarity"], @@ -269,7 +272,7 @@ class CardMakerReader(BaseReader): self.logger.info(f"Added ongeki card {row['cardId']} to gacha") - async def read_ongeki_gacha(self, base_dir: str) -> None: + def read_ongeki_gacha(self, base_dir: str) -> None: self.logger.info(f"Reading gachas from {base_dir}...") # assuming some GachaKinds based on the GachaType @@ -292,7 +295,7 @@ class CardMakerReader(BaseReader): # skip already existing gachas if ( - await self.ongeki_data.static.get_gacha( + self.ongeki_data.static.get_gacha( OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY, gacha_id ) is not None @@ -318,7 +321,7 @@ class CardMakerReader(BaseReader): is_ceiling = 1 max_select_point = 33 - await self.ongeki_data.static.put_gacha( + self.ongeki_data.static.put_gacha( version, gacha_id, name, diff --git a/titles/cxb/__init__.py b/titles/cxb/__init__.py index 53ea00d..37abdab 100644 --- a/titles/cxb/__init__.py +++ b/titles/cxb/__init__.py @@ -1,9 +1,10 @@ +from titles.cxb.index import CxbServlet from titles.cxb.const import CxbConstants from titles.cxb.database import CxbData -from titles.cxb.index import CxbServlet from titles.cxb.read import CxbReader index = CxbServlet database = CxbData reader = CxbReader game_codes = [CxbConstants.GAME_CODE] +current_schema_version = 1 diff --git a/titles/cxb/base.py b/titles/cxb/base.py index 7f23afc..fe583e6 100644 --- a/titles/cxb/base.py +++ b/titles/cxb/base.py @@ -1,16 +1,16 @@ -import json import logging +import json +from decimal import Decimal from base64 import b64encode -from os import path -from threading import Thread from typing import Any, Dict, List +from os import path from core.config import CoreConfig - from .config import CxbConfig from .const import CxbConstants from .database import CxbData +from threading import Thread class CxbBase: def __init__(self, cfg: CoreConfig, game_cfg: CxbConfig) -> None: @@ -21,25 +21,21 @@ class CxbBase: self.logger = logging.getLogger("cxb") self.version = CxbConstants.VER_CROSSBEATS_REV - def _get_data_contents( - self, folder: str, filetype: str, encoding: str = None, subfolder: str = "" - ) -> List[str]: + def _get_data_contents(self, folder: str, filetype: str, encoding: str = None, subfolder: str = "") -> List[str]: if path.exists(f"titles/cxb/data/{folder}/{subfolder}{filetype}.csv"): - with open( - f"titles/cxb/data/{folder}/{subfolder}{filetype}.csv", encoding=encoding - ) as f: + with open(f"titles/cxb/data/{folder}/{subfolder}{filetype}.csv", encoding=encoding) as f: return f.readlines() - + return [] - async def handle_action_rpreq_request(self, data: Dict) -> Dict: + def handle_action_rpreq_request(self, data: Dict) -> Dict: return {} - async def handle_action_hitreq_request(self, data: Dict) -> Dict: + def handle_action_hitreq_request(self, data: Dict) -> Dict: return {"data": []} - async def handle_auth_usercheck_request(self, data: Dict) -> Dict: - profile = await self.data.profile.get_profile_index( + def handle_auth_usercheck_request(self, data: Dict) -> Dict: + profile = self.data.profile.get_profile_index( 0, data["usercheck"]["authid"], self.version ) if profile is not None: @@ -49,12 +45,12 @@ class CxbBase: self.logger.info(f"No profile for aime id {data['usercheck']['authid']}") return {"exist": "false", "logout": "true"} - async def handle_auth_entry_request(self, data: Dict) -> Dict: + def handle_auth_entry_request(self, data: Dict) -> Dict: self.logger.info(f"New profile for {data['entry']['authid']}") return {"token": data["entry"]["authid"], "uid": data["entry"]["authid"]} - async def handle_auth_login_request(self, data: Dict) -> Dict: - profile = await self.data.profile.get_profile_index( + def handle_auth_login_request(self, data: Dict) -> Dict: + profile = self.data.profile.get_profile_index( 0, data["login"]["authid"], self.version ) @@ -202,14 +198,14 @@ class CxbBase: ).decode("utf-8") ) - async def handle_action_loadrange_request(self, data: Dict) -> Dict: + def handle_action_loadrange_request(self, data: Dict) -> Dict: range_start = data["loadrange"]["range"][0] range_end = data["loadrange"]["range"][1] uid = data["loadrange"]["uid"] self.logger.info(f"Load data for {uid}") - profile = await self.data.profile.get_profile(uid, self.version) - songs = await self.data.score.get_best_scores(uid) + profile = self.data.profile.get_profile(uid, self.version) + songs = self.data.score.get_best_scores(uid) data1 = [] index = [] @@ -257,12 +253,8 @@ class CxbBase: # Async threads to generate the response thread_Coupon = Thread(target=CxbBase.task_generateCoupon(index, data1)) - thread_ShopListTitle = Thread( - target=CxbBase.task_generateShopListTitle(index, data1) - ) - thread_ShopListIcon = Thread( - target=CxbBase.task_generateShopListIcon(index, data1) - ) + thread_ShopListTitle = Thread(target=CxbBase.task_generateShopListTitle(index, data1)) + thread_ShopListIcon = Thread(target=CxbBase.task_generateShopListIcon(index, data1)) thread_Stories = Thread(target=CxbBase.task_generateStories(index, data1)) thread_Coupon.start() @@ -276,12 +268,10 @@ class CxbBase: thread_Stories.join() for song in songs: - thread_ScoreData = Thread( - target=CxbBase.task_generateScoreData(song, index, data1) - ) + thread_ScoreData = Thread(target=CxbBase.task_generateScoreData(song, index, data1)) thread_ScoreData.start() - v_profile = await self.data.profile.get_profile_index(0, uid, self.version) + v_profile = self.data.profile.get_profile_index(0, uid, self.version) v_profile_data = v_profile["data"] for _, data in enumerate(profile): @@ -292,7 +282,7 @@ class CxbBase: return {"index": index, "data": data1, "version": versionindex} - async def handle_action_saveindex_request(self, data: Dict) -> Dict: + def handle_action_saveindex_request(self, data: Dict) -> Dict: save_data = data["saveindex"] try: @@ -310,11 +300,11 @@ class CxbBase: for value in data["saveindex"]["data"]: if "playedUserId" in value[1]: - await self.data.profile.put_profile( + self.data.profile.put_profile( data["saveindex"]["uid"], self.version, value[0], value[1] ) if "mcode" not in value[1]: - await self.data.profile.put_profile( + self.data.profile.put_profile( data["saveindex"]["uid"], self.version, value[0], value[1] ) if "shopId" in value: @@ -345,7 +335,7 @@ class CxbBase: "index": value[0], } ) - await self.data.score.put_best_score( + self.data.score.put_best_score( data["saveindex"]["uid"], song_json["mcode"], self.version, @@ -370,32 +360,32 @@ class CxbBase: for index, value in enumerate(data["saveindex"]["data"]): if int(data["saveindex"]["index"][index]) == 101: - await self.data.profile.put_profile( + self.data.profile.put_profile( aimeId, self.version, data["saveindex"]["index"][index], value ) if ( int(data["saveindex"]["index"][index]) >= 700000 and int(data["saveindex"]["index"][index]) <= 701000 ): - await self.data.profile.put_profile( + self.data.profile.put_profile( aimeId, self.version, data["saveindex"]["index"][index], value ) if ( int(data["saveindex"]["index"][index]) >= 500 and int(data["saveindex"]["index"][index]) <= 510 ): - await self.data.profile.put_profile( + self.data.profile.put_profile( aimeId, self.version, data["saveindex"]["index"][index], value ) if "playedUserId" in value: - await self.data.profile.put_profile( + self.data.profile.put_profile( aimeId, self.version, data["saveindex"]["index"][index], json.loads(value), ) if "mcode" not in value and "normalCR" not in value: - await self.data.profile.put_profile( + self.data.profile.put_profile( aimeId, self.version, data["saveindex"]["index"][index], @@ -447,16 +437,16 @@ class CxbBase: } ) - await self.data.score.put_best_score( + self.data.score.put_best_score( aimeId, data1["mcode"], self.version, indexSongList[i], songCode[0] ) i += 1 return {} - async def handle_action_sprankreq_request(self, data: Dict) -> Dict: + def handle_action_sprankreq_request(self, data: Dict) -> Dict: uid = data["sprankreq"]["uid"] self.logger.info(f"Get best rankings for {uid}") - p = await self.data.score.get_best_rankings(uid) + p = self.data.score.get_best_rankings(uid) rankList: List[Dict[str, Any]] = [] @@ -485,16 +475,16 @@ class CxbBase: "rankx": [1, 1, 1], } - async def handle_action_getadv_request(self, data: Dict) -> Dict: + def handle_action_getadv_request(self, data: Dict) -> Dict: return {"data": [{"r": "1", "i": "100300", "c": "20"}]} - async def handle_action_getmsg_request(self, data: Dict) -> Dict: + def handle_action_getmsg_request(self, data: Dict) -> Dict: return {"msgs": []} - async def handle_auth_logout_request(self, data: Dict) -> Dict: + def handle_auth_logout_request(self, data: Dict) -> Dict: return {"auth": True} - async def handle_action_rankreg_request(self, data: Dict) -> Dict: + def handle_action_rankreg_request(self, data: Dict) -> Dict: uid = data["rankreg"]["uid"] self.logger.info(f"Put {len(data['rankreg']['data'])} rankings for {uid}") @@ -502,7 +492,7 @@ class CxbBase: # REV S2 if "clear" in rid: try: - await self.data.score.put_ranking( + self.data.score.put_ranking( user_id=uid, rev_id=int(rid["rid"]), song_id=int(rid["sc"][1]), @@ -510,7 +500,7 @@ class CxbBase: clear=rid["clear"], ) except Exception: - await self.data.score.put_ranking( + self.data.score.put_ranking( user_id=uid, rev_id=int(rid["rid"]), song_id=0, @@ -520,7 +510,7 @@ class CxbBase: # REV else: try: - await self.data.score.put_ranking( + self.data.score.put_ranking( user_id=uid, rev_id=int(rid["rid"]), song_id=int(rid["sc"][1]), @@ -528,7 +518,7 @@ class CxbBase: clear=0, ) except Exception: - await self.data.score.put_ranking( + self.data.score.put_ranking( user_id=uid, rev_id=int(rid["rid"]), song_id=0, @@ -537,15 +527,15 @@ class CxbBase: ) return {} - async def handle_action_addenergy_request(self, data: Dict) -> Dict: + def handle_action_addenergy_request(self, data: Dict) -> Dict: uid = data["addenergy"]["uid"] self.logger.info(f"Add energy to user {uid}") - profile = await self.data.profile.get_profile_index(0, uid, self.version) + profile = self.data.profile.get_profile_index(0, uid, self.version) data1 = profile["data"] - p = await self.data.item.get_energy(uid) + p = self.data.item.get_energy(uid) if not p: - await self.data.item.put_energy(uid, 5) + self.data.item.put_energy(uid, 5) return { "class": data1["myClass"], @@ -558,7 +548,7 @@ class CxbBase: energy = p["energy"] newenergy = int(energy) + 5 - await self.data.item.put_energy(uid, newenergy) + self.data.item.put_energy(uid, newenergy) if int(energy) <= 995: array.append( @@ -580,10 +570,10 @@ class CxbBase: ) return array[0] - async def handle_action_eventreq_request(self, data: Dict) -> Dict: + def handle_action_eventreq_request(self, data: Dict) -> Dict: self.logger.info(data) return {"eventreq": ""} - async def handle_action_stampreq_request(self, data: Dict) -> Dict: + def handle_action_stampreq_request(self, data: Dict) -> Dict: self.logger.info(data) - return {"stampreq": ""} + return {"stampreq": ""} \ No newline at end of file diff --git a/titles/cxb/config.py b/titles/cxb/config.py index 32e4a42..fa5a6a3 100644 --- a/titles/cxb/config.py +++ b/titles/cxb/config.py @@ -19,12 +19,6 @@ class CxbServerConfig: ) ) - @property - def use_https(self) -> bool: - return CoreConfig.get_config_field( - self.__config, "cxb", "server", "use_https", default=True - ) - class CxbConfig(dict): def __init__(self) -> None: diff --git a/titles/cxb/database.py b/titles/cxb/database.py index f230e4a..081e2bd 100644 --- a/titles/cxb/database.py +++ b/titles/cxb/database.py @@ -1,6 +1,6 @@ -from core.config import CoreConfig from core.data import Data -from titles.cxb.schema import CxbItemData, CxbProfileData, CxbScoreData, CxbStaticData +from core.config import CoreConfig +from titles.cxb.schema import CxbProfileData, CxbScoreData, CxbItemData, CxbStaticData class CxbData(Data): diff --git a/titles/cxb/index.py b/titles/cxb/index.py index 6e676a4..af728c6 100644 --- a/titles/cxb/index.py +++ b/titles/cxb/index.py @@ -1,22 +1,18 @@ -import json -import logging -import re -import sys +from twisted.web.http import Request import traceback -from logging.handlers import TimedRotatingFileHandler -from os import path -from typing import Dict, List, Tuple - -import coloredlogs -import inflection +import sys import yaml -from core.config import CoreConfig -from core.title import BaseServlet, JSONResponseNoASCII -from core.utils import Utils -from starlette.requests import Request -from starlette.responses import JSONResponse, Response -from starlette.routing import Route +import json +import re +import inflection +import logging, coloredlogs +from logging.handlers import TimedRotatingFileHandler +from typing import Dict, Tuple, List +from os import path +from core.config import CoreConfig +from core.title import BaseServlet +from core.utils import Utils from .config import CxbConfig from .const import CxbConstants from .rev import CxbRev @@ -66,18 +62,8 @@ class CxbServlet(BaseServlet): CxbRevSunriseS2(core_cfg, self.game_cfg), ] - def get_routes(self) -> List[Route]: - return [ - Route("/data", self.handle_data, methods=["POST"]), - Route("/action", self.handle_action, methods=["POST"]), - Route("/v2/action", self.handle_action, methods=["POST"]), - Route("/auth", self.handle_auth, methods=["POST"]), - ] - @classmethod - def is_game_enabled( - cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str - ) -> bool: + def is_game_enabled(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> bool: game_cfg = CxbConfig() if path.exists(f"{cfg_dir}/{CxbConstants.CONFIG_NAME}"): game_cfg.update( @@ -86,31 +72,35 @@ class CxbServlet(BaseServlet): 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]: + if not self.core_cfg.server.is_using_proxy and Utils.get_title_port_ssl(self.core_cfg): + return ( + f"https://{self.core_cfg.title.hostname}:{self.core_cfg.title.port_ssl}", + "", + ) - 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 else "http" - - if proto == "https": - t_port = f":{title_port_ssl_int}" if title_port_ssl_int != 443 else "" - - else: - t_port = f":{title_port_int}" if title_port_int != 80 else "" - + return (f"https://{self.core_cfg.title.hostname}", "") + + def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: return ( - f"{proto}://{self.core_cfg.server.hostname}{t_port}", - "", + [], + [ + ("handle_data", "/data", {}), + ("handle_action", "/action", {}), + ("handle_action", "/v2/action", {}), + ("handle_auth", "/auth", {}), + ] ) - async def preprocess(self, req: Request) -> Dict: - req_bytes = await req.body() - + def preprocess(self, req: Request) -> Dict: + try: + req_bytes = req.content.getvalue() + except: + req_bytes = req.content.read() # Can we just use this one? + try: req_json: Dict = json.loads(req_bytes) @@ -125,35 +115,36 @@ class CxbServlet(BaseServlet): f"Error decoding json to /data endpoint: {e} / {f} - {req_bytes}" ) return b"" - + return req_json - async def handle_data(self, request: Request) -> bytes: - req_json = await self.preprocess(request) + def handle_data(self, request: Request, game_code: str, matchers: Dict) -> bytes: + req_json = self.preprocess(request) func_to_find = "handle_data_" version_string = "Base" internal_ver = 0 version = 0 - + if req_json == {}: self.logger.warning(f"Empty json request to /data") - return Response() + return b"" subcmd = list(req_json.keys())[0] if subcmd == "dldate": + if ( not type(req_json["dldate"]) is dict or "filetype" not in req_json["dldate"] ): self.logger.warning(f"Malformed dldate request: {req_json}") - return Response() + return b"" filetype = req_json["dldate"]["filetype"] filetype_split = filetype.split("/") if len(filetype_split) < 2 or not filetype_split[0].isnumeric(): self.logger.warning(f"Malformed dldate request: {req_json}") - return Response() + return b"" version = int(filetype_split[0]) filename = filetype_split[len(filetype_split) - 1] @@ -168,9 +159,9 @@ class CxbServlet(BaseServlet): else: filetype = subcmd func_to_find += filetype - + func_to_find += "_request" - + if version <= 10102: version_string = "Rev" internal_ver = CxbConstants.VER_CROSSBEATS_REV @@ -184,91 +175,83 @@ class CxbServlet(BaseServlet): internal_ver = CxbConstants.VER_CROSSBEATS_REV_SUNRISE_S2 if not hasattr(self.versions[internal_ver], func_to_find): - self.logger.warn( - f"{version_string} has no handler for filetype {filetype} / {func_to_find}" - ) - return JSONResponse({"data": ""}) - + self.logger.warn(f"{version_string} has no handler for filetype {filetype} / {func_to_find}") + return({"data":""}) + self.logger.info(f"{version_string} request for filetype {filetype}") self.logger.debug(req_json) handler = getattr(self.versions[internal_ver], func_to_find) - + try: - resp = await handler(req_json) + resp = handler(req_json) except Exception as e: self.logger.error(f"Error handling request for file {filetype} - {e}") if self.logger.level == logging.DEBUG: - tp, val, tb = sys.exc_info() + tp, val, tb = sys.exc_info() traceback.print_exception(tp, val, tb, limit=1) - with open( - "{0}/{1}.log".format(self.core_cfg.server.log_dir, "cxb"), "a" - ) as f: + with open("{0}/{1}.log".format(self.core_cfg.server.log_dir, "cxb"), "a") as f: traceback.print_exception(tp, val, tb, limit=1, file=f) - return Response() - + return "" + self.logger.debug(f"{version_string} Response {resp}") - return JSONResponseNoASCII(resp) + return json.dumps(resp, ensure_ascii=False).encode("utf-8") - async def handle_action(self, request: Request) -> bytes: - req_json = await self.preprocess(request) + def handle_action(self, request: Request, game_code: str, matchers: Dict) -> bytes: + req_json = self.preprocess(request) subcmd = list(req_json.keys())[0] func_to_find = f"handle_action_{subcmd}_request" - + if not hasattr(self.versions[0], func_to_find): self.logger.warn(f"No handler for action {subcmd} request") - return Response() - + return "" + self.logger.info(f"Action {subcmd} Request") self.logger.debug(req_json) handler = getattr(self.versions[0], func_to_find) - + try: - resp = await handler(req_json) + resp = handler(req_json) except Exception as e: self.logger.error(f"Error handling action {subcmd} request - {e}") if self.logger.level == logging.DEBUG: - tp, val, tb = sys.exc_info() + tp, val, tb = sys.exc_info() traceback.print_exception(tp, val, tb, limit=1) - with open( - "{0}/{1}.log".format(self.core_cfg.server.log_dir, "cxb"), "a" - ) as f: + with open("{0}/{1}.log".format(self.core_cfg.server.log_dir, "cxb"), "a") as f: traceback.print_exception(tp, val, tb, limit=1, file=f) - return Response() - + return "" + self.logger.debug(f"Response {resp}") - return JSONResponseNoASCII(resp) + return json.dumps(resp, ensure_ascii=False).encode("utf-8") - async def handle_auth(self, request: Request) -> bytes: - req_json = await self.preprocess(request) + def handle_auth(self, request: Request, game_code: str, matchers: Dict) -> bytes: + req_json = self.preprocess(request) subcmd = list(req_json.keys())[0] func_to_find = f"handle_auth_{subcmd}_request" - + if not hasattr(self.versions[0], func_to_find): self.logger.warn(f"No handler for auth {subcmd} request") - return Response() - + return "" + self.logger.info(f"Action {subcmd} Request") self.logger.debug(req_json) handler = getattr(self.versions[0], func_to_find) - + try: - resp = await handler(req_json) + resp = handler(req_json) except Exception as e: self.logger.error(f"Error handling auth {subcmd} request - {e}") if self.logger.level == logging.DEBUG: - tp, val, tb = sys.exc_info() + tp, val, tb = sys.exc_info() traceback.print_exception(tp, val, tb, limit=1) - with open( - "{0}/{1}.log".format(self.core_cfg.server.log_dir, "cxb"), "a" - ) as f: + with open("{0}/{1}.log".format(self.core_cfg.server.log_dir, "cxb"), "a") as f: traceback.print_exception(tp, val, tb, limit=1, file=f) - return Response() - + return "" + self.logger.debug(f"Response {resp}") - return JSONResponseNoASCII(resp) + return json.dumps(resp, ensure_ascii=False).encode("utf-8") diff --git a/titles/cxb/read.py b/titles/cxb/read.py index e2a77f4..b71740d 100644 --- a/titles/cxb/read.py +++ b/titles/cxb/read.py @@ -1,11 +1,12 @@ +from typing import Optional, Dict, List +from os import walk, path +import urllib import csv -from os import path -from typing import Optional -from core.config import CoreConfig from read import BaseReader -from titles.cxb.const import CxbConstants +from core.config import CoreConfig from titles.cxb.database import CxbData +from titles.cxb.const import CxbConstants class CxbReader(BaseReader): @@ -28,14 +29,17 @@ class CxbReader(BaseReader): self.logger.error(f"Invalid project cxb version {version}") exit(1) - async def read(self) -> None: - if path.exists(self.bin_dir): - await self.read_csv(self.bin_dir) + def read(self) -> None: + pull_bin_ram = True - else: - self.logger.warn(f"{self.bin_dir} does not exist, nothing to import") + 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 - async def read_csv(self, bin_dir: str) -> None: + 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}") try: @@ -51,7 +55,7 @@ class CxbReader(BaseReader): if not "N/A" in row["standard"]: self.logger.info(f"Added song {song_id} chart 0") - await self.data.static.put_music( + self.data.static.put_music( self.version, song_id, index, @@ -67,7 +71,7 @@ class CxbReader(BaseReader): ) if not "N/A" in row["hard"]: self.logger.info(f"Added song {song_id} chart 1") - await self.data.static.put_music( + self.data.static.put_music( self.version, song_id, index, @@ -79,7 +83,7 @@ class CxbReader(BaseReader): ) if not "N/A" in row["master"]: self.logger.info(f"Added song {song_id} chart 2") - await self.data.static.put_music( + self.data.static.put_music( self.version, song_id, index, @@ -93,7 +97,7 @@ class CxbReader(BaseReader): ) if not "N/A" in row["unlimited"]: self.logger.info(f"Added song {song_id} chart 3") - await self.data.static.put_music( + self.data.static.put_music( self.version, song_id, index, @@ -109,7 +113,7 @@ class CxbReader(BaseReader): ) if not "N/A" in row["easy"]: self.logger.info(f"Added song {song_id} chart 4") - await self.data.static.put_music( + self.data.static.put_music( self.version, song_id, index, diff --git a/titles/cxb/rev.py b/titles/cxb/rev.py index ef51fee..e311a1e 100644 --- a/titles/cxb/rev.py +++ b/titles/cxb/rev.py @@ -1,12 +1,14 @@ import json from decimal import Decimal -from typing import Dict +from base64 import b64encode +from typing import Any, Dict +from hashlib import md5 +from datetime import datetime from core.config import CoreConfig -from core.data import cached - -from .base import CxbBase +from core.data import Data, cached from .config import CxbConfig +from .base import CxbBase from .const import CxbConstants @@ -15,15 +17,15 @@ class CxbRev(CxbBase): super().__init__(cfg, game_cfg) self.version = CxbConstants.VER_CROSSBEATS_REV - async def handle_data_path_list_request(self, data: Dict) -> Dict: + def handle_data_path_list_request(self, data: Dict) -> Dict: return {"data": ""} - async def handle_data_putlog_request(self, data: Dict) -> Dict: + def handle_data_putlog_request(self, data: Dict) -> Dict: if data["putlog"]["type"] == "ResultLog": score_data = json.loads(data["putlog"]["data"]) userid = score_data["usid"] - await self.data.score.put_playlog( + self.data.score.put_playlog( userid, score_data["mcode"], score_data["difficulty"], @@ -43,9 +45,9 @@ class CxbRev(CxbBase): return {"data": True} @cached(lifetime=86400) - async def handle_data_music_list_request(self, data: Dict) -> Dict: + def handle_data_music_list_request(self, data: Dict) -> Dict: ret_str = "" - with open(r"titles/cxb/data/rev/MusicArchiveList.csv") as music: + with open(r"titles/cxb/data/rss/MusicArchiveList.csv") as music: lines = music.readlines() for line in lines: line_split = line.split(",") @@ -54,10 +56,10 @@ class CxbRev(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - async def handle_data_item_list_icon_request(self, data: Dict) -> Dict: + def handle_data_item_list_icon_request(self, data: Dict) -> Dict: ret_str = "\r\n#ItemListIcon\r\n" with open( - r"titles/cxb/data/rev/Item/ItemArchiveList_Icon.csv", encoding="shift-jis" + r"titles/cxb/data/rss/Item/ItemArchiveList_Icon.csv", encoding="utf-8" ) as item: lines = item.readlines() for line in lines: @@ -65,10 +67,10 @@ class CxbRev(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - async def handle_data_item_list_skin_notes_request(self, data: Dict) -> Dict: + def handle_data_item_list_skin_notes_request(self, data: Dict) -> Dict: ret_str = "\r\n#ItemListSkinNotes\r\n" with open( - r"titles/cxb/data/rev/Item/ItemArchiveList_SkinNotes.csv", encoding="utf-8" + r"titles/cxb/data/rss/Item/ItemArchiveList_SkinNotes.csv", encoding="utf-8" ) as item: lines = item.readlines() for line in lines: @@ -76,10 +78,10 @@ class CxbRev(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - async def handle_data_item_list_skin_effect_request(self, data: Dict) -> Dict: + def handle_data_item_list_skin_effect_request(self, data: Dict) -> Dict: ret_str = "\r\n#ItemListSkinEffect\r\n" with open( - r"titles/cxb/data/rev/Item/ItemArchiveList_SkinEffect.csv", encoding="utf-8" + r"titles/cxb/data/rss/Item/ItemArchiveList_SkinEffect.csv", encoding="utf-8" ) as item: lines = item.readlines() for line in lines: @@ -87,10 +89,10 @@ class CxbRev(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - async def handle_data_item_list_skin_bg_request(self, data: Dict) -> Dict: + def handle_data_item_list_skin_bg_request(self, data: Dict) -> Dict: ret_str = "\r\n#ItemListSkinBg\r\n" with open( - r"titles/cxb/data/rev/Item/ItemArchiveList_SkinBg.csv", encoding="utf-8" + r"titles/cxb/data/rss/Item/ItemArchiveList_SkinBg.csv", encoding="utf-8" ) as item: lines = item.readlines() for line in lines: @@ -98,10 +100,10 @@ class CxbRev(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - async def handle_data_item_list_title_request(self, data: Dict) -> Dict: + def handle_data_item_list_title_request(self, data: Dict) -> Dict: ret_str = "\r\n#ItemListTitle\r\n" with open( - r"titles/cxb/data/rev/Item/ItemList_Title.csv", encoding="shift-jis" + r"titles/cxb/data/rss/Item/ItemList_Title.csv", encoding="shift-jis" ) as item: lines = item.readlines() for line in lines: @@ -109,10 +111,10 @@ class CxbRev(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - async def handle_data_shop_list_music_request(self, data: Dict) -> Dict: + def handle_data_shop_list_music_request(self, data: Dict) -> Dict: ret_str = "\r\n#ShopListMusic\r\n" with open( - r"titles/cxb/data/rev/Shop/ShopList_Music.csv", encoding="shift-jis" + r"titles/cxb/data/rss/Shop/ShopList_Music.csv", encoding="shift-jis" ) as shop: lines = shop.readlines() for line in lines: @@ -120,10 +122,10 @@ class CxbRev(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - async def handle_data_shop_list_icon_request(self, data: Dict) -> Dict: + def handle_data_shop_list_icon_request(self, data: Dict) -> Dict: ret_str = "\r\n#ShopListIcon\r\n" with open( - r"titles/cxb/data/rev/Shop/ShopList_Icon.csv", encoding="shift-jis" + r"titles/cxb/data/rss/Shop/ShopList_Icon.csv", encoding="shift-jis" ) as shop: lines = shop.readlines() for line in lines: @@ -131,90 +133,83 @@ class CxbRev(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - async def handle_data_shop_list_title_request(self, data: Dict) -> Dict: + def handle_data_shop_list_title_request(self, data: Dict) -> Dict: ret_str = "\r\n#ShopListTitle\r\n" with open( - r"titles/cxb/data/rev/Shop/ShopList_Title.csv", encoding="shift-jis" + r"titles/cxb/data/rss/Shop/ShopList_Title.csv", encoding="shift-jis" ) as shop: lines = shop.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - async def handle_data_shop_list_skin_hud_request(self, data: Dict) -> Dict: + def handle_data_shop_list_skin_hud_request(self, data: Dict) -> Dict: return {"data": ""} - async def handle_data_shop_list_skin_arrow_request(self, data: Dict) -> Dict: + def handle_data_shop_list_skin_arrow_request(self, data: Dict) -> Dict: return {"data": ""} - async def handle_data_shop_list_skin_hit_request(self, data: Dict) -> Dict: + def handle_data_shop_list_skin_hit_request(self, data: Dict) -> Dict: return {"data": ""} @cached(lifetime=86400) - async def handle_data_shop_list_sale_request(self, data: Dict) -> Dict: + def handle_data_shop_list_sale_request(self, data: Dict) -> Dict: ret_str = "\r\n#ShopListSale\r\n" with open( - r"titles/cxb/data/rev/Shop/ShopList_Sale.csv", encoding="shift-jis" + r"titles/cxb/data/rss/Shop/ShopList_Sale.csv", encoding="shift-jis" ) as shop: lines = shop.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - async def handle_data_extra_stage_list_request(self, data: Dict) -> Dict: - ret_str = "" - with open( - r"titles/cxb/data/rev/ExtraStageList.csv", encoding="shift-jis" - ) as stage: - lines = stage.readlines() - for line in lines: - ret_str += f"{line[:-1]}\r\n" - return {"data": ret_str} + def handle_data_extra_stage_list_request(self, data: Dict) -> Dict: + return {"data": ""} @cached(lifetime=86400) - async def handle_data_exxxxx_request(self, data: Dict) -> Dict: + def handle_data_exxxxx_request(self, data: Dict) -> Dict: extra_num = int(data["dldate"]["filetype"][-4:]) ret_str = "" with open( - rf"titles/cxb/data/rev/Ex000{extra_num}.csv", encoding="shift-jis" + rf"titles/cxb/data/rss/Ex000{extra_num}.csv", encoding="shift-jis" ) as stage: lines = stage.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - async def handle_data_bonus_list10100_request(self, data: Dict) -> Dict: + def handle_data_bonus_list10100_request(self, data: Dict) -> Dict: return {"data": ""} - async def handle_data_free_coupon_request(self, data: Dict) -> Dict: + def handle_data_free_coupon_request(self, data: Dict) -> Dict: return {"data": ""} @cached(lifetime=86400) - async def handle_data_news_list_request(self, data: Dict) -> Dict: + def handle_data_news_list_request(self, data: Dict) -> Dict: ret_str = "" - with open(r"titles/cxb/data/rev/NewsList.csv", encoding="UTF-8") as news: + with open(r"titles/cxb/data/rss/NewsList.csv", encoding="UTF-8") as news: lines = news.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - async def handle_data_tips_request(self, data: Dict) -> Dict: + def handle_data_tips_request(self, data: Dict) -> Dict: return {"data": ""} @cached(lifetime=86400) - async def handle_data_license_request(self, data: Dict) -> Dict: + def handle_data_license_request(self, data: Dict) -> Dict: ret_str = "" - with open(r"titles/cxb/data/rev/License_Offline.csv", encoding="UTF-8") as lic: + with open(r"titles/cxb/data/rss/License_Offline.csv", encoding="UTF-8") as lic: lines = lic.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} @cached(lifetime=86400) - async def handle_data_course_list_request(self, data: Dict) -> Dict: + def handle_data_course_list_request(self, data: Dict) -> Dict: ret_str = "" with open( - r"titles/cxb/data/rev/Course/CourseList.csv", encoding="UTF-8" + r"titles/cxb/data/rss/Course/CourseList.csv", encoding="UTF-8" ) as course: lines = course.readlines() for line in lines: @@ -222,12 +217,12 @@ class CxbRev(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - async def handle_data_csxxxx_request(self, data: Dict) -> Dict: + def handle_data_csxxxx_request(self, data: Dict) -> Dict: # Removed the CSVs since the format isnt quite right extra_num = int(data["dldate"]["filetype"][-4:]) ret_str = "" with open( - rf"titles/cxb/data/rev/Course/Cs000{extra_num}.csv", encoding="shift-jis" + rf"titles/cxb/data/rss/Course/Cs000{extra_num}.csv", encoding="shift-jis" ) as course: lines = course.readlines() for line in lines: @@ -235,81 +230,77 @@ class CxbRev(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - async def handle_data_mission_list_request(self, data: Dict) -> Dict: + def handle_data_mission_list_request(self, data: Dict) -> Dict: ret_str = "" with open( - r"titles/cxb/data/rev/MissionList.csv", encoding="shift-jis" + r"titles/cxb/data/rss/MissionList.csv", encoding="shift-jis" ) as mission: lines = mission.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - async def handle_data_mission_bonus_request(self, data: Dict) -> Dict: + def handle_data_mission_bonus_request(self, data: Dict) -> Dict: return {"data": ""} - async def handle_data_unlimited_mission_request(self, data: Dict) -> Dict: + def handle_data_unlimited_mission_request(self, data: Dict) -> Dict: return {"data": ""} @cached(lifetime=86400) - async def handle_data_event_list_request(self, data: Dict) -> Dict: + def handle_data_event_list_request(self, data: Dict) -> Dict: ret_str = "" with open( - r"titles/cxb/data/rev/Event/EventArchiveList.csv", encoding="shift-jis" + r"titles/cxb/data/rss/Event/EventArchiveList.csv", encoding="shift-jis" ) as mission: lines = mission.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - async def handle_data_event_music_list_request(self, data: Dict) -> Dict: + def handle_data_event_music_list_request(self, data: Dict) -> Dict: return {"data": ""} - async def handle_data_event_mission_list_request(self, data: Dict) -> Dict: + def handle_data_event_mission_list_request(self, data: Dict) -> Dict: return {"data": ""} - async def handle_data_event_achievement_single_high_score_list_request( + def handle_data_event_achievement_single_high_score_list_request( self, data: Dict ) -> Dict: return {"data": ""} - async def handle_data_event_achievement_single_accumulation_request( + def handle_data_event_achievement_single_accumulation_request( self, data: Dict ) -> Dict: return {"data": ""} - async def handle_data_event_ranking_high_score_list_request( - self, data: Dict - ) -> Dict: + def handle_data_event_ranking_high_score_list_request(self, data: Dict) -> Dict: return {"data": ""} - async def handle_data_event_ranking_accumulation_list_request( - self, data: Dict - ) -> Dict: + def handle_data_event_ranking_accumulation_list_request(self, data: Dict) -> Dict: return {"data": ""} - async def handle_data_event_ranking_stamp_list_request(self, data: Dict) -> Dict: + def handle_data_event_ranking_stamp_list_request(self, data: Dict) -> Dict: return {"data": ""} - async def handle_data_event_ranking_store_list_request(self, data: Dict) -> Dict: + def handle_data_event_ranking_store_list_request(self, data: Dict) -> Dict: return {"data": ""} - async def handle_data_event_ranking_area_list_request(self, data: Dict) -> Dict: + def handle_data_event_ranking_area_list_request(self, data: Dict) -> Dict: return {"data": ""} @cached(lifetime=86400) - async def handle_data_event_stamp_list_request(self, data: Dict) -> Dict: + def handle_data_event_stamp_list_request(self, data: Dict) -> Dict: ret_str = "" with open( - r"titles/cxb/data/rev/Event/EventStampList.csv", encoding="shift-jis" + r"titles/cxb/data/rss/Event/EventStampList.csv", encoding="shift-jis" ) as event: lines = event.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - async def handle_data_event_stamp_map_list_csxxxx_request(self, data: Dict) -> Dict: + def handle_data_event_stamp_map_list_csxxxx_request(self, data: Dict) -> Dict: return {"data": "1,2,1,1,2,3,9,5,6,7,8,9,10,\r\n"} - async def handle_data_server_state_request(self, data: Dict) -> Dict: + def handle_data_server_state_request(self, data: Dict) -> Dict: return {"data": True} diff --git a/titles/cxb/rss1.py b/titles/cxb/rss1.py index 59a5adc..fe43e42 100644 --- a/titles/cxb/rss1.py +++ b/titles/cxb/rss1.py @@ -1,10 +1,14 @@ -from typing import Dict +import json +from decimal import Decimal +from base64 import b64encode +from typing import Any, Dict +from hashlib import md5 +from datetime import datetime from core.config import CoreConfig -from core.data import cached - -from .base import CxbBase +from core.data import Data, cached from .config import CxbConfig +from .base import CxbBase from .const import CxbConstants @@ -13,11 +17,11 @@ class CxbRevSunriseS1(CxbBase): super().__init__(cfg, game_cfg) self.version = CxbConstants.VER_CROSSBEATS_REV_SUNRISE_S1 - async def handle_data_path_list_request(self, data: Dict) -> Dict: + def handle_data_path_list_request(self, data: Dict) -> Dict: return {"data": ""} @cached(lifetime=86400) - async def handle_data_music_list_request(self, data: Dict) -> Dict: + def handle_data_music_list_request(self, data: Dict) -> Dict: ret_str = "" with open(r"titles/cxb/data/rss1/MusicArchiveList.csv") as music: lines = music.readlines() @@ -28,7 +32,7 @@ class CxbRevSunriseS1(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - async def handle_data_item_list_detail_request(self, data: Dict) -> Dict: + def handle_data_item_list_detail_request(self, data: Dict) -> Dict: # ItemListIcon load ret_str = "#ItemListIcon\r\n" with open( @@ -50,7 +54,7 @@ class CxbRevSunriseS1(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - async def handle_data_shop_list_detail_request(self, data: Dict) -> Dict: + def handle_data_shop_list_detail_request(self, data: Dict) -> Dict: # ShopListIcon load ret_str = "#ShopListIcon\r\n" with open( @@ -69,6 +73,42 @@ class CxbRevSunriseS1(CxbBase): for line in lines: ret_str += f"{line[:-1]}\r\n" + # ShopListSale load + ret_str += "\r\n#ShopListSale\r\n" + with open( + r"titles/cxb/data/rss1/Shop/ShopList_Sale.csv", encoding="shift-jis" + ) as shop: + lines = shop.readlines() + for line in lines: + ret_str += f"{line[:-1]}\r\n" + + # ShopListSkinBg load + ret_str += "\r\n#ShopListSkinBg\r\n" + with open( + r"titles/cxb/data/rss1/Shop/ShopList_SkinBg.csv", encoding="shift-jis" + ) as shop: + lines = shop.readlines() + for line in lines: + ret_str += f"{line[:-1]}\r\n" + + # ShopListSkinEffect load + ret_str += "\r\n#ShopListSkinEffect\r\n" + with open( + r"titles/cxb/data/rss1/Shop/ShopList_SkinEffect.csv", encoding="shift-jis" + ) as shop: + lines = shop.readlines() + for line in lines: + ret_str += f"{line[:-1]}\r\n" + + # ShopListSkinNotes load + ret_str += "\r\n#ShopListSkinNotes\r\n" + with open( + r"titles/cxb/data/rss1/Shop/ShopList_SkinNotes.csv", encoding="shift-jis" + ) as shop: + lines = shop.readlines() + for line in lines: + ret_str += f"{line[:-1]}\r\n" + # ShopListTitle load ret_str += "\r\n#ShopListTitle\r\n" with open( @@ -79,26 +119,26 @@ class CxbRevSunriseS1(CxbBase): ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - async def handle_data_extra_stage_list_request(self, data: Dict) -> Dict: + def handle_data_extra_stage_list_request(self, data: Dict) -> Dict: return {"data": ""} - async def handle_data_exxxxx_request(self, data: Dict) -> Dict: + def handle_data_exxxxx_request(self, data: Dict) -> Dict: return {"data": ""} - async def handle_data_one_more_extra_list_request(self, data: Dict) -> Dict: + def handle_data_one_more_extra_list_request(self, data: Dict) -> Dict: return {"data": ""} - async def handle_data_bonus_list10100_request(self, data: Dict) -> Dict: + def handle_data_bonus_list10100_request(self, data: Dict) -> Dict: return {"data": ""} - async def handle_data_oexxxx_request(self, data: Dict) -> Dict: + def handle_data_oexxxx_request(self, data: Dict) -> Dict: return {"data": ""} - async def handle_data_free_coupon_request(self, data: Dict) -> Dict: + def handle_data_free_coupon_request(self, data: Dict) -> Dict: return {"data": ""} @cached(lifetime=86400) - async def handle_data_news_list_request(self, data: Dict) -> Dict: + def handle_data_news_list_request(self, data: Dict) -> Dict: ret_str = "" with open(r"titles/cxb/data/rss1/NewsList.csv", encoding="UTF-8") as news: lines = news.readlines() @@ -106,14 +146,14 @@ class CxbRevSunriseS1(CxbBase): ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - async def handle_data_tips_request(self, data: Dict) -> Dict: + def handle_data_tips_request(self, data: Dict) -> Dict: return {"data": ""} - async def handle_data_release_info_list_request(self, data: Dict) -> Dict: + def handle_data_release_info_list_request(self, data: Dict) -> Dict: return {"data": ""} @cached(lifetime=86400) - async def handle_data_random_music_list_request(self, data: Dict) -> Dict: + def handle_data_random_music_list_request(self, data: Dict) -> Dict: ret_str = "" with open(r"titles/cxb/data/rss1/MusicArchiveList.csv") as music: lines = music.readlines() @@ -127,7 +167,7 @@ class CxbRevSunriseS1(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - async def handle_data_license_request(self, data: Dict) -> Dict: + def handle_data_license_request(self, data: Dict) -> Dict: ret_str = "" with open(r"titles/cxb/data/rss1/License.csv", encoding="UTF-8") as licenses: lines = licenses.readlines() @@ -136,7 +176,7 @@ class CxbRevSunriseS1(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - async def handle_data_course_list_request(self, data: Dict) -> Dict: + def handle_data_course_list_request(self, data: Dict) -> Dict: ret_str = "" with open( r"titles/cxb/data/rss1/Course/CourseList.csv", encoding="UTF-8" @@ -147,7 +187,7 @@ class CxbRevSunriseS1(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - async def handle_data_csxxxx_request(self, data: Dict) -> Dict: + def handle_data_csxxxx_request(self, data: Dict) -> Dict: extra_num = int(data["dldate"]["filetype"][-4:]) ret_str = "" with open( @@ -158,16 +198,16 @@ class CxbRevSunriseS1(CxbBase): ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - async def handle_data_mission_list_request(self, data: Dict) -> Dict: + def handle_data_mission_list_request(self, data: Dict) -> Dict: return {"data": ""} - async def handle_data_mission_bonus_request(self, data: Dict) -> Dict: + def handle_data_mission_bonus_request(self, data: Dict) -> Dict: return {"data": ""} - async def handle_data_unlimited_mission_request(self, data: Dict) -> Dict: + def handle_data_unlimited_mission_request(self, data: Dict) -> Dict: return {"data": ""} - async def handle_data_partner_list_request(self, data: Dict) -> Dict: + def handle_data_partner_list_request(self, data: Dict) -> Dict: ret_str = "" # Lord forgive me for the sins I am about to commit for i in range(0, 10): @@ -186,7 +226,7 @@ class CxbRevSunriseS1(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - async def handle_data_partnerxxxx_request(self, data: Dict) -> Dict: + def handle_data_partnerxxxx_request(self, data: Dict) -> Dict: partner_num = int(data["dldate"]["filetype"][-4:]) ret_str = f"{partner_num},,{partner_num},1,10000,\r\n" with open(r"titles/cxb/data/rss1/Partner0000.csv") as partner: @@ -195,13 +235,13 @@ class CxbRevSunriseS1(CxbBase): ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - async def handle_data_server_state_request(self, data: Dict) -> Dict: + def handle_data_server_state_request(self, data: Dict) -> Dict: return {"data": True} - async def handle_data_settings_request(self, data: Dict) -> Dict: + def handle_data_settings_request(self, data: Dict) -> Dict: return {"data": "2,\r\n"} - async def handle_data_story_list_request(self, data: Dict) -> Dict: + def handle_data_story_list_request(self, data: Dict) -> Dict: # story id, story name, game version, start time, end time, course arc, unlock flag, song mcode for menu ret_str = "\r\n" ret_str += ( @@ -213,23 +253,23 @@ class CxbRevSunriseS1(CxbBase): ret_str += f"st0002,REMNANT,10104,1502127790,4096483201,Cs1000,-1,overcl,\r\n" return {"data": ret_str} - async def handle_data_stxxxx_request(self, data: Dict) -> Dict: + def handle_data_stxxxx_request(self, data: Dict) -> Dict: story_num = int(data["dldate"]["filetype"][-4:]) ret_str = "" for i in range(1, 11): ret_str += f"{i},st000{story_num}_{i-1},,,,,,,,,,,,,,,,1,,-1,1,\r\n" return {"data": ret_str} - async def handle_data_event_stamp_list_request(self, data: Dict) -> Dict: + def handle_data_event_stamp_list_request(self, data: Dict) -> Dict: return {"data": "Cs1032,1,1,1,1,1,1,1,1,1,1,\r\n"} - async def handle_data_premium_list_request(self, data: Dict) -> Dict: + def handle_data_premium_list_request(self, data: Dict) -> Dict: return {"data": "1,,,,10,,,,,99,,,,,,,,,100,,\r\n"} - async def handle_data_event_list_request(self, data: Dict) -> Dict: + def handle_data_event_list_request(self, data: Dict) -> Dict: return {"data": ""} - async def handle_data_event_detail_list_request(self, data: Dict) -> Dict: + def handle_data_event_detail_list_request(self, data: Dict) -> Dict: event_id = data["dldate"]["filetype"].split("/")[2] if "EventStampMapListCs1002" in event_id: return {"data": "1,2,1,1,2,3,9,5,6,7,8,9,10,\r\n"} @@ -238,7 +278,7 @@ class CxbRevSunriseS1(CxbBase): else: return {"data": ""} - async def handle_data_event_stamp_map_list_csxxxx_request(self, data: Dict) -> Dict: + def handle_data_event_stamp_map_list_csxxxx_request(self, data: Dict) -> Dict: event_id = data["dldate"]["filetype"].split("/")[2] if "EventStampMapListCs1002" in event_id: return {"data": "1,2,1,1,2,3,9,5,6,7,8,9,10,\r\n"} diff --git a/titles/cxb/rss2.py b/titles/cxb/rss2.py index d638131..b15deda 100644 --- a/titles/cxb/rss2.py +++ b/titles/cxb/rss2.py @@ -1,10 +1,14 @@ -from typing import Dict +import json +from decimal import Decimal +from base64 import b64encode +from typing import Any, Dict +from hashlib import md5 +from datetime import datetime from core.config import CoreConfig -from core.data import cached - -from .base import CxbBase +from core.data import Data, cached from .config import CxbConfig +from .base import CxbBase from .const import CxbConstants @@ -13,11 +17,11 @@ class CxbRevSunriseS2(CxbBase): super().__init__(cfg, game_cfg) self.version = CxbConstants.VER_CROSSBEATS_REV_SUNRISE_S2_OMNI - async def handle_data_path_list_request(self, data: Dict) -> Dict: + def handle_data_path_list_request(self, data: Dict) -> Dict: return {"data": ""} @cached(lifetime=86400) - async def handle_data_music_list_request(self, data: Dict) -> Dict: + def handle_data_music_list_request(self, data: Dict) -> Dict: version = data["dldate"]["filetype"].split("/")[0] ret_str = "" @@ -27,7 +31,7 @@ class CxbRevSunriseS2(CxbBase): else: self.logger.warning("Game Version is Season 2 Omnimix") file = "titles/cxb/data/rss2/MusicArchiveList.csv" - + with open(rf"{file}") as music: lines = music.readlines() for line in lines: @@ -37,7 +41,7 @@ class CxbRevSunriseS2(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - async def handle_data_item_list_detail_request(self, data: Dict) -> Dict: + def handle_data_item_list_detail_request(self, data: Dict) -> Dict: # ItemListIcon load ret_str = "#ItemListIcon\r\n" with open( @@ -59,7 +63,7 @@ class CxbRevSunriseS2(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - async def handle_data_shop_list_detail_request(self, data: Dict) -> Dict: + def handle_data_shop_list_detail_request(self, data: Dict) -> Dict: # ShopListIcon load ret_str = "#ShopListIcon\r\n" with open( @@ -124,36 +128,36 @@ class CxbRevSunriseS2(CxbBase): ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - async def handle_data_extra_stage_list_request(self, data: Dict) -> Dict: - ret_str = "" + def handle_data_extra_stage_list_request(self, data: Dict) -> Dict: + ret_str="" with open(r"titles/cxb/data/rss2/ExtraStageList.csv") as extra: lines = extra.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" - return {"data": ret_str} + return({"data":ret_str}) - async def handle_data_exxxxx_request(self, data: Dict) -> Dict: + def handle_data_exxxxx_request(self, data: Dict) -> Dict: return {"data": ""} - async def handle_data_one_more_extra_list_request(self, data: Dict) -> Dict: + def handle_data_one_more_extra_list_request(self, data: Dict) -> Dict: return {"data": ""} - async def handle_data_bonus_list10100_request(self, data: Dict) -> Dict: + def handle_data_bonus_list10100_request(self, data: Dict) -> Dict: return {"data": ""} - async def handle_data_oexxxx_request(self, data: Dict) -> Dict: + def handle_data_oexxxx_request(self, data: Dict) -> Dict: return {"data": ""} - async def handle_data_free_coupon_request(self, data: Dict) -> Dict: - ret_str = "" + def handle_data_free_coupon_request(self, data: Dict) -> Dict: + ret_str="" with open(r"titles/cxb/data/rss2/FreeCoupon.csv") as coupon: lines = coupon.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" - return {"data": ret_str} + return({"data":ret_str}) @cached(lifetime=86400) - async def handle_data_news_list_request(self, data: Dict) -> Dict: + def handle_data_news_list_request(self, data: Dict) -> Dict: ret_str = "" with open(r"titles/cxb/data/rss2/NewsList.csv", encoding="UTF-8") as news: lines = news.readlines() @@ -161,14 +165,14 @@ class CxbRevSunriseS2(CxbBase): ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - async def handle_data_tips_request(self, data: Dict) -> Dict: + def handle_data_tips_request(self, data: Dict) -> Dict: return {"data": ""} - async def handle_data_release_info_list_request(self, data: Dict) -> Dict: + def handle_data_release_info_list_request(self, data: Dict) -> Dict: return {"data": ""} @cached(lifetime=86400) - async def handle_data_random_music_list_request(self, data: Dict) -> Dict: + def handle_data_random_music_list_request(self, data: Dict) -> Dict: ret_str = "" with open(r"titles/cxb/data/rss2/MusicArchiveList.csv") as music: lines = music.readlines() @@ -182,7 +186,7 @@ class CxbRevSunriseS2(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - async def handle_data_license_request(self, data: Dict) -> Dict: + def handle_data_license_request(self, data: Dict) -> Dict: ret_str = "" with open(r"titles/cxb/data/rss2/License.csv", encoding="UTF-8") as licenses: lines = licenses.readlines() @@ -191,7 +195,7 @@ class CxbRevSunriseS2(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - async def handle_data_course_list_request(self, data: Dict) -> Dict: + def handle_data_course_list_request(self, data: Dict) -> Dict: ret_str = "" with open( r"titles/cxb/data/rss2/Course/CourseList.csv", encoding="UTF-8" @@ -202,7 +206,7 @@ class CxbRevSunriseS2(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - async def handle_data_csxxxx_request(self, data: Dict) -> Dict: + def handle_data_csxxxx_request(self, data: Dict) -> Dict: extra_num = int(data["dldate"]["filetype"][-4:]) ret_str = "" with open( @@ -213,16 +217,16 @@ class CxbRevSunriseS2(CxbBase): ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - async def handle_data_mission_list_request(self, data: Dict) -> Dict: + def handle_data_mission_list_request(self, data: Dict) -> Dict: return {"data": ""} - async def handle_data_mission_bonus_request(self, data: Dict) -> Dict: + def handle_data_mission_bonus_request(self, data: Dict) -> Dict: return {"data": ""} - async def handle_data_unlimited_mission_request(self, data: Dict) -> Dict: + def handle_data_unlimited_mission_request(self, data: Dict) -> Dict: return {"data": ""} - async def handle_data_partner_list_request(self, data: Dict) -> Dict: + def handle_data_partner_list_request(self, data: Dict) -> Dict: ret_str = "" # Lord forgive me for the sins I am about to commit for i in range(0, 10): @@ -241,7 +245,7 @@ class CxbRevSunriseS2(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - async def handle_data_partnerxxxx_request(self, data: Dict) -> Dict: + def handle_data_partnerxxxx_request(self, data: Dict) -> Dict: partner_num = int(data["dldate"]["filetype"][-4:]) ret_str = f"{partner_num},,{partner_num},1,10000,\r\n" with open(r"titles/cxb/data/rss2/Partner0000.csv") as partner: @@ -250,13 +254,13 @@ class CxbRevSunriseS2(CxbBase): ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - async def handle_data_server_state_request(self, data: Dict) -> Dict: + def handle_data_server_state_request(self, data: Dict) -> Dict: return {"data": True} - async def handle_data_settings_request(self, data: Dict) -> Dict: + def handle_data_settings_request(self, data: Dict) -> Dict: return {"data": "2,\r\n"} - async def handle_data_story_list_request(self, data: Dict) -> Dict: + def handle_data_story_list_request(self, data: Dict) -> Dict: # story id, story name, game version, start time, end time, course arc, unlock flag, song mcode for menu ret_str = "\r\n" ret_str += ( @@ -268,7 +272,7 @@ class CxbRevSunriseS2(CxbBase): ret_str += f"st0002,REMNANT,10104,1502127790,4096483201,Cs1000,-1,overcl,\r\n" return {"data": ret_str} - async def handle_data_stxxxx_request(self, data: Dict) -> Dict: + def handle_data_stxxxx_request(self, data: Dict) -> Dict: story_num = int(data["dldate"]["filetype"][-4:]) ret_str = "" # Each stories appears to have 10 pieces based on the wiki but as on how they are set.... no clue @@ -276,18 +280,18 @@ class CxbRevSunriseS2(CxbBase): ret_str += f"{i},st000{story_num}_{i-1},,,,,,,,,,,,,,,,1,,-1,1,\r\n" return {"data": ret_str} - async def handle_data_event_stamp_list_request(self, data: Dict) -> Dict: + def handle_data_event_stamp_list_request(self, data: Dict) -> Dict: return {"data": "Cs1002,1,1,1,1,1,1,1,1,1,1,\r\n"} - async def handle_data_premium_list_request(self, data: Dict) -> Dict: + def handle_data_premium_list_request(self, data: Dict) -> Dict: return {"data": "1,,,,10,,,,,99,,,,,,,,,100,,\r\n"} - async def handle_data_event_list_request(self, data: Dict) -> Dict: + def handle_data_event_list_request(self, data: Dict) -> Dict: return { "data": "Cs4001,0,10000,1601510400,1604188799,1,nv2006,1,\r\nCs4005,0,10000,1609459200,1615766399,1,nv2006,1,\r\n" } - async def handle_data_event_detail_list_request(self, data: Dict) -> Dict: + def handle_data_event_detail_list_request(self, data: Dict) -> Dict: event_id = data["dldate"]["filetype"].split("/")[2] if "Cs4001" in event_id: return { @@ -304,7 +308,7 @@ class CxbRevSunriseS2(CxbBase): else: return {"data": ""} - async def handle_data_event_stamp_map_list_csxxxx_request(self, data: Dict) -> Dict: + def handle_data_event_stamp_map_list_csxxxx_request(self, data: Dict) -> Dict: event_id = data["dldate"]["filetype"].split("/")[2] if "EventStampMapListCs1002" in event_id: return {"data": "1,2,1,1,2,3,9,5,6,7,8,9,10,\r\n"} diff --git a/titles/cxb/schema/__init__.py b/titles/cxb/schema/__init__.py index 7cba521..ce70412 100644 --- a/titles/cxb/schema/__init__.py +++ b/titles/cxb/schema/__init__.py @@ -1,6 +1,6 @@ -from titles.cxb.schema.item import CxbItemData from titles.cxb.schema.profile import CxbProfileData from titles.cxb.schema.score import CxbScoreData +from titles.cxb.schema.item import CxbItemData from titles.cxb.schema.static import CxbStaticData __all__ = [CxbProfileData, CxbScoreData, CxbItemData, CxbStaticData] diff --git a/titles/cxb/schema/item.py b/titles/cxb/schema/item.py index e707318..022a036 100644 --- a/titles/cxb/schema/item.py +++ b/titles/cxb/schema/item.py @@ -1,10 +1,11 @@ -from typing import Dict, Optional +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean +from sqlalchemy.schema import ForeignKey +from sqlalchemy.sql import func +from sqlalchemy.dialects.mysql import insert from core.data.schema import BaseData, metadata -from sqlalchemy import Column, Table, UniqueConstraint, and_ -from sqlalchemy.dialects.mysql import insert -from sqlalchemy.schema import ForeignKey -from sqlalchemy.types import Integer energy = Table( "cxb_rev_energy", @@ -18,12 +19,12 @@ energy = Table( class CxbItemData(BaseData): - async def put_energy(self, user_id: int, rev_energy: int) -> Optional[int]: + def put_energy(self, user_id: int, rev_energy: int) -> Optional[int]: sql = insert(energy).values(user=user_id, energy=rev_energy) conflict = sql.on_duplicate_key_update(energy=rev_energy) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.error( f"{__name__} failed to insert item! user: {user_id}, energy: {rev_energy}" @@ -32,10 +33,10 @@ class CxbItemData(BaseData): return result.lastrowid - async def get_energy(self, user_id: int) -> Optional[Dict]: + def get_energy(self, user_id: int) -> Optional[Dict]: sql = energy.select(and_(energy.c.user == user_id)) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() diff --git a/titles/cxb/schema/profile.py b/titles/cxb/schema/profile.py index 8069a66..5c62f76 100644 --- a/titles/cxb/schema/profile.py +++ b/titles/cxb/schema/profile.py @@ -1,10 +1,11 @@ -from typing import Dict, List, Optional +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON +from sqlalchemy.schema import ForeignKey +from sqlalchemy.sql import func, select +from sqlalchemy.dialects.mysql import insert from core.data.schema import BaseData, metadata -from sqlalchemy import Column, Table, UniqueConstraint, and_ -from sqlalchemy.dialects.mysql import insert -from sqlalchemy.schema import ForeignKey -from sqlalchemy.types import JSON, Integer profile = Table( "cxb_profile", @@ -20,7 +21,7 @@ profile = Table( class CxbProfileData(BaseData): - async def put_profile( + def put_profile( self, user_id: int, version: int, index: int, data: JSON ) -> Optional[int]: sql = insert(profile).values( @@ -29,7 +30,7 @@ class CxbProfileData(BaseData): conflict = sql.on_duplicate_key_update(index=index, data=data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.error( f"{__name__} failed to update! user: {user_id}, index: {index}, data: {data}" @@ -38,7 +39,7 @@ class CxbProfileData(BaseData): return result.lastrowid - async def get_profile(self, aime_id: int, version: int) -> Optional[List[Dict]]: + def get_profile(self, aime_id: int, version: int) -> Optional[List[Dict]]: """ Given a game version and either a profile or aime id, return the profile """ @@ -46,12 +47,12 @@ class CxbProfileData(BaseData): and_(profile.c.version == version, profile.c.user == aime_id) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_profile_index( + def get_profile_index( self, index: int, aime_id: int = None, version: int = None ) -> Optional[Dict]: """ @@ -71,7 +72,7 @@ class CxbProfileData(BaseData): ) return None - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() diff --git a/titles/cxb/schema/score.py b/titles/cxb/schema/score.py index ea74a30..b6f4f16 100644 --- a/titles/cxb/schema/score.py +++ b/titles/cxb/schema/score.py @@ -1,11 +1,12 @@ -from typing import Dict, List, Optional - -from core.data.schema import BaseData, metadata -from sqlalchemy import Column, Table, UniqueConstraint, and_ -from sqlalchemy.dialects.mysql import insert +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ +from sqlalchemy.types import Integer, String, TIMESTAMP, JSON, Boolean from sqlalchemy.schema import ForeignKey from sqlalchemy.sql import func -from sqlalchemy.types import JSON, TIMESTAMP, Integer, String +from sqlalchemy.dialects.mysql import insert +from typing import Optional, List, Dict, Any + +from core.data.schema import BaseData, metadata +from core.data import cached score = Table( "cxb_score", @@ -57,7 +58,7 @@ ranking = Table( class CxbScoreData(BaseData): - async def put_best_score( + def put_best_score( self, user_id: int, song_mcode: str, @@ -78,7 +79,7 @@ class CxbScoreData(BaseData): conflict = sql.on_duplicate_key_update(data=sql.inserted.data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.error( f"{__name__} failed to insert best score! profile: {user_id}, song: {song_mcode}, data: {data}" @@ -87,7 +88,7 @@ class CxbScoreData(BaseData): return result.lastrowid - async def put_playlog( + def put_playlog( self, user_id: int, song_mcode: str, @@ -124,7 +125,7 @@ class CxbScoreData(BaseData): combo=combo, ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: self.logger.error( f"{__name__} failed to insert playlog! profile: {user_id}, song: {song_mcode}, chart: {chart_id}" @@ -133,7 +134,7 @@ class CxbScoreData(BaseData): return result.lastrowid - async def put_ranking( + def put_ranking( self, user_id: int, rev_id: int, song_id: int, score: int, clear: int ) -> Optional[int]: """ @@ -150,7 +151,7 @@ class CxbScoreData(BaseData): conflict = sql.on_duplicate_key_update(score=score) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.error( f"{__name__} failed to insert ranking log! profile: {user_id}, score: {score}, clear: {clear}" @@ -159,28 +160,28 @@ class CxbScoreData(BaseData): return result.lastrowid - async def get_best_score(self, user_id: int, song_mcode: int) -> Optional[Dict]: + def get_best_score(self, user_id: int, song_mcode: int) -> Optional[Dict]: sql = score.select( and_(score.c.user == user_id, score.c.song_mcode == song_mcode) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_best_scores(self, user_id: int) -> Optional[Dict]: + def get_best_scores(self, user_id: int) -> Optional[Dict]: sql = score.select(score.c.user == user_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_best_rankings(self, user_id: int) -> Optional[List[Dict]]: + def get_best_rankings(self, user_id: int) -> Optional[List[Dict]]: sql = ranking.select(ranking.c.user == user_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() diff --git a/titles/cxb/schema/static.py b/titles/cxb/schema/static.py index c480875..6459e99 100644 --- a/titles/cxb/schema/static.py +++ b/titles/cxb/schema/static.py @@ -1,11 +1,13 @@ -from typing import List, Optional +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 -from sqlalchemy import Column, Table, UniqueConstraint, and_ -from sqlalchemy.dialects.mysql import insert -from sqlalchemy.engine import Row -from sqlalchemy.sql import select -from sqlalchemy.types import Float, Integer, String music = Table( "cxb_static_music", @@ -27,7 +29,7 @@ music = Table( class CxbStaticData(BaseData): - async def put_music( + def put_music( self, version: int, mcode: str, @@ -53,12 +55,12 @@ class CxbStaticData(BaseData): title=title, artist=artist, category=category, level=level ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: return None return result.lastrowid - async def get_music( + def get_music( self, version: int, song_id: Optional[int] = None ) -> Optional[List[Row]]: if song_id is None: @@ -71,12 +73,12 @@ class CxbStaticData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_music_chart( + def get_music_chart( self, version: int, song_id: int, chart_id: int ) -> Optional[List[Row]]: sql = select(music).where( @@ -87,7 +89,7 @@ class CxbStaticData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() diff --git a/titles/diva/__init__.py b/titles/diva/__init__.py index c12cccf..9a9e6ef 100644 --- a/titles/diva/__init__.py +++ b/titles/diva/__init__.py @@ -1,9 +1,10 @@ +from titles.diva.index import DivaServlet from titles.diva.const import DivaConstants from titles.diva.database import DivaData -from titles.diva.index import DivaServlet from titles.diva.read import DivaReader index = DivaServlet database = DivaData reader = DivaReader game_codes = [DivaConstants.GAME_CODE] +current_schema_version = 6 diff --git a/titles/diva/base.py b/titles/diva/base.py index a0a85eb..6db0dbc 100644 --- a/titles/diva/base.py +++ b/titles/diva/base.py @@ -1,8 +1,9 @@ import datetime +from typing import Any, List, Dict import logging -import urllib.parse +import json +import urllib from threading import Thread -from typing import Dict from core.config import CoreConfig from titles.diva.config import DivaConfig @@ -23,13 +24,13 @@ class DivaBase: dt = datetime.datetime.now() self.time_lut = urllib.parse.quote(dt.strftime("%Y-%m-%d %H:%M:%S:16.0")) - async def handle_test_request(self, data: Dict) -> Dict: + def handle_test_request(self, data: Dict) -> Dict: return "" - async def handle_game_init_request(self, data: Dict) -> Dict: + def handle_game_init_request(self, data: Dict) -> Dict: return f"" - async def handle_attend_request(self, data: Dict) -> Dict: + def handle_attend_request(self, data: Dict) -> Dict: encoded = "&" params = { "atnd_prm1": "0,1,1,0,0,0,1,0,100,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1", @@ -43,7 +44,7 @@ class DivaBase: return encoded - async def handle_ping_request(self, data: Dict) -> Dict: + def handle_ping_request(self, data: Dict) -> Dict: encoded = "&" params = { "ping_b_msg": f"Welcome to {self.core_cfg.server.name} network!", @@ -88,7 +89,7 @@ class DivaBase: return encoded - async def handle_pv_list_request(self, data: Dict) -> Dict: + def handle_pv_list_request(self, data: Dict) -> Dict: pvlist = "" with open(r"titles/diva/data/PvList0.dat", encoding="utf-8") as shop: lines = shop.readlines() @@ -125,10 +126,10 @@ class DivaBase: return response - async def handle_shop_catalog_request(self, data: Dict) -> Dict: + def handle_shop_catalog_request(self, data: Dict) -> Dict: catalog = "" - shopList = await self.data.static.get_enabled_shops(self.version) + shopList = self.data.static.get_enabled_shops(self.version) if not shopList: with open(r"titles/diva/data/ShopCatalog.dat", encoding="utf-8") as shop: lines = shop.readlines() @@ -163,11 +164,9 @@ class DivaBase: return response - async def handle_buy_module_request(self, data: Dict) -> Dict: - profile = await self.data.profile.get_profile(data["pd_id"], self.version) - module = await self.data.static.get_enabled_shop( - self.version, int(data["mdl_id"]) - ) + def handle_buy_module_request(self, data: Dict) -> Dict: + profile = self.data.profile.get_profile(data["pd_id"], self.version) + module = self.data.static.get_enabled_shop(self.version, int(data["mdl_id"])) # make sure module is available to purchase if not module: @@ -179,13 +178,11 @@ class DivaBase: new_vcld_pts = profile["vcld_pts"] - int(data["mdl_price"]) - await self.data.profile.update_profile(profile["user"], vcld_pts=new_vcld_pts) - await self.data.module.put_module(data["pd_id"], self.version, data["mdl_id"]) + self.data.profile.update_profile(profile["user"], vcld_pts=new_vcld_pts) + self.data.module.put_module(data["pd_id"], self.version, data["mdl_id"]) # generate the mdl_have string - mdl_have = await self.data.module.get_modules_have_string( - data["pd_id"], self.version - ) + mdl_have = self.data.module.get_modules_have_string(data["pd_id"], self.version) response = "&shp_rslt=1" response += f"&mdl_id={data['mdl_id']}" @@ -194,10 +191,10 @@ class DivaBase: return response - async def handle_cstmz_itm_ctlg_request(self, data: Dict) -> Dict: + def handle_cstmz_itm_ctlg_request(self, data: Dict) -> Dict: catalog = "" - itemList = await self.data.static.get_enabled_items(self.version) + itemList = self.data.static.get_enabled_items(self.version) if not itemList: with open(r"titles/diva/data/ItemCatalog.dat", encoding="utf-8") as item: lines = item.readlines() @@ -232,9 +229,9 @@ class DivaBase: return response - async def handle_buy_cstmz_itm_request(self, data: Dict) -> Dict: - profile = await self.data.profile.get_profile(data["pd_id"], self.version) - item = await self.data.static.get_enabled_item( + def handle_buy_cstmz_itm_request(self, data: Dict) -> Dict: + profile = self.data.profile.get_profile(data["pd_id"], self.version) + item = self.data.static.get_enabled_item( self.version, int(data["cstmz_itm_id"]) ) @@ -249,14 +246,14 @@ class DivaBase: new_vcld_pts = profile["vcld_pts"] - int(data["cstmz_itm_price"]) # save new Vocaloid Points balance - await self.data.profile.update_profile(profile["user"], vcld_pts=new_vcld_pts) + self.data.profile.update_profile(profile["user"], vcld_pts=new_vcld_pts) - await self.data.customize.put_customize_item( + self.data.customize.put_customize_item( data["pd_id"], self.version, data["cstmz_itm_id"] ) # generate the cstmz_itm_have string - cstmz_itm_have = await self.data.customize.get_customize_items_have_string( + cstmz_itm_have = self.data.customize.get_customize_items_have_string( data["pd_id"], self.version ) @@ -267,7 +264,7 @@ class DivaBase: return response - async def handle_festa_info_request(self, data: Dict) -> Dict: + def handle_festa_info_request(self, data: Dict) -> Dict: encoded = "&" params = { "fi_id": "1,2", @@ -290,7 +287,7 @@ class DivaBase: return encoded - async def handle_contest_info_request(self, data: Dict) -> Dict: + def handle_contest_info_request(self, data: Dict) -> Dict: response = "" response += f"&ci_lut={self.time_lut}" @@ -298,10 +295,10 @@ class DivaBase: return response - async def handle_qst_inf_request(self, data: Dict) -> Dict: + def handle_qst_inf_request(self, data: Dict) -> Dict: quest = "" - questList = await self.data.static.get_enabled_quests(self.version) + questList = self.data.static.get_enabled_quests(self.version) if not questList: with open(r"titles/diva/data/QuestInfo.dat", encoding="utf-8") as shop: lines = shop.readlines() @@ -348,45 +345,45 @@ class DivaBase: return response - async def handle_nv_ranking_request(self, data: Dict) -> Dict: + def handle_nv_ranking_request(self, data: Dict) -> Dict: return f"" - async def handle_ps_ranking_request(self, data: Dict) -> Dict: + def handle_ps_ranking_request(self, data: Dict) -> Dict: return f"" - async def handle_ng_word_request(self, data: Dict) -> Dict: + def handle_ng_word_request(self, data: Dict) -> Dict: return f"" - async def handle_rmt_wp_list_request(self, data: Dict) -> Dict: + def handle_rmt_wp_list_request(self, data: Dict) -> Dict: return f"" - async def handle_pv_def_chr_list_request(self, data: Dict) -> Dict: + def handle_pv_def_chr_list_request(self, data: Dict) -> Dict: return f"" - async def handle_pv_ng_mdl_list_request(self, data: Dict) -> Dict: + def handle_pv_ng_mdl_list_request(self, data: Dict) -> Dict: return f"" - async def handle_cstmz_itm_ng_mdl_lst_request(self, data: Dict) -> Dict: + def handle_cstmz_itm_ng_mdl_lst_request(self, data: Dict) -> Dict: return f"" - async def handle_banner_info_request(self, data: Dict) -> Dict: + def handle_banner_info_request(self, data: Dict) -> Dict: return f"" - async def handle_banner_data_request(self, data: Dict) -> Dict: + def handle_banner_data_request(self, data: Dict) -> Dict: return f"" - async def handle_cm_ply_info_request(self, data: Dict) -> Dict: + def handle_cm_ply_info_request(self, data: Dict) -> Dict: return f"" - async def handle_pstd_h_ctrl_request(self, data: Dict) -> Dict: + def handle_pstd_h_ctrl_request(self, data: Dict) -> Dict: return f"" - async def handle_pstd_item_ng_lst_request(self, data: Dict) -> Dict: + def handle_pstd_item_ng_lst_request(self, data: Dict) -> Dict: return f"" - async def handle_pre_start_request(self, data: Dict) -> str: - profile = await self.data.profile.get_profile(data["aime_id"], self.version) - profile_shop = await self.data.item.get_shop(data["aime_id"], self.version) + def handle_pre_start_request(self, data: Dict) -> str: + profile = self.data.profile.get_profile(data["aime_id"], self.version) + profile_shop = self.data.item.get_shop(data["aime_id"], self.version) if profile is None: return f"&ps_result=-3" @@ -425,29 +422,29 @@ class DivaBase: return response - async def handle_registration_request(self, data: Dict) -> Dict: - await self.data.profile.create_profile( + def handle_registration_request(self, data: Dict) -> Dict: + self.data.profile.create_profile( self.version, data["aime_id"], data["player_name"] ) return f"&cd_adm_result=1&pd_id={data['aime_id']}" - async def handle_start_request(self, data: Dict) -> Dict: - profile = await self.data.profile.get_profile(data["pd_id"], self.version) - profile_shop = await self.data.item.get_shop(data["pd_id"], self.version) + def handle_start_request(self, data: Dict) -> Dict: + profile = self.data.profile.get_profile(data["pd_id"], self.version) + profile_shop = self.data.item.get_shop(data["pd_id"], self.version) if profile is None: return mdl_have = "F" * 250 # generate the mdl_have string if "unlock_all_modules" is disabled if not self.game_config.mods.unlock_all_modules: - mdl_have = await self.data.module.get_modules_have_string( + mdl_have = self.data.module.get_modules_have_string( data["pd_id"], self.version ) cstmz_itm_have = "F" * 250 # generate the cstmz_itm_have string if "unlock_all_items" is disabled if not self.game_config.mods.unlock_all_items: - cstmz_itm_have = await self.data.customize.get_customize_items_have_string( + cstmz_itm_have = self.data.customize.get_customize_items_have_string( data["pd_id"], self.version ) @@ -528,7 +525,7 @@ class DivaBase: } # get clear status from user scores - pv_records = await self.data.score.get_best_scores(data["pd_id"]) + pv_records = self.data.score.get_best_scores(data["pd_id"]) clear_status = "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0" if pv_records is not None: @@ -586,11 +583,11 @@ class DivaBase: return response - async def handle_pd_unlock_request(self, data: Dict) -> Dict: + def handle_pd_unlock_request(self, data: Dict) -> Dict: return f"" - async def handle_spend_credit_request(self, data: Dict) -> Dict: - profile = await self.data.profile.get_profile(data["pd_id"], self.version) + def handle_spend_credit_request(self, data: Dict) -> Dict: + profile = self.data.profile.get_profile(data["pd_id"], self.version) if profile is None: return @@ -667,29 +664,30 @@ class DivaBase: return pv_result - async def task_generateScoreData(self, data: Dict, pd_by_pv_id, song): + def task_generateScoreData(self, data: Dict, pd_by_pv_id, song): + if int(song) > 0: # the request do not send a edition so just perform a query best score and ranking for each edition. # 0=ORIGINAL, 1=EXTRA - pd_db_song_0 = await self.data.score.get_best_user_score( + pd_db_song_0 = self.data.score.get_best_user_score( data["pd_id"], int(song), data["difficulty"], edition=0 ) - pd_db_song_1 = await self.data.score.get_best_user_score( + pd_db_song_1 = self.data.score.get_best_user_score( data["pd_id"], int(song), data["difficulty"], edition=1 ) pd_db_ranking_0, pd_db_ranking_1 = None, None if pd_db_song_0: - pd_db_ranking_0 = await self.data.score.get_global_ranking( + pd_db_ranking_0 = self.data.score.get_global_ranking( data["pd_id"], int(song), data["difficulty"], edition=0 ) if pd_db_song_1: - pd_db_ranking_1 = await self.data.score.get_global_ranking( + pd_db_ranking_1 = self.data.score.get_global_ranking( data["pd_id"], int(song), data["difficulty"], edition=1 ) - pd_db_customize = await self.data.pv_customize.get_pv_customize( + pd_db_customize = self.data.pv_customize.get_pv_customize( data["pd_id"], int(song) ) @@ -707,7 +705,7 @@ class DivaBase: pd_by_pv_id.append(urllib.parse.quote(f"{song}***")) pd_by_pv_id.append(",") - async def handle_get_pv_pd_request(self, data: Dict) -> Dict: + def handle_get_pv_pd_request(self, data: Dict) -> Dict: song_id = data["pd_pv_id_lst"].split(",") pv = "" @@ -715,9 +713,7 @@ class DivaBase: pd_by_pv_id = [] for song in song_id: - thread_ScoreData = Thread( - target=await self.task_generateScoreData(data, pd_by_pv_id, song) - ) + thread_ScoreData = Thread(target=self.task_generateScoreData(data, pd_by_pv_id, song)) threads.append(thread_ScoreData) for x in threads: @@ -736,11 +732,11 @@ class DivaBase: return response - async def handle_stage_start_request(self, data: Dict) -> Dict: + def handle_stage_start_request(self, data: Dict) -> Dict: return f"" - async def handle_stage_result_request(self, data: Dict) -> Dict: - profile = await self.data.profile.get_profile(data["pd_id"], self.version) + def handle_stage_result_request(self, data: Dict) -> Dict: + profile = self.data.profile.get_profile(data["pd_id"], self.version) pd_song_list = data["stg_ply_pv_id"].split(",") pd_song_difficulty = data["stg_difficulty"].split(",") @@ -758,14 +754,14 @@ class DivaBase: for index, value in enumerate(pd_song_list): if "-1" not in pd_song_list[index]: - profile_pd_db_song = await self.data.score.get_best_user_score( + profile_pd_db_song = self.data.score.get_best_user_score( data["pd_id"], pd_song_list[index], pd_song_difficulty[index], pd_song_edition[index], ) if profile_pd_db_song is None: - await self.data.score.put_best_score( + self.data.score.put_best_score( data["pd_id"], self.version, pd_song_list[index], @@ -782,7 +778,7 @@ class DivaBase: pd_song_worst_cnt[index], pd_song_max_combo[index], ) - await self.data.score.put_playlog( + self.data.score.put_playlog( data["pd_id"], self.version, pd_song_list[index], @@ -800,7 +796,7 @@ class DivaBase: pd_song_max_combo[index], ) elif int(pd_song_max_score[index]) >= int(profile_pd_db_song["score"]): - await self.data.score.put_best_score( + self.data.score.put_best_score( data["pd_id"], self.version, pd_song_list[index], @@ -817,7 +813,7 @@ class DivaBase: pd_song_worst_cnt[index], pd_song_max_combo[index], ) - await self.data.score.put_playlog( + self.data.score.put_playlog( data["pd_id"], self.version, pd_song_list[index], @@ -835,7 +831,7 @@ class DivaBase: pd_song_max_combo[index], ) elif int(pd_song_max_score[index]) != int(profile_pd_db_song["score"]): - await self.data.score.put_playlog( + self.data.score.put_playlog( data["pd_id"], self.version, pd_song_list[index], @@ -856,7 +852,7 @@ class DivaBase: # Profile saving based on registration list # Calculate new level - best_scores = await self.data.score.get_best_scores(data["pd_id"]) + best_scores = self.data.score.get_best_scores(data["pd_id"]) total_atn_pnt = 0 for best_score in best_scores: @@ -870,7 +866,7 @@ class DivaBase: response += f"&lv_pnt_old={int(profile['lv_pnt'])}" # update the profile and commit changes to the db - await self.data.profile.update_profile( + self.data.profile.update_profile( profile["user"], lv_num=new_level, lv_pnt=new_level_pnt, @@ -918,16 +914,16 @@ class DivaBase: return response - async def handle_end_request(self, data: Dict) -> Dict: - profile = await self.data.profile.get_profile(data["pd_id"], self.version) + def handle_end_request(self, data: Dict) -> Dict: + profile = self.data.profile.get_profile(data["pd_id"], self.version) - await self.data.profile.update_profile( + self.data.profile.update_profile( profile["user"], my_qst_id=data["my_qst_id"], my_qst_sts=data["my_qst_sts"] ) return f"" - async def handle_shop_exit_request(self, data: Dict) -> Dict: - await self.data.item.put_shop( + def handle_shop_exit_request(self, data: Dict) -> Dict: + self.data.item.put_shop( data["pd_id"], self.version, data["mdl_eqp_cmn_ary"], @@ -935,7 +931,7 @@ class DivaBase: data["ms_itm_flg_cmn_ary"], ) if int(data["use_pv_mdl_eqp"]) == 1: - await self.data.pv_customize.put_pv_customize( + self.data.pv_customize.put_pv_customize( data["pd_id"], self.version, data["ply_pv_id"], @@ -944,7 +940,7 @@ class DivaBase: data["ms_itm_flg_pv_ary"], ) else: - await self.data.pv_customize.put_pv_customize( + self.data.pv_customize.put_pv_customize( data["pd_id"], self.version, data["ply_pv_id"], @@ -956,8 +952,8 @@ class DivaBase: response = "&shp_rslt=1" return response - async def handle_card_procedure_request(self, data: Dict) -> str: - profile = await self.data.profile.get_profile(data["aime_id"], self.version) + def handle_card_procedure_request(self, data: Dict) -> str: + profile = self.data.profile.get_profile(data["aime_id"], self.version) if profile is None: return "&cd_adm_result=0" @@ -976,8 +972,8 @@ class DivaBase: return response - async def handle_change_name_request(self, data: Dict) -> str: - profile = await self.data.profile.get_profile(data["pd_id"], self.version) + def handle_change_name_request(self, data: Dict) -> str: + profile = self.data.profile.get_profile(data["pd_id"], self.version) # make sure user has enough Vocaloid Points if profile["vcld_pts"] < int(data["chg_name_price"]): @@ -985,7 +981,7 @@ class DivaBase: # update the vocaloid points and player name new_vcld_pts = profile["vcld_pts"] - int(data["chg_name_price"]) - await self.data.profile.update_profile( + self.data.profile.update_profile( profile["user"], player_name=data["player_name"], vcld_pts=new_vcld_pts ) @@ -996,15 +992,15 @@ class DivaBase: return response - async def handle_change_passwd_request(self, data: Dict) -> str: - profile = await self.data.profile.get_profile(data["pd_id"], self.version) + def handle_change_passwd_request(self, data: Dict) -> str: + profile = self.data.profile.get_profile(data["pd_id"], self.version) # TODO: return correct error number instead of 0 if data["passwd"] != profile["passwd"]: return "&cd_adm_result=0" # set password to true and update the saved password - await self.data.profile.update_profile( + self.data.profile.update_profile( profile["user"], passwd_stat=1, passwd=data["new_passwd"] ) diff --git a/titles/diva/database.py b/titles/diva/database.py index ca2631d..cf36af9 100644 --- a/titles/diva/database.py +++ b/titles/diva/database.py @@ -1,12 +1,12 @@ -from core.config import CoreConfig from core.data import Data +from core.config import CoreConfig from titles.diva.schema import ( - DivaCustomizeItemData, - DivaItemData, - DivaModuleData, DivaProfileData, - DivaPvCustomizeData, DivaScoreData, + DivaModuleData, + DivaCustomizeItemData, + DivaPvCustomizeData, + DivaItemData, DivaStaticData, ) diff --git a/titles/diva/index.py b/titles/diva/index.py index 517e3b2..ac4114e 100644 --- a/titles/diva/index.py +++ b/titles/diva/index.py @@ -1,24 +1,20 @@ -import base64 -import json -import logging -import urllib.parse -import zlib -from logging.handlers import TimedRotatingFileHandler -from os import path -from typing import Dict, List, Tuple - -import coloredlogs +from twisted.web.http import Request import yaml +import logging, coloredlogs +from logging.handlers import TimedRotatingFileHandler +import zlib +import json +import urllib.parse +import base64 +from os import path +from typing import Tuple, Dict, List + from core.config import CoreConfig from core.title import BaseServlet from core.utils import Utils -from starlette.requests import Request -from starlette.responses import PlainTextResponse -from starlette.routing import Route - -from .base import DivaBase from .config import DivaConfig from .const import DivaConstants +from .base import DivaBase class DivaServlet(BaseServlet): @@ -55,25 +51,17 @@ class DivaServlet(BaseServlet): level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str ) - def get_routes(self) -> List[Route]: - return [Route("/DivaServlet/", self.render_POST, methods=["POST"])] - - 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.server.hostname}:{Utils.get_title_port(self.core_cfg)}/DivaServlet/", - self.core_cfg.server.hostname, - ) - + def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: return ( - f"http://{self.core_cfg.server.hostname}/DivaServlet/", - self.core_cfg.server.hostname, + [], + [("render_POST", "/DivaServlet/", {})] ) + + 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)}/DivaServlet/", self.core_cfg.title.hostname) + + return (f"http://{self.core_cfg.title.hostname}/DivaServlet/", self.core_cfg.title.hostname) @classmethod def is_game_enabled( @@ -90,11 +78,9 @@ class DivaServlet(BaseServlet): return True - async def render_POST( - self, request: Request, game_code: str, matchers: Dict - ) -> bytes: - req_raw = await request.body() - url_header = request.headers + def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes: + req_raw = request.content.getvalue() + url_header = request.getAllHeaders() # Ping Dispatch if "THIS_STRING_SEPARATES" in str(url_header): @@ -117,8 +103,8 @@ class DivaServlet(BaseServlet): self.logger.debug( f"Response cmd={bin_req_data['cmd']}&req_id={bin_req_data['req_id']}&stat=ok{resp}" ) - return PlainTextResponse( - f"cmd={bin_req_data['cmd']}&req_id={bin_req_data['req_id']}&stat=ok{resp}" + return f"cmd={bin_req_data['cmd']}&req_id={bin_req_data['req_id']}&stat=ok{resp}".encode( + "utf-8" ) # Main Dispatch @@ -136,7 +122,7 @@ class DivaServlet(BaseServlet): ) # Decompressing the gzip except zlib.error as e: self.logger.error(f"Failed to defalte! {e} -> {gz_string}") - return PlainTextResponse("stat=0") + return "stat=0" req_kvp = urllib.parse.unquote(url_data) req_data = {} @@ -155,24 +141,27 @@ class DivaServlet(BaseServlet): # Load the requests try: handler = getattr(self.base, func_to_find) - resp = await handler(req_data) + resp = handler(req_data) except AttributeError as e: self.logger.warning(f"Unhandled {req_data['cmd']} request {e}") - return PlainTextResponse( - f"cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok" + return f"cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok".encode( + "utf-8" ) except Exception as e: self.logger.error(f"Error handling method {func_to_find} {e}") - return PlainTextResponse( - f"cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok" + return f"cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok".encode( + "utf-8" ) + request.responseHeaders.addRawHeader(b"content-type", b"text/plain") self.logger.debug( f"Response cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok{resp}" ) - return PlainTextResponse( - f"cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok{resp}" + return ( + f"cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok{resp}".encode( + "utf-8" + ) ) diff --git a/titles/diva/read.py b/titles/diva/read.py index 9a6f1fc..f143bb6 100644 --- a/titles/diva/read.py +++ b/titles/diva/read.py @@ -1,11 +1,11 @@ +from typing import Optional, Dict, List +from os import walk, path import urllib -from os import path, walk -from typing import Dict, List, Optional -from core.config import CoreConfig from read import BaseReader -from titles.diva.const import DivaConstants +from core.config import CoreConfig from titles.diva.database import DivaData +from titles.diva.const import DivaConstants class DivaReader(BaseReader): @@ -28,7 +28,7 @@ class DivaReader(BaseReader): self.logger.error(f"Invalid project diva version {version}") exit(1) - async def read(self) -> None: + def read(self) -> None: pull_bin_ram = True pull_bin_rom = True pull_opt_rom = True @@ -48,14 +48,14 @@ class DivaReader(BaseReader): self.logger.warning("No option directory specified, skipping") if pull_bin_ram: - await self.read_ram(f"{self.bin_dir}/ram") + self.read_ram(f"{self.bin_dir}/ram") if pull_bin_rom: - await self.read_rom(f"{self.bin_dir}/rom") + self.read_rom(f"{self.bin_dir}/rom") if pull_opt_rom: for dir in opt_dirs: - await self.read_rom(f"{dir}/rom") + self.read_rom(f"{dir}/rom") - async def read_ram(self, ram_root_dir: str) -> None: + def read_ram(self, ram_root_dir: str) -> None: self.logger.info(f"Read RAM from {ram_root_dir}") if path.exists(f"{ram_root_dir}/databank"): @@ -91,7 +91,7 @@ class DivaReader(BaseReader): f"Added shop item {split[x+0]}" ) - await self.data.static.put_shop( + self.data.static.put_shop( self.version, split[x + 0], split[x + 2], @@ -109,7 +109,7 @@ class DivaReader(BaseReader): for x in range(0, len(split), 7): self.logger.info(f"Added item {split[x+0]}") - await self.data.static.put_items( + self.data.static.put_items( self.version, split[x + 0], split[x + 2], @@ -123,7 +123,7 @@ class DivaReader(BaseReader): elif file.startswith("QuestInfo") and len(split) >= 9: self.logger.info(f"Added quest {split[0]}") - await self.data.static.put_quests( + self.data.static.put_quests( self.version, split[0], split[6], @@ -139,11 +139,9 @@ class DivaReader(BaseReader): else: continue else: - self.logger.warning( - f"Databank folder not found in {ram_root_dir}, skipping" - ) + self.logger.warning(f"Databank folder not found in {ram_root_dir}, skipping") - async def read_rom(self, rom_root_dir: str) -> None: + def read_rom(self, rom_root_dir: str) -> None: self.logger.info(f"Read ROM from {rom_root_dir}") pv_list: Dict[str, Dict] = {} @@ -201,7 +199,7 @@ class DivaReader(BaseReader): diff = pv_data["difficulty"]["easy"]["0"]["level"].split("_") self.logger.info(f"Added song {song_id} chart 0") - await self.data.static.put_music( + self.data.static.put_music( self.version, song_id, 0, @@ -222,7 +220,7 @@ class DivaReader(BaseReader): diff = pv_data["difficulty"]["normal"]["0"]["level"].split("_") self.logger.info(f"Added song {song_id} chart 1") - await self.data.static.put_music( + self.data.static.put_music( self.version, song_id, 1, @@ -240,7 +238,7 @@ class DivaReader(BaseReader): diff = pv_data["difficulty"]["hard"]["0"]["level"].split("_") self.logger.info(f"Added song {song_id} chart 2") - await self.data.static.put_music( + self.data.static.put_music( self.version, song_id, 2, @@ -259,7 +257,7 @@ class DivaReader(BaseReader): diff = pv_data["difficulty"]["extreme"]["0"]["level"].split("_") self.logger.info(f"Added song {song_id} chart 3") - await self.data.static.put_music( + self.data.static.put_music( self.version, song_id, 3, @@ -277,7 +275,7 @@ class DivaReader(BaseReader): diff = pv_data["difficulty"]["extreme"]["1"]["level"].split("_") self.logger.info(f"Added song {song_id} chart 4") - await self.data.static.put_music( + self.data.static.put_music( self.version, song_id, 4, diff --git a/titles/diva/schema/__init__.py b/titles/diva/schema/__init__.py index 8b3c9ac..e149e6d 100644 --- a/titles/diva/schema/__init__.py +++ b/titles/diva/schema/__init__.py @@ -1,9 +1,9 @@ -from titles.diva.schema.customize import DivaCustomizeItemData -from titles.diva.schema.item import DivaItemData -from titles.diva.schema.module import DivaModuleData from titles.diva.schema.profile import DivaProfileData -from titles.diva.schema.pv_customize import DivaPvCustomizeData from titles.diva.schema.score import DivaScoreData +from titles.diva.schema.module import DivaModuleData +from titles.diva.schema.customize import DivaCustomizeItemData +from titles.diva.schema.pv_customize import DivaPvCustomizeData +from titles.diva.schema.item import DivaItemData from titles.diva.schema.static import DivaStaticData __all__ = [ diff --git a/titles/diva/schema/customize.py b/titles/diva/schema/customize.py index d58a17e..91480f5 100644 --- a/titles/diva/schema/customize.py +++ b/titles/diva/schema/customize.py @@ -1,10 +1,10 @@ -from typing import Dict, List, Optional +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, UniqueConstraint, and_ +from sqlalchemy.types import Integer +from sqlalchemy.schema import ForeignKey +from sqlalchemy.dialects.mysql import insert from core.data.schema import BaseData, metadata -from sqlalchemy import Column, Table, UniqueConstraint, and_ -from sqlalchemy.dialects.mysql import insert -from sqlalchemy.schema import ForeignKey -from sqlalchemy.types import Integer customize = Table( "diva_profile_customize_item", @@ -25,12 +25,10 @@ customize = Table( class DivaCustomizeItemData(BaseData): - async def put_customize_item( - self, aime_id: int, version: int, item_id: int - ) -> None: + def put_customize_item(self, aime_id: int, version: int, item_id: int) -> None: sql = insert(customize).values(version=version, user=aime_id, item_id=item_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: self.logger.error( f"{__name__} Failed to insert diva profile customize item! aime id: {aime_id} item: {item_id}" @@ -38,9 +36,7 @@ class DivaCustomizeItemData(BaseData): return None return result.lastrowid - async def get_customize_items( - self, aime_id: int, version: int - ) -> Optional[List[Dict]]: + def get_customize_items(self, aime_id: int, version: int) -> Optional[List[Dict]]: """ Given a game version and an aime id, return all the customize items, not used directly """ @@ -48,12 +44,12 @@ class DivaCustomizeItemData(BaseData): and_(customize.c.version == version, customize.c.user == aime_id) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_customize_items_have_string(self, aime_id: int, version: int) -> str: + def get_customize_items_have_string(self, aime_id: int, version: int) -> str: """ Given a game version and an aime id, return the cstmz_itm_have hex string required for diva directly diff --git a/titles/diva/schema/item.py b/titles/diva/schema/item.py index b54e111..4d484ae 100644 --- a/titles/diva/schema/item.py +++ b/titles/diva/schema/item.py @@ -1,10 +1,11 @@ -from typing import Dict, List, Optional +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean +from sqlalchemy.schema import ForeignKey +from sqlalchemy.sql import func +from sqlalchemy.dialects.mysql import insert from core.data.schema import BaseData, metadata -from sqlalchemy import Column, Table, UniqueConstraint, and_ -from sqlalchemy.dialects.mysql import insert -from sqlalchemy.schema import ForeignKey -from sqlalchemy.types import Integer, String shop = Table( "diva_profile_shop", @@ -25,7 +26,7 @@ shop = Table( class DivaItemData(BaseData): - async def put_shop( + def put_shop( self, aime_id: int, version: int, @@ -47,7 +48,7 @@ class DivaItemData(BaseData): ms_itm_flg_ary=ms_itm_flg_ary, ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.error( f"{__name__} Failed to insert diva profile! aime id: {aime_id} array: {mdl_eqp_ary}" @@ -55,13 +56,13 @@ class DivaItemData(BaseData): return None return result.lastrowid - async def get_shop(self, aime_id: int, version: int) -> Optional[List[Dict]]: + def get_shop(self, aime_id: int, version: int) -> Optional[List[Dict]]: """ Given a game version and either a profile or aime id, return the profile """ sql = shop.select(and_(shop.c.version == version, shop.c.user == aime_id)) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() diff --git a/titles/diva/schema/module.py b/titles/diva/schema/module.py index 68f7b0e..5872d68 100644 --- a/titles/diva/schema/module.py +++ b/titles/diva/schema/module.py @@ -1,10 +1,10 @@ -from typing import Dict, List, Optional +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, UniqueConstraint, and_ +from sqlalchemy.types import Integer +from sqlalchemy.schema import ForeignKey +from sqlalchemy.dialects.mysql import insert from core.data.schema import BaseData, metadata -from sqlalchemy import Column, Table, UniqueConstraint, and_ -from sqlalchemy.dialects.mysql import insert -from sqlalchemy.schema import ForeignKey -from sqlalchemy.types import Integer module = Table( "diva_profile_module", @@ -23,10 +23,10 @@ module = Table( class DivaModuleData(BaseData): - async def put_module(self, aime_id: int, version: int, module_id: int) -> None: + def put_module(self, aime_id: int, version: int, module_id: int) -> None: sql = insert(module).values(version=version, user=aime_id, module_id=module_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: self.logger.error( f"{__name__} Failed to insert diva profile module! aime id: {aime_id} module: {module_id}" @@ -34,18 +34,18 @@ class DivaModuleData(BaseData): return None return result.lastrowid - async def get_modules(self, aime_id: int, version: int) -> Optional[List[Dict]]: + def get_modules(self, aime_id: int, version: int) -> Optional[List[Dict]]: """ Given a game version and an aime id, return all the modules, not used directly """ sql = module.select(and_(module.c.version == version, module.c.user == aime_id)) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_modules_have_string(self, aime_id: int, version: int) -> str: + def get_modules_have_string(self, aime_id: int, version: int) -> str: """ Given a game version and an aime id, return the mdl_have hex string required for diva directly diff --git a/titles/diva/schema/profile.py b/titles/diva/schema/profile.py index 01f39c4..7bd6bf0 100644 --- a/titles/diva/schema/profile.py +++ b/titles/diva/schema/profile.py @@ -1,10 +1,11 @@ -from typing import Dict, List, Optional +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON +from sqlalchemy.schema import ForeignKey +from sqlalchemy.sql import func, select +from sqlalchemy.dialects.mysql import insert from core.data.schema import BaseData, metadata -from sqlalchemy import Column, Table, UniqueConstraint, and_ -from sqlalchemy.dialects.mysql import insert -from sqlalchemy.schema import ForeignKey -from sqlalchemy.types import Boolean, Integer, String profile = Table( "diva_profile", @@ -69,7 +70,7 @@ profile = Table( class DivaProfileData(BaseData): - async def create_profile( + def create_profile( self, version: int, aime_id: int, player_name: str ) -> Optional[int]: """ @@ -81,7 +82,7 @@ class DivaProfileData(BaseData): conflict = sql.on_duplicate_key_update(player_name=sql.inserted.player_name) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.error( f"{__name__} Failed to insert diva profile! aime id: {aime_id} username: {player_name}" @@ -89,21 +90,21 @@ class DivaProfileData(BaseData): return None return result.lastrowid - async def update_profile(self, aime_id: int, **profile_args) -> None: + def update_profile(self, aime_id: int, **profile_args) -> None: """ Given an aime_id update the profile corresponding to the arguments which are the diva_profile Columns """ sql = profile.update(profile.c.user == aime_id).values(**profile_args) - result = await self.execute(sql) + result = self.execute(sql) if result is None: self.logger.error( f"update_profile: failed to update profile! profile: {aime_id}" ) return None - async def get_profile(self, aime_id: int, version: int) -> Optional[List[Dict]]: + def get_profile(self, aime_id: int, version: int) -> Optional[List[Dict]]: """ Given a game version and either a profile or aime id, return the profile """ @@ -111,7 +112,7 @@ class DivaProfileData(BaseData): and_(profile.c.version == version, profile.c.user == aime_id) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() diff --git a/titles/diva/schema/pv_customize.py b/titles/diva/schema/pv_customize.py index 4e3a520..1ca8909 100644 --- a/titles/diva/schema/pv_customize.py +++ b/titles/diva/schema/pv_customize.py @@ -1,10 +1,10 @@ -from typing import Dict, List, Optional +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, UniqueConstraint, and_ +from sqlalchemy.types import Integer, String +from sqlalchemy.schema import ForeignKey +from sqlalchemy.dialects.mysql import insert from core.data.schema import BaseData, metadata -from sqlalchemy import Column, Table, UniqueConstraint, and_ -from sqlalchemy.dialects.mysql import insert -from sqlalchemy.schema import ForeignKey -from sqlalchemy.types import Integer, String pv_customize = Table( "diva_profile_pv_customize", @@ -39,7 +39,7 @@ pv_customize = Table( class DivaPvCustomizeData(BaseData): - async def put_pv_customize( + def put_pv_customize( self, aime_id: int, version: int, @@ -64,7 +64,7 @@ class DivaPvCustomizeData(BaseData): ms_itm_flg_ary=ms_itm_flg_ary, ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.error( f"{__name__} Failed to insert diva pv customize! aime id: {aime_id}" @@ -72,7 +72,7 @@ class DivaPvCustomizeData(BaseData): return None return result.lastrowid - async def get_pv_customize(self, aime_id: int, pv_id: int) -> Optional[List[Dict]]: + def get_pv_customize(self, aime_id: int, pv_id: int) -> Optional[List[Dict]]: """ Given either a profile or aime id, return a Pv Customize row """ @@ -80,7 +80,7 @@ class DivaPvCustomizeData(BaseData): and_(pv_customize.c.user == aime_id, pv_customize.c.pv_id == pv_id) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() diff --git a/titles/diva/schema/score.py b/titles/diva/schema/score.py index 555f1c1..2171659 100644 --- a/titles/diva/schema/score.py +++ b/titles/diva/schema/score.py @@ -1,12 +1,13 @@ -from typing import List, Optional - -from core.data.schema import BaseData, metadata -from sqlalchemy import Column, Table, UniqueConstraint, and_ -from sqlalchemy.dialects.mysql import insert -from sqlalchemy.engine import Row +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ +from sqlalchemy.types import Integer, String, TIMESTAMP, JSON, Boolean from sqlalchemy.schema import ForeignKey from sqlalchemy.sql import func, select -from sqlalchemy.types import TIMESTAMP, Integer +from sqlalchemy.dialects.mysql import insert +from sqlalchemy.engine import Row +from typing import Optional, List, Dict, Any + +from core.data.schema import BaseData, metadata +from core.data import cached score = Table( "diva_score", @@ -56,7 +57,7 @@ playlog = Table( class DivaScoreData(BaseData): - async def put_best_score( + def put_best_score( self, user_id: int, game_version: int, @@ -108,7 +109,7 @@ class DivaScoreData(BaseData): max_combo=max_combo, ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.error( f"{__name__} failed to insert best score! profile: {user_id}, song: {song_id}" @@ -117,7 +118,7 @@ class DivaScoreData(BaseData): return result.lastrowid - async def put_playlog( + def put_playlog( self, user_id: int, game_version: int, @@ -156,7 +157,7 @@ class DivaScoreData(BaseData): max_combo=max_combo, ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: self.logger.error( f"{__name__} failed to insert playlog! profile: {user_id}, song: {song_id}, chart: {difficulty}" @@ -165,7 +166,7 @@ class DivaScoreData(BaseData): return result.lastrowid - async def get_best_user_score( + def get_best_user_score( self, user_id: int, pv_id: int, difficulty: int, edition: int ) -> Optional[Row]: sql = score.select( @@ -177,12 +178,12 @@ class DivaScoreData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_top3_scores( + def get_top3_scores( self, pv_id: int, difficulty: int, edition: int ) -> Optional[List[Row]]: sql = ( @@ -197,12 +198,12 @@ class DivaScoreData(BaseData): .limit(3) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_global_ranking( + def get_global_ranking( self, user_id: int, pv_id: int, difficulty: int, edition: int ) -> Optional[List[Row]]: # get the subquery max score of a user with pv_id, difficulty and @@ -226,15 +227,15 @@ class DivaScoreData(BaseData): score.c.edition == edition, ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_best_scores(self, user_id: int) -> Optional[List[Row]]: + def get_best_scores(self, user_id: int) -> Optional[List[Row]]: sql = score.select(score.c.user == user_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() diff --git a/titles/diva/schema/static.py b/titles/diva/schema/static.py index 3f2cd74..02ee0ec 100644 --- a/titles/diva/schema/static.py +++ b/titles/diva/schema/static.py @@ -1,11 +1,13 @@ -from typing import List, Optional +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 -from sqlalchemy import Column, Table, UniqueConstraint, and_ -from sqlalchemy.dialects.mysql import insert -from sqlalchemy.engine import Row -from sqlalchemy.sql import select -from sqlalchemy.types import Boolean, Float, Integer, String music = Table( "diva_static_music", @@ -81,7 +83,7 @@ items = Table( class DivaStaticData(BaseData): - async def put_quests( + def put_quests( self, version: int, questId: int, @@ -109,22 +111,22 @@ class DivaStaticData(BaseData): conflict = sql.on_duplicate_key_update(name=name) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: return None return result.lastrowid - async def get_enabled_quests(self, version: int) -> Optional[List[Row]]: + def get_enabled_quests(self, version: int) -> Optional[List[Row]]: sql = select(quests).where( and_(quests.c.version == version, quests.c.quest_enable == True) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_shop( + def put_shop( self, version: int, shopId: int, @@ -148,12 +150,12 @@ class DivaStaticData(BaseData): conflict = sql.on_duplicate_key_update(name=name) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: return None return result.lastrowid - async def get_enabled_shop(self, version: int, shopId: int) -> Optional[Row]: + def get_enabled_shop(self, version: int, shopId: int) -> Optional[Row]: sql = select(shop).where( and_( shop.c.version == version, @@ -162,22 +164,22 @@ class DivaStaticData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_enabled_shops(self, version: int) -> Optional[List[Row]]: + def get_enabled_shops(self, version: int) -> Optional[List[Row]]: sql = select(shop).where( and_(shop.c.version == version, shop.c.enabled == True) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_items( + def put_items( self, version: int, itemId: int, @@ -201,12 +203,12 @@ class DivaStaticData(BaseData): conflict = sql.on_duplicate_key_update(name=name) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: return None return result.lastrowid - async def get_enabled_item(self, version: int, itemId: int) -> Optional[Row]: + def get_enabled_item(self, version: int, itemId: int) -> Optional[Row]: sql = select(items).where( and_( items.c.version == version, @@ -215,22 +217,22 @@ class DivaStaticData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_enabled_items(self, version: int) -> Optional[List[Row]]: + def get_enabled_items(self, version: int) -> Optional[List[Row]]: sql = select(items).where( and_(items.c.version == version, items.c.enabled == True) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_music( + def put_music( self, version: int, song: int, @@ -269,12 +271,12 @@ class DivaStaticData(BaseData): date=date, ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: return None return result.lastrowid - async def get_music( + def get_music( self, version: int, song_id: Optional[int] = None ) -> Optional[List[Row]]: if song_id is None: @@ -287,12 +289,12 @@ class DivaStaticData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_music_chart( + def get_music_chart( self, version: int, song_id: int, chart_id: int ) -> Optional[List[Row]]: sql = select(music).where( @@ -303,7 +305,7 @@ class DivaStaticData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() diff --git a/titles/idac/__init__.py b/titles/idac/__init__.py index 8641077..0c632bd 100644 --- a/titles/idac/__init__.py +++ b/titles/idac/__init__.py @@ -1,11 +1,12 @@ +from titles.idac.index import IDACServlet from titles.idac.const import IDACConstants from titles.idac.database import IDACData -from titles.idac.frontend import IDACFrontend -from titles.idac.index import IDACServlet from titles.idac.read import IDACReader +from titles.idac.frontend import IDACFrontend index = IDACServlet database = IDACData reader = IDACReader frontend = IDACFrontend game_codes = [IDACConstants.GAME_CODE] +current_schema_version = 1 diff --git a/titles/idac/const.py b/titles/idac/const.py index 0cfb645..cfae20e 100644 --- a/titles/idac/const.py +++ b/titles/idac/const.py @@ -1,4 +1,4 @@ -class IDACConstants: +class IDACConstants(): GAME_CODE = "SDGT" CONFIG_NAME = "idac.yaml" diff --git a/titles/idac/data/create_delivery_images.py b/titles/idac/data/create_delivery_images.py index 199ebbc..ac37925 100644 --- a/titles/idac/data/create_delivery_images.py +++ b/titles/idac/data/create_delivery_images.py @@ -1,5 +1,5 @@ -import hashlib import os +import hashlib def prepare_images(image_folder="titles/idac/data/images"): diff --git a/titles/idac/database.py b/titles/idac/database.py index 9de6250..dac4556 100644 --- a/titles/idac/database.py +++ b/titles/idac/database.py @@ -1,7 +1,7 @@ -from core.config import CoreConfig from core.data import Data -from titles.idac.schema.item import IDACItemData +from core.config import CoreConfig from titles.idac.schema.profile import IDACProfileData +from titles.idac.schema.item import IDACItemData class IDACData(Data): diff --git a/titles/idac/echo.py b/titles/idac/echo.py index 32559fc..08e5526 100644 --- a/titles/idac/echo.py +++ b/titles/idac/echo.py @@ -1,5 +1,7 @@ import logging import socket + +from twisted.internet.protocol import DatagramProtocol from socketserver import BaseRequestHandler, TCPServer from typing import Tuple @@ -8,13 +10,19 @@ from titles.idac.config import IDACConfig from titles.idac.database import IDACData -class IDACEchoUDP: - def connection_made(self, transport): - self.transport = transport +class IDACEchoUDP(DatagramProtocol): + def __init__(self, cfg: CoreConfig, game_cfg: IDACConfig, port: int) -> None: + super().__init__() + self.port = port + self.core_config = cfg + self.game_config = game_cfg + self.logger = logging.getLogger("idac") - def datagram_received(self, data, addr): - logging.getLogger("idz").debug(f"Received echo from {addr}") - self.transport.sendto(data, addr) + def datagramReceived(self, data, addr): + self.logger.info( + f"UDP Ping from from {addr[0]}:{addr[1]} -> {self.port} - {data.hex()}" + ) + self.transport.write(data, addr) class IDACEchoTCP(BaseRequestHandler): diff --git a/titles/idac/frontend.py b/titles/idac/frontend.py index cb2fb33..78abae8 100644 --- a/titles/idac/frontend.py +++ b/titles/idac/frontend.py @@ -1,19 +1,18 @@ import json -from os import path -from typing import List - -import jinja2 import yaml +import jinja2 +from os import path +from twisted.web.util import redirectTo +from twisted.web.http import Request +from twisted.web.server import Session + +from core.frontend import FE_Base, IUserSession from core.config import CoreConfig -from core.frontend import FE_Base, UserSession -from starlette.requests import Request -from starlette.responses import RedirectResponse, Response -from starlette.routing import Route +from titles.idac.database import IDACData +from titles.idac.schema.profile import * +from titles.idac.schema.item import * from titles.idac.config import IDACConfig from titles.idac.const import IDACConstants -from titles.idac.database import IDACData -from titles.idac.schema.item import * -from titles.idac.schema.profile import * class IDACFrontend(FE_Base): @@ -27,8 +26,7 @@ class IDACFrontend(FE_Base): self.game_cfg.update( yaml.safe_load(open(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}")) ) - # self.nav_name = "頭文字D THE ARCADE" - self.nav_name = "IDAC" + self.nav_name = "頭文字D THE ARCADE" # TODO: Add version list self.version = IDACConstants.VER_IDAC_SEASON_2 @@ -39,10 +37,7 @@ class IDACFrontend(FE_Base): 34: "full_tune_fragments", } - def get_routes(self) -> List[Route]: - return [Route("/", self.render_GET)] - - async def generate_all_tables_json(self, user_id: int): + def generate_all_tables_json(self, user_id: int): json_export = {} idac_tables = { @@ -65,7 +60,7 @@ class IDACFrontend(FE_Base): theory_running, vs_info, stamp, - timetrial_event, + timetrial_event } for table in idac_tables: @@ -78,7 +73,7 @@ class IDACFrontend(FE_Base): sql = sql.where(table.c.version == self.version) # lol use the profile connection for items, dirty hack - result = await self.data.profile.execute(sql) + result = self.data.profile.execute(sql) data_list = result.fetchall() # add the list to the json export with the correct table name @@ -91,55 +86,57 @@ class IDACFrontend(FE_Base): return json.dumps(json_export, indent=4, default=str, ensure_ascii=False) - async def render_GET(self, request: Request) -> bytes: - uri: str = request.url.path + def render_GET(self, request: Request) -> bytes: + uri: str = request.uri.decode() template = self.environment.get_template( - "titles/idac/templates/idac_index.jinja" + "titles/idac/frontend/idac_index.jinja" ) - usr_sesh = self.validate_session(request) - if not usr_sesh: - usr_sesh = UserSession() - user_id = usr_sesh.user_id + sesh: Session = request.getSession() + usr_sesh = IUserSession(sesh) + user_id = usr_sesh.userId # user_id = usr_sesh.user_id # profile export if uri.startswith("/game/idac/export"): if user_id == 0: - return RedirectResponse(b"/game/idac", request) + return redirectTo(b"/game/idac", request) # set the file name, content type and size to download the json - content = await self.generate_all_tables_json(user_id).encode("utf-8") + content = self.generate_all_tables_json(user_id).encode("utf-8") + request.responseHeaders.addRawHeader( + b"content-type", b"application/octet-stream" + ) + request.responseHeaders.addRawHeader( + b"content-disposition", b"attachment; filename=idac_profile.json" + ) + request.responseHeaders.addRawHeader( + b"content-length", str(len(content)).encode("utf-8") + ) self.logger.info(f"User {user_id} exported their IDAC data") - return Response( - content, - 200, - {"content-disposition": "attachment; filename=idac_profile.json"}, - "application/octet-stream", - ) + return content profile_data, tickets, rank = None, None, None if user_id > 0: - profile_data = await self.data.profile.get_profile(user_id, self.version) - ticket_data = await self.data.item.get_tickets(user_id) - rank = await self.data.profile.get_profile_rank(user_id, self.version) + profile_data = self.data.profile.get_profile(user_id, self.version) + ticket_data = self.data.item.get_tickets(user_id) + rank = self.data.profile.get_profile_rank(user_id, self.version) - if ticket_data: - tickets = { - self.ticket_names[ticket["ticket_id"]]: ticket["ticket_cnt"] - for ticket in ticket_data - } + tickets = { + self.ticket_names[ticket["ticket_id"]]: ticket["ticket_cnt"] + for ticket in ticket_data + } - return Response( - template.render( - title=f"{self.core_config.server.name} | {self.nav_name}", - game_list=self.environment.globals["game_list"], - profile=profile_data, - tickets=tickets, - rank=rank, - sesh=vars(usr_sesh), - active_page="idac", - ), - media_type="text/html; charset=utf-8", - ) + return template.render( + title=f"{self.core_config.server.name} | {self.nav_name}", + game_list=self.environment.globals["game_list"], + profile=profile_data, + tickets=tickets, + rank=rank, + sesh=vars(usr_sesh), + active_page="idac", + ).encode("utf-16") + + def render_POST(self, request: Request) -> bytes: + pass diff --git a/titles/idac/index.py b/titles/idac/index.py index e50f29d..7daedae 100644 --- a/titles/idac/index.py +++ b/titles/idac/index.py @@ -1,27 +1,28 @@ -import asyncio import json -import logging import traceback -from logging.handlers import TimedRotatingFileHandler +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 -import coloredlogs -import yaml from core.config import CoreConfig -from core.title import BaseServlet, JSONResponseNoASCII from core.utils import Utils -from starlette.requests import Request -from starlette.responses import JSONResponse -from starlette.routing import Route 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.season2 import IDACSeason2 +from titles.idac.matching import IDACMatching -class IDACServlet(BaseServlet): +class IDACServlet: def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: self.core_cfg = core_cfg self.game_cfg = IDACConfig() @@ -32,7 +33,7 @@ class IDACServlet(BaseServlet): self.versions = [ IDACBase(core_cfg, self.game_cfg), - IDACSeason2(core_cfg, self.game_cfg), + IDACSeason2(core_cfg, self.game_cfg) ] self.logger = logging.getLogger("idac") @@ -59,9 +60,7 @@ class IDACServlet(BaseServlet): ) @classmethod - def is_game_enabled( - cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str - ) -> bool: + 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}"): @@ -71,45 +70,33 @@ class IDACServlet(BaseServlet): if not game_cfg.server.enable: return False - + return True - def get_routes(self) -> List[Route]: - return [ - Route( - "/{version:int}/initiald/{category:str}/{endpoint:str}", - self.render_POST, - methods=["POST"], - ), - Route( - "/{version:int}/initiald-matching/{endpoint:str}", - self.render_matching, - methods=["POST"], - ), - ] + 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 "" - ) + 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.server.hostname}{t_port}/{game_ver}/", + f"http://{self.core_cfg.title.hostname}{t_port}/{game_code}/{game_ver}/", ) - async def render_POST(self, request: Request) -> bytes: - req_raw = await request.body() + def render_POST(self, request: Request, game_code: int, matchers: Dict) -> bytes: + req_raw = request.content.getvalue() internal_ver = 0 - version: int = request.path_params.get("version") - category: str = request.path_params.get("category") - endpoint: str = request.path_params.get("endpoint") + 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 @@ -117,7 +104,7 @@ class IDACServlet(BaseServlet): elif version >= 140 and version < 171: # IDAC Season 2 internal_ver = IDACConstants.VER_IDAC_SEASON_2 - header_application = self.decode_header(request.headers.get("application", "")) + header_application = self.decode_header(request.getAllHeaders()) req_data = json.loads(req_raw) @@ -132,57 +119,27 @@ class IDACServlet(BaseServlet): if not hasattr(self.versions[internal_ver], func_to_find): self.logger.warning(f"Unhandled v{version} request {endpoint}") - return JSONResponse('{"status_code": "0"}') + return '{"status_code": "0"}'.encode("utf-8") resp = None try: handler = getattr(self.versions[internal_ver], func_to_find) - resp = await handler(req_data, header_application) + 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 JSONResponse('{"status_code": "0"}') + return '{"status_code": "0"}'.encode("utf-8") if resp is None: resp = {"status_code": "0"} self.logger.debug(f"Response {resp}") - return JSONResponseNoASCII(resp) + return json.dumps(resp, ensure_ascii=False).encode("utf-8") - async def render_matching(self, request: Request): - url: str = request.path_params.get("endpoint") - ver: int = request.path_params.get("version") - client_ip = Utils.get_ip_addr(request) - req_data = await request.json() - header_application = self.decode_header(request.headers.get("application", "")) - user_id = int(header_application["session"]) - # self.getMatchingStatus(user_id) - - self.logger.info(f"IDAC Matching request from {client_ip}: {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 {client_ip} 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 JSONResponseNoASCII(resp) - - def decode_header(self, app: str) -> Dict: + def decode_header(self, data: Dict) -> Dict: + app: str = data[b"application"].decode() ret = {} for x in app.split(", "): @@ -193,17 +150,18 @@ class IDACServlet(BaseServlet): def setup(self): if self.game_cfg.server.enable: - loop = asyncio.get_running_loop() - asyncio.create_task( - loop.create_datagram_endpoint( - lambda: IDACEchoUDP(), - local_addr=( - self.core_cfg.server.listen_address, - self.game_cfg.server.echo1, - ), - ) - ) + 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))) - self.logger.info( - f"Matching listening on {self.game_cfg.server.matching} with echo on {self.game_cfg.server.echo1}" + 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/read.py b/titles/idac/read.py index 4bd1f34..8798e9b 100644 --- a/titles/idac/read.py +++ b/titles/idac/read.py @@ -1,14 +1,15 @@ import json +import logging import os -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional -from core.config import CoreConfig -from core.data import Data 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.item import * from titles.idac.schema.profile import * +from titles.idac.schema.item import * class IDACReader(BaseReader): @@ -32,7 +33,7 @@ class IDACReader(BaseReader): self.logger.error(f"Invalid Initial D THE ARCADE version {version}") exit(1) - async def read(self) -> None: + def read(self) -> None: if self.bin_dir is None and self.opt_dir is None: self.logger.error( ( @@ -58,9 +59,9 @@ class IDACReader(BaseReader): ) exit(1) - await self.read_idac_profile(self.opt_dir) + self.read_idac_profile(self.opt_dir) - async def read_idac_profile(self, file_path: str) -> None: + 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 @@ -82,21 +83,19 @@ class IDACReader(BaseReader): # 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): " - ) + 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 = await self.data.user.create_user() + 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 = await self.data.card.create_card(user_id, access_code) + card_id = self.data.card.create_card(user_id, access_code) if card_id is None: self.logger.error("Failed to register card!") @@ -145,11 +144,13 @@ class IDACReader(BaseReader): if "version" in table.c: data["version"] = self.version - sql = insert(table).values(**data) + sql = insert(table).values( + **data + ) # lol use the profile connection for items, dirty hack conflict = sql.on_duplicate_key_update(**data) - result = await self.data.profile.execute(conflict) + result = self.data.profile.execute(conflict) if result is None: self.logger.error(f"Failed to insert data into table {name}") diff --git a/titles/idac/schema/item.py b/titles/idac/schema/item.py index 4979fd2..80ee7ba 100644 --- a/titles/idac/schema/item.py +++ b/titles/idac/schema/item.py @@ -1,18 +1,19 @@ -from typing import Dict, List, Optional - -from core.data.schema import BaseData, metadata +from typing import Dict, Optional, List from sqlalchemy import ( - Column, Table, + Column, UniqueConstraint, + PrimaryKeyConstraint, and_, update, ) -from sqlalchemy.dialects.mysql import insert -from sqlalchemy.engine import Row +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.types import JSON, TIMESTAMP, Integer, String +from sqlalchemy.dialects.mysql import insert + +from core.data.schema import BaseData, metadata car = Table( "idac_user_car", @@ -296,9 +297,7 @@ timetrial_event = Table( class IDACItemData(BaseData): - async def get_random_user_car( - self, aime_id: int, version: int - ) -> Optional[List[Row]]: + 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)) @@ -306,20 +305,20 @@ class IDACItemData(BaseData): .limit(1) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_random_car(self, version: int) -> Optional[List[Row]]: + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_car( + def get_car( self, aime_id: int, version: int, style_car_id: int ) -> Optional[List[Row]]: sql = select(car).where( @@ -330,12 +329,12 @@ class IDACItemData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_cars( + def get_cars( self, version: int, aime_id: int, only_pickup: bool = False ) -> Optional[List[Row]]: if only_pickup: @@ -351,108 +350,106 @@ class IDACItemData(BaseData): and_(car.c.user == aime_id, car.c.version == version) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_ticket(self, aime_id: int, ticket_id: int) -> Optional[Row]: + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_tickets(self, aime_id: int) -> Optional[List[Row]]: + def get_tickets(self, aime_id: int) -> Optional[List[Row]]: sql = select(ticket).where(ticket.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_story(self, aime_id: int, chapter_id: int) -> Optional[Row]: + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_stories(self, aime_id: int) -> Optional[List[Row]]: + def get_stories(self, aime_id: int) -> Optional[List[Row]]: sql = select(story).where(story.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_story_episodes( - self, aime_id: int, chapter_id: int - ) -> Optional[List[Row]]: + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_story_episode(self, aime_id: int, episode_id: int) -> Optional[Row]: + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_story_episode_difficulties( + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_courses(self, aime_id: int) -> Optional[List[Row]]: + def get_courses(self, aime_id: int) -> Optional[List[Row]]: sql = select(course).where(course.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_course(self, aime_id: int, course_id: int) -> Optional[Row]: + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_time_trial_courses(self, version: int) -> Optional[List[Row]]: + def get_time_trial_courses(self, version: int) -> Optional[List[Row]]: sql = select(trial.c.course_id).where(trial.c.version == version).distinct() - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_time_trial_user_best_time_by_course_car( + 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( @@ -464,12 +461,12 @@ class IDACItemData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_time_trial_user_best_courses( + 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 @@ -494,28 +491,31 @@ class IDACItemData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_time_trial_best_cars_by_course( + 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, + 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( @@ -527,12 +527,12 @@ class IDACItemData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_time_trial_ranking_by_course( + def get_time_trial_ranking_by_course( self, version: int, course_id: int, @@ -568,12 +568,12 @@ class IDACItemData(BaseData): if limit is not None: sql = sql.limit(limit) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_time_trial_best_ranking_by_course( + def get_time_trial_best_ranking_by_course( self, version: int, aime_id: int, course_id: int ) -> Optional[Row]: sql = ( @@ -589,12 +589,12 @@ class IDACItemData(BaseData): .limit(1) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_challenge( + def get_challenge( self, aime_id: int, vs_type: int, play_difficulty: int ) -> Optional[Row]: sql = select(challenge).where( @@ -605,20 +605,20 @@ class IDACItemData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_challenges(self, aime_id: int) -> Optional[List[Row]]: + def get_challenges(self, aime_id: int) -> Optional[List[Row]]: sql = select(challenge).where(challenge.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_best_challenges_by_vs_type( + def get_best_challenges_by_vs_type( self, aime_id: int, story_type: int = 4 ) -> Optional[List[Row]]: subquery = ( @@ -653,20 +653,20 @@ class IDACItemData(BaseData): .order_by(challenge.c.vs_type) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_theory_courses(self, aime_id: int) -> Optional[List[Row]]: + def get_theory_courses(self, aime_id: int) -> Optional[List[Row]]: sql = select(theory_course).where(theory_course.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_theory_course_by_powerhouse_lv( + def get_theory_course_by_powerhouse_lv( self, aime_id: int, course_id: int, powerhouse_lv: int, count: int = 3 ) -> Optional[List[Row]]: sql = ( @@ -682,42 +682,40 @@ class IDACItemData(BaseData): .limit(count) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_theory_course( - self, aime_id: int, course_id: int - ) -> Optional[List[Row]]: + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_theory_partners(self, aime_id: int) -> Optional[List[Row]]: + def get_theory_partners(self, aime_id: int) -> Optional[List[Row]]: sql = select(theory_partner).where(theory_partner.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_theory_running(self, aime_id: int) -> Optional[List[Row]]: + def get_theory_running(self, aime_id: int) -> Optional[List[Row]]: sql = select(theory_running).where(theory_running.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_theory_running_by_course( + def get_theory_running_by_course( self, aime_id: int, course_id: int ) -> Optional[Row]: sql = select(theory_running).where( @@ -727,34 +725,32 @@ class IDACItemData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_vs_infos(self, aime_id: int) -> Optional[List[Row]]: + def get_vs_infos(self, aime_id: int) -> Optional[List[Row]]: sql = select(vs_info).where(vs_info.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_stamps(self, aime_id: int) -> Optional[List[Row]]: + def get_stamps(self, aime_id: int) -> Optional[List[Row]]: sql = select(stamp).where( and_( stamp.c.user == aime_id, ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_timetrial_event( - self, aime_id: int, timetrial_event_id: int - ) -> Optional[Row]: + 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, @@ -762,51 +758,49 @@ class IDACItemData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def put_car( - self, aime_id: int, version: int, car_data: Dict - ) -> Optional[int]: + 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 = await self.execute(conflict) + 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 - async def put_ticket(self, aime_id: int, ticket_data: Dict) -> Optional[int]: + 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 = await self.execute(conflict) + 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 - async def put_story(self, aime_id: int, story_data: Dict) -> Optional[int]: + 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 = await self.execute(conflict) + 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 - async def put_story_episode_play_status( + def put_story_episode_play_status( self, aime_id: int, chapter_id: int, play_status: int = 1 ) -> Optional[int]: sql = ( @@ -815,7 +809,7 @@ class IDACItemData(BaseData): .values(play_status=play_status) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: self.logger.warn( f"put_story_episode_play_status: Failed to update! aime_id: {aime_id}" @@ -823,7 +817,7 @@ class IDACItemData(BaseData): return None return result.lastrowid - async def put_story_episode( + def put_story_episode( self, aime_id: int, chapter_id: int, episode_data: Dict ) -> Optional[int]: episode_data["user"] = aime_id @@ -831,14 +825,14 @@ class IDACItemData(BaseData): sql = insert(episode).values(**episode_data) conflict = sql.on_duplicate_key_update(**episode_data) - result = await self.execute(conflict) + 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 - async def put_story_episode_difficulty( + def put_story_episode_difficulty( self, aime_id: int, episode_id: int, difficulty_data: Dict ) -> Optional[int]: difficulty_data["user"] = aime_id @@ -846,7 +840,7 @@ class IDACItemData(BaseData): sql = insert(difficulty).values(**difficulty_data) conflict = sql.on_duplicate_key_update(**difficulty_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warn( @@ -855,19 +849,19 @@ class IDACItemData(BaseData): return None return result.lastrowid - async def put_course(self, aime_id: int, course_data: Dict) -> Optional[int]: + 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 = await self.execute(conflict) + 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 - async def put_time_trial( + def put_time_trial( self, version: int, aime_id: int, time_trial_data: Dict ) -> Optional[int]: time_trial_data["user"] = aime_id @@ -875,47 +869,47 @@ class IDACItemData(BaseData): sql = insert(trial).values(**time_trial_data) conflict = sql.on_duplicate_key_update(**time_trial_data) - result = await self.execute(conflict) + 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 - async def put_challenge(self, aime_id: int, challenge_data: Dict) -> Optional[int]: + 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 = await self.execute(conflict) + 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 - async def put_theory_course( + 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 = await self.execute(conflict) + 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 - async def put_theory_partner( + 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 = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warn( @@ -924,14 +918,14 @@ class IDACItemData(BaseData): return None return result.lastrowid - async def put_theory_running( + 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 = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warn( @@ -940,31 +934,35 @@ class IDACItemData(BaseData): return None return result.lastrowid - async def put_vs_info(self, aime_id: int, vs_info_data: Dict) -> Optional[int]: + 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 = await self.execute(conflict) + 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 - async def put_stamp(self, aime_id: int, stamp_data: Dict) -> Optional[int]: + 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 = await self.execute(conflict) + result = self.execute(conflict) if result is None: - self.logger.warn(f"putstamp: Failed to update! aime_id: {aime_id}") + self.logger.warn( + f"putstamp: Failed to update! aime_id: {aime_id}" + ) return None return result.lastrowid - async def put_timetrial_event( + def put_timetrial_event( self, aime_id: int, time_trial_event_id: int, point: int ) -> Optional[int]: timetrial_event_data = { @@ -975,7 +973,7 @@ class IDACItemData(BaseData): sql = insert(timetrial_event).values(**timetrial_event_data) conflict = sql.on_duplicate_key_update(**timetrial_event_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warn( diff --git a/titles/idac/schema/profile.py b/titles/idac/schema/profile.py index 7f362f7..5e363ca 100644 --- a/titles/idac/schema/profile.py +++ b/titles/idac/schema/profile.py @@ -1,14 +1,14 @@ -from typing import Dict, Optional - -from core.config import CoreConfig -from core.data.schema import BaseData, metadata -from sqlalchemy import Column, Table, UniqueConstraint, and_ -from sqlalchemy.dialects.mysql import insert -from sqlalchemy.engine import Row +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.types import TIMESTAMP, Integer, String +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", @@ -253,7 +253,7 @@ class IDACProfileData(BaseData): ) self.date_time_format_short = "%Y-%m-%d" - async def get_profile(self, aime_id: int, version: int) -> Optional[Row]: + def get_profile(self, aime_id: int, version: int) -> Optional[Row]: sql = select(profile).where( and_( profile.c.user == aime_id, @@ -261,12 +261,12 @@ class IDACProfileData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_different_random_profiles( + def get_different_random_profiles( self, aime_id: int, version: int, count: int = 9 ) -> Optional[Row]: sql = ( @@ -281,36 +281,36 @@ class IDACProfileData(BaseData): .limit(count) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_profile_config(self, aime_id: int) -> Optional[Row]: + def get_profile_config(self, aime_id: int) -> Optional[Row]: sql = select(config).where( and_( config.c.user == aime_id, ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_profile_avatar(self, aime_id: int) -> Optional[Row]: + def get_profile_avatar(self, aime_id: int) -> Optional[Row]: sql = select(avatar).where( and_( avatar.c.user == aime_id, ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_profile_rank(self, aime_id: int, version: int) -> Optional[Row]: + def get_profile_rank(self, aime_id: int, version: int) -> Optional[Row]: sql = select(rank).where( and_( rank.c.user == aime_id, @@ -318,12 +318,12 @@ class IDACProfileData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_profile_stock(self, aime_id: int, version: int) -> Optional[Row]: + def get_profile_stock(self, aime_id: int, version: int) -> Optional[Row]: sql = select(stock).where( and_( stock.c.user == aime_id, @@ -331,12 +331,12 @@ class IDACProfileData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_profile_theory(self, aime_id: int, version: int) -> Optional[Row]: + def get_profile_theory(self, aime_id: int, version: int) -> Optional[Row]: sql = select(theory).where( and_( theory.c.user == aime_id, @@ -344,12 +344,12 @@ class IDACProfileData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def put_profile( + def put_profile( self, aime_id: int, version: int, profile_data: Dict ) -> Optional[int]: profile_data["user"] = aime_id @@ -357,21 +357,19 @@ class IDACProfileData(BaseData): sql = insert(profile).values(**profile_data) conflict = sql.on_duplicate_key_update(**profile_data) - result = await self.execute(conflict) + 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 - async def put_profile_config( - self, aime_id: int, config_data: Dict - ) -> Optional[int]: + 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 = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warn( @@ -380,14 +378,12 @@ class IDACProfileData(BaseData): return None return result.lastrowid - async def put_profile_avatar( - self, aime_id: int, avatar_data: Dict - ) -> Optional[int]: + 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 = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warn( @@ -396,7 +392,7 @@ class IDACProfileData(BaseData): return None return result.lastrowid - async def put_profile_rank( + def put_profile_rank( self, aime_id: int, version: int, rank_data: Dict ) -> Optional[int]: rank_data["user"] = aime_id @@ -404,14 +400,14 @@ class IDACProfileData(BaseData): sql = insert(rank).values(**rank_data) conflict = sql.on_duplicate_key_update(**rank_data) - result = await self.execute(conflict) + 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 - async def put_profile_stock( + def put_profile_stock( self, aime_id: int, version: int, stock_data: Dict ) -> Optional[int]: stock_data["user"] = aime_id @@ -419,14 +415,14 @@ class IDACProfileData(BaseData): sql = insert(stock).values(**stock_data) conflict = sql.on_duplicate_key_update(**stock_data) - result = await self.execute(conflict) + 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 - async def put_profile_theory( + def put_profile_theory( self, aime_id: int, version: int, theory_data: Dict ) -> Optional[int]: theory_data["user"] = aime_id @@ -434,7 +430,7 @@ class IDACProfileData(BaseData): sql = insert(theory).values(**theory_data) conflict = sql.on_duplicate_key_update(**theory_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warn( diff --git a/titles/idac/season2.py b/titles/idac/season2.py index 8cfea44..ca57392 100644 --- a/titles/idac/season2.py +++ b/titles/idac/season2.py @@ -1,14 +1,14 @@ -import json -import os from datetime import datetime, timedelta +import os from random import choice -from typing import Dict, List +from typing import Any, Dict, List +import json +import logging from core.config import CoreConfig -from core.utils import Utils -from titles.idac.base import IDACBase -from titles.idac.config import IDACConfig from titles.idac.const import IDACConstants +from titles.idac.config import IDACConfig +from titles.idac.base import IDACBase class IDACSeason2(IDACBase): @@ -52,7 +52,7 @@ class IDACSeason2(IDACBase): "timetrial_event_id" ) - async def handle_alive_get_request(self, data: Dict, headers: Dict): + def handle_alive_get_request(self, data: Dict, headers: Dict): return { "status_code": "0", # 1 = success, 0 = failed @@ -75,7 +75,7 @@ class IDACSeason2(IDACBase): output[key] = value return output - async def handle_boot_getconfigdata_request(self, data: Dict, headers: Dict): + def handle_boot_getconfigdata_request(self, data: Dict, headers: Dict): """ category: 1 = D Coin @@ -108,10 +108,10 @@ class IDACSeason2(IDACBase): version = headers["device_version"] ver_str = version.replace(".", "")[:3] - if self.core_cfg.server.is_using_proxy: - domain_api_game = f"http://{self.core_cfg.server.hostname}/{ver_str}/" + 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.server.hostname}:{Utils.get_title_port(self.core_cfg)}/{ver_str}/" + domain_api_game = f"http://{self.core_cfg.title.hostname}/SDGT/{ver_str}/" return { "status_code": "0", @@ -136,10 +136,10 @@ class IDACSeason2(IDACBase): "server_maintenance_end_hour": 0, "server_maintenance_end_minutes": 0, "domain_api_game": domain_api_game, - "domain_matching": f"{domain_api_game}initiald-matching/", - "domain_echo1": f"{self.core_cfg.server.hostname}:{self.game_config.server.echo1}", - "domain_echo2": f"{self.core_cfg.server.hostname}:{self.game_config.server.echo1}", - "domain_ping": f"{self.core_cfg.server.hostname}", + "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": [ { @@ -179,7 +179,7 @@ class IDACSeason2(IDACBase): "reward_upper_limit": 180, "reward_lower_limit": 180, "reward": [{"reward_category": 21, "reward_type": 462}], - }, + } ], "rank": [], "point": [], @@ -296,10 +296,10 @@ class IDACSeason2(IDACBase): "timetrial_event_data": self.timetrial_event, } - async def handle_boot_bookkeep_request(self, data: Dict, headers: Dict): + def handle_boot_bookkeep_request(self, data: Dict, headers: Dict): pass - async def handle_boot_getgachadata_request(self, data: Dict, headers: Dict): + def handle_boot_getgachadata_request(self, data: Dict, headers: Dict): """ Reward category types: 9: Face @@ -349,7 +349,7 @@ class IDACSeason2(IDACBase): return avatar_gacha_data - async def handle_boot_gettimereleasedata_request(self, data: Dict, headers: Dict): + 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?) @@ -384,12 +384,12 @@ class IDACSeason2(IDACBase): return time_release_data - async def handle_advertise_getrankingdata_request(self, data: Dict, headers: Dict): + 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 = await self.data.item.get_time_trial_ranking_by_course( + ranking = self.data.item.get_time_trial_ranking_by_course( self.version, course_id ) ranking_data = [] @@ -397,8 +397,8 @@ class IDACSeason2(IDACBase): user_id = rank["user"] # get the username, country and store from the profile - profile = await self.data.profile.get_profile(user_id, self.version) - arcade = await self.data.arcade.get_arcade(profile["store"]) + profile = self.data.profile.get_profile(user_id, self.version) + arcade = self.data.arcade.get_arcade(profile["store"]) if arcade is None: arcade = {} @@ -443,17 +443,17 @@ class IDACSeason2(IDACBase): "rank_management_flag": 0, } - async def handle_login_checklock_request(self, data: Dict, headers: Dict): + 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 == await self.data.card.get_user_id_from_card(access_code): + if user_id == self.data.card.get_user_id_from_card(access_code): lock_result = 1 # check if an IDAC profile already exists - p = await self.data.profile.get_profile(user_id, self.version) + p = self.data.profile.get_profile(user_id, self.version) is_new_player = 1 if p is None else 0 else: lock_result = 0 @@ -473,35 +473,35 @@ class IDACSeason2(IDACBase): "server_status": 1, } - async def handle_login_unlock_request(self, data: Dict, headers: Dict): + def handle_login_unlock_request(self, data: Dict, headers: Dict): return { "status_code": "0", "lock_result": 1, } - async def handle_login_relock_request(self, data: Dict, headers: Dict): + def handle_login_relock_request(self, data: Dict, headers: Dict): return { "status_code": "0", "lock_result": 1, "lock_date": int(datetime.now().timestamp()), } - async def handle_login_guestplay_request(self, data: Dict, headers: Dict): + def handle_login_guestplay_request(self, data: Dict, headers: Dict): # TODO pass - async def _generate_story_data(self, user_id: int) -> Dict: - stories = await self.data.item.get_stories(user_id) + 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 = await self.data.item.get_story_episodes(user_id, chapter_id) + episodes = self.data.item.get_story_episodes(user_id, chapter_id) episode_data = [] for e in episodes: episode_id = e["episode"] - difficulties = await self.data.item.get_story_episode_difficulties( + difficulties = self.data.item.get_story_episode_difficulties( user_id, episode_id ) @@ -536,11 +536,9 @@ class IDACSeason2(IDACBase): return story_data - async def _generate_special_data(self, user_id: int) -> Dict: + def _generate_special_data(self, user_id: int) -> Dict: # 4 = special mode - specials = await self.data.item.get_best_challenges_by_vs_type( - user_id, story_type=4 - ) + specials = self.data.item.get_best_challenges_by_vs_type(user_id, story_type=4) special_data = [] for s in specials: @@ -557,9 +555,9 @@ class IDACSeason2(IDACBase): return special_data - async def _generate_challenge_data(self, user_id: int) -> Dict: + def _generate_challenge_data(self, user_id: int) -> Dict: # challenge mode (Bunta challenge only right now) - challenges = await self.data.item.get_best_challenges_by_vs_type( + challenges = self.data.item.get_best_challenges_by_vs_type( user_id, story_type=3 ) @@ -579,24 +577,24 @@ class IDACSeason2(IDACBase): return challenge_data - async def _save_stock_data(self, user_id: int, stock_data: Dict): + 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: - await self.data.profile.put_profile_stock( + self.data.profile.put_profile_stock( user_id, self.version, updated_stock_data ) - async def handle_user_getdata_request(self, data: Dict, headers: Dict): + 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 = await self.data.profile.get_profile(user_id, self.version) + p = self.data.profile.get_profile(user_id, self.version) user_data = p._asdict() - arcade = await self.data.arcade.get_arcade(user_data["store"]) + arcade = self.data.arcade.get_arcade(user_data["store"]) del user_data["id"] del user_data["user"] @@ -609,7 +607,7 @@ class IDACSeason2(IDACBase): user_data["create_date"] = int(user_data["create_date"].timestamp()) # get the user's rank - r = await self.data.profile.get_profile_rank(user_id, self.version) + r = self.data.profile.get_profile_rank(user_id, self.version) rank_data = r._asdict() del rank_data["id"] del rank_data["user"] @@ -619,27 +617,27 @@ class IDACSeason2(IDACBase): user_data["mode_rank_data"] = rank_data # get the user's avatar - a = await self.data.profile.get_profile_avatar(user_id) + 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 = await self.data.profile.get_profile_stock(user_id, self.version) + 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 = await self.data.profile.get_profile_config(user_id) + 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 = await self.data.item.get_tickets(user_id) + tickets: list = self.data.item.get_tickets(user_id) """ ticket_id: @@ -659,7 +657,7 @@ class IDACSeason2(IDACBase): ) # get the user's course, required for the "course proeficiency" - courses = await self.data.item.get_courses(user_id) + courses = self.data.item.get_courses(user_id) course_data = [] for course in courses: course_data.append( @@ -674,7 +672,7 @@ class IDACSeason2(IDACBase): # get the profile theory data theory_data = {} - theory = await self.data.profile.get_profile_theory(user_id, self.version) + theory = self.data.profile.get_profile_theory(user_id, self.version) if theory is not None: theory_data = theory._asdict() del theory_data["id"] @@ -683,7 +681,7 @@ class IDACSeason2(IDACBase): # get the users theory course data theory_course_data = [] - theory_courses = await self.data.item.get_theory_courses(user_id) + theory_courses = self.data.item.get_theory_courses(user_id) for course in theory_courses: tmp = course._asdict() del tmp["id"] @@ -694,7 +692,7 @@ class IDACSeason2(IDACBase): # get the users theory partner data theory_partner_data = [] - theory_partners = await self.data.item.get_theory_partners(user_id) + theory_partners = self.data.item.get_theory_partners(user_id) for partner in theory_partners: tmp = partner._asdict() del tmp["id"] @@ -704,7 +702,7 @@ class IDACSeason2(IDACBase): # get the users theory running pram data theory_running_pram_data = [] - theory_running = await self.data.item.get_theory_running(user_id) + theory_running = self.data.item.get_theory_running(user_id) for running in theory_running: tmp = running._asdict() del tmp["id"] @@ -714,7 +712,7 @@ class IDACSeason2(IDACBase): # get the users vs info data vs_info_data = [] - vs_info = await self.data.item.get_vs_infos(user_id) + vs_info = self.data.item.get_vs_infos(user_id) for vs in vs_info: vs_info_data.append( { @@ -738,7 +736,7 @@ class IDACSeason2(IDACBase): ) # get the user's car - cars = await self.data.item.get_cars(self.version, user_id, only_pickup=True) + cars = self.data.item.get_cars(self.version, user_id, only_pickup=True) fulltune_count = 0 total_car_parts_count = 0 car_data = [] @@ -762,7 +760,7 @@ class IDACSeason2(IDACBase): user_data["have_car_cnt"] = len(car_data) # get the user's play stamps - stamps = await self.data.item.get_stamps(user_id) + stamps = self.data.item.get_stamps(user_id) stamp_event_data = [] for stamp in stamps: tmp = stamp._asdict() @@ -791,7 +789,7 @@ class IDACSeason2(IDACBase): tmp["weekly_bonus"] = 0 # update the play stamp in the database - await self.data.item.put_stamp(user_id, tmp) + self.data.item.put_stamp(user_id, tmp) del tmp["create_date_daily"] del tmp["create_date_weekly"] @@ -799,9 +797,7 @@ class IDACSeason2(IDACBase): # get the user's timetrial event data timetrial_event_data = {} - timetrial = await self.data.item.get_timetrial_event( - user_id, self.timetrial_event_id - ) + 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"], @@ -813,7 +809,7 @@ class IDACSeason2(IDACBase): "user_base_data": user_data, "avatar_data": avatar_data, "pick_up_car_data": car_data, - "story_data": await self._generate_story_data(user_id), + "story_data": self._generate_story_data(user_id), "vsinfo_data": vs_info_data, "stock_data": stock_data, "mission_data": { @@ -881,25 +877,21 @@ class IDACSeason2(IDACBase): "theory_course_data": theory_course_data, "theory_partner_data": theory_partner_data, "theory_running_pram_data": theory_running_pram_data, - "special_mode_data": await self._generate_special_data(user_id), - "challenge_mode_data": await self._generate_challenge_data(user_id), + "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}, } - async def handle_timetrial_getbestrecordpreta_request( - self, data: Dict, headers: Dict - ): + 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 = await self.data.item.get_time_trial_user_best_courses( - self.version, user_id - ) + courses = self.data.item.get_time_trial_user_best_courses(self.version, user_id) for course in courses: course_mybest_data.append( { @@ -927,10 +919,10 @@ class IDACSeason2(IDACBase): ) course_pickup_car_best_data = [] - courses = await self.data.item.get_time_trial_courses(self.version) + courses = self.data.item.get_time_trial_courses(self.version) for course in courses: car_list = [] - best_cars = await self.data.item.get_time_trial_best_cars_by_course( + best_cars = self.data.item.get_time_trial_best_cars_by_course( self.version, course["course_id"], user_id ) @@ -967,9 +959,7 @@ class IDACSeason2(IDACBase): "course_pickup_car_best_data": course_pickup_car_best_data, } - async def handle_timetrial_getbestrecordprerace_request( - self, data: Dict, headers: Dict - ): + def handle_timetrial_getbestrecordprerace_request(self, data: Dict, headers: Dict): user_id = headers["session"] course_id = data["course_id"] @@ -978,7 +968,7 @@ class IDACSeason2(IDACBase): style_car_id = car["style_car_id"] # Not sure if this is actually correct - ranking = await self.data.item.get_time_trial_ranking_by_course( + ranking = self.data.item.get_time_trial_ranking_by_course( self.version, course_id ) course_best_data = [] @@ -986,8 +976,8 @@ class IDACSeason2(IDACBase): car_user_id = rank["user"] # get the username, country and store from the profile - profile = await self.data.profile.get_profile(car_user_id, self.version) - arcade = await self.data.arcade.get_arcade(profile["store"]) + profile = self.data.profile.get_profile(car_user_id, self.version) + arcade = self.data.arcade.get_arcade(profile["store"]) if arcade is None: arcade = {} @@ -1016,7 +1006,7 @@ class IDACSeason2(IDACBase): } ) - best_cars = await self.data.item.get_time_trial_best_cars_by_course( + best_cars = self.data.item.get_time_trial_best_cars_by_course( self.version, course_id ) @@ -1024,8 +1014,8 @@ class IDACSeason2(IDACBase): for i, rank in enumerate(best_cars): car_user_id = rank["user"] # get the username, country and store from the profile - profile = await self.data.profile.get_profile(car_user_id, self.version) - arcade = await self.data.arcade.get_arcade(profile["store"]) + profile = self.data.profile.get_profile(car_user_id, self.version) + arcade = self.data.arcade.get_arcade(profile["store"]) if arcade is None: arcade = {} @@ -1060,7 +1050,7 @@ class IDACSeason2(IDACBase): "course_best_data": course_best_data, } - async def handle_user_createaccount_request(self, data: Dict, headers: Dict): + def handle_user_createaccount_request(self, data: Dict, headers: Dict): user_id = headers["session"] car_data: Dict = data.pop("car_obj") @@ -1079,45 +1069,45 @@ class IDACSeason2(IDACBase): data["store"] = headers.get("a_store", 0) data["country"] = headers.get("a_country", 0) data["asset_version"] = headers.get("asset_version", 1) - await self.data.profile.put_profile(user_id, self.version, data) + self.data.profile.put_profile(user_id, self.version, data) # save rank data in database - await self.data.profile.put_profile_rank(user_id, self.version, rank_data) + self.data.profile.put_profile_rank(user_id, self.version, rank_data) # save stock data in database - await self._save_stock_data(user_id, stock_data) + self._save_stock_data(user_id, stock_data) # save tickets in database for ticket in takeover_ticket_list: - await self.data.item.put_ticket(user_id, ticket) + self.data.item.put_ticket(user_id, ticket) config_data["config_id"] = config_data.pop("id") - await self.data.profile.put_profile_config(user_id, config_data) - await self.data.profile.put_profile_avatar(user_id, avatar_data) + 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 - await self.data.item.put_car(user_id, self.version, car_data) + self.data.item.put_car(user_id, self.version, car_data) return {"status_code": "0"} - async def handle_user_updatelogin_request(self, data: Dict, headers: Dict): + def handle_user_updatelogin_request(self, data: Dict, headers: Dict): pass - async def handle_timetrial_getcarbest_request(self, data: Dict, headers: Dict): + def handle_timetrial_getcarbest_request(self, data: Dict, headers: Dict): pass - async def handle_factory_avatargacharesult_request(self, data: Dict, headers: Dict): + 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 - await self._save_stock_data(user_id, stock_data) + self._save_stock_data(user_id, stock_data) # get the user's ticket - tickets: list = await self.data.item.get_tickets(user_id) + tickets: list = self.data.item.get_tickets(user_id) ticket_list = [] for ticket in tickets: # avatar tickets @@ -1128,7 +1118,7 @@ class IDACSeason2(IDACBase): } # update the ticket in the database - await self.data.item.put_ticket(user_id, ticket_data) + self.data.item.put_ticket(user_id, ticket_data) ticket_list.append(ticket_data) continue @@ -1142,15 +1132,15 @@ class IDACSeason2(IDACBase): return {"status_code": "0", "ticket_data": ticket_list} - async def handle_factory_savefavoritecar_request(self, data: Dict, headers: Dict): + 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"]: - await self.data.item.put_car(user_id, self.version, car) + self.data.item.put_car(user_id, self.version, car) for car in data["pickup_off_car_ids"]: - await self.data.item.put_car( + self.data.item.put_car( user_id, self.version, {"style_car_id": car["style_car_id"], "pickup_seq": 0}, @@ -1158,7 +1148,7 @@ class IDACSeason2(IDACBase): return {"status_code": "0"} - async def handle_factory_updatemultiplecustomizeresult_request( + def handle_factory_updatemultiplecustomizeresult_request( self, data: Dict, headers: Dict ): user_id = headers["session"] @@ -1171,17 +1161,15 @@ class IDACSeason2(IDACBase): # save tickets in database for ticket in ticket_data: - await self.data.item.put_ticket(user_id, ticket) + self.data.item.put_ticket(user_id, ticket) for car in car_list: # save car data and car parts in database - await self.data.item.put_car(user_id, self.version, car) + self.data.item.put_car(user_id, self.version, car) return {"status_code": "0"} - async def handle_factory_updatecustomizeresult_request( - self, data: Dict, headers: Dict - ): + def handle_factory_updatecustomizeresult_request(self, data: Dict, headers: Dict): user_id = headers["session"] parts_data: List = data.pop("parts_list") @@ -1189,18 +1177,18 @@ class IDACSeason2(IDACBase): # save tickets in database for ticket in ticket_data: - await self.data.item.put_ticket(user_id, ticket) + self.data.item.put_ticket(user_id, ticket) # save car data in database data["parts_list"] = parts_data - await self.data.item.put_car(user_id, self.version, data) + self.data.item.put_car(user_id, self.version, data) return {"status_code": "0"} - async def handle_factory_getcardata_request(self, data: Dict, headers: Dict): + def handle_factory_getcardata_request(self, data: Dict, headers: Dict): user_id = headers["session"] - cars = await self.data.item.get_cars(self.version, user_id) + cars = self.data.item.get_cars(self.version, user_id) car_data = [] for car in cars: tmp = car._asdict() @@ -1215,10 +1203,10 @@ class IDACSeason2(IDACBase): "car_data": car_data, } - async def handle_factory_renamebefore_request(self, data: Dict, headers: Dict): + def handle_factory_renamebefore_request(self, data: Dict, headers: Dict): pass - async def handle_factory_buycarresult_request(self, data: Dict, headers: Dict): + def handle_factory_buycarresult_request(self, data: Dict, headers: Dict): user_id = headers["session"] parts_data: List = data.pop("parts_list") @@ -1235,7 +1223,7 @@ class IDACSeason2(IDACBase): if car["style_car_id"] == style_car_id: pickup_seq = car["pickup_seq"] else: - await self.data.item.put_car(user_id, self.version, car) + self.data.item.put_car(user_id, self.version, car) data["pickup_seq"] = pickup_seq @@ -1243,7 +1231,7 @@ class IDACSeason2(IDACBase): total_cash = data.pop("total_cash") # save the new cash in database - await self.data.profile.put_profile( + self.data.profile.put_profile( user_id, self.version, {"total_cash": total_cash, "cash": cash} ) @@ -1251,10 +1239,10 @@ class IDACSeason2(IDACBase): use_ticket = data.pop("use_ticket") if use_ticket: # get the user's tickets, full tune ticket id is 25 - ticket = await self.data.item.get_ticket(user_id, ticket_id=25) + ticket = self.data.item.get_ticket(user_id, ticket_id=25) # update the ticket in the database - await self.data.item.put_ticket( + self.data.item.put_ticket( user_id, { "ticket_id": ticket["ticket_id"], @@ -1267,17 +1255,17 @@ class IDACSeason2(IDACBase): # save car data and car parts in database data["parts_list"] = parts_data - await self.data.item.put_car(user_id, self.version, data) + self.data.item.put_car(user_id, self.version, data) for car in pickup_off_list: - await self.data.item.put_car( + 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 = await self.data.item.get_cars(self.version, user_id) + cars = self.data.item.get_cars(self.version, user_id) fulltune_count = 0 total_car_parts_count = 0 for car in cars: @@ -1289,7 +1277,7 @@ class IDACSeason2(IDACBase): # total_car_parts_count += car["total_car_parts_count"] # get the user's ticket - tickets = await self.data.item.get_tickets(user_id) + tickets = self.data.item.get_tickets(user_id) ticket_data = [] for ticket in tickets: ticket_data.append( @@ -1308,60 +1296,54 @@ class IDACSeason2(IDACBase): "car_style_count": [], } - async def handle_factory_renameresult_request(self, data: Dict, headers: Dict): + 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: - await self.data.profile.put_profile(user_id, self.version, data) + self.data.profile.put_profile(user_id, self.version, data) return {"status_code": "0"} - async def handle_factory_updatecustomizeavatar_request( - self, data: Dict, headers: Dict - ): + 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 - await self._save_stock_data(user_id, stock_data) + self._save_stock_data(user_id, stock_data) # save avatar data and avatar parts in database - await self.data.profile.put_profile_avatar(user_id, avatar_data) + self.data.profile.put_profile_avatar(user_id, avatar_data) return {"status_code": "0"} - async def handle_factory_updatecustomizeuser_request( - self, data: Dict, headers: Dict - ): + 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 - await self._save_stock_data(user_id, stock_data) + self._save_stock_data(user_id, stock_data) # update profile data and config in database - await self.data.profile.put_profile(user_id, self.version, data) + self.data.profile.put_profile(user_id, self.version, data) return {"status_code": "0"} - async def handle_user_updatestampinfo_request(self, data: Dict, headers: Dict): + 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: - await self.data.item.put_stamp(user_id, stamp) + self.data.item.put_stamp(user_id, stamp) return {"status_code": "0"} - async def handle_user_updatetimetrialresult_request( - self, data: Dict, headers: Dict - ): + def handle_user_updatetimetrialresult_request(self, data: Dict, headers: Dict): user_id = headers["session"] stock_data: Dict = data.pop("stock_obj") @@ -1374,22 +1356,22 @@ class IDACSeason2(IDACBase): event_point = data.pop("event_point") # save stock data in database - await self._save_stock_data(user_id, stock_data) + self._save_stock_data(user_id, stock_data) # save tickets in database for ticket in ticket_data: - await self.data.item.put_ticket(user_id, ticket) + self.data.item.put_ticket(user_id, ticket) # save mode rank data in database rank_data.update(reward_dist_data) - await self.data.profile.put_profile_rank(user_id, self.version, rank_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 = await self.data.profile.get_profile(user_id, self.version) + profile = self.data.profile.get_profile(user_id, self.version) total_play = profile["total_play"] + 1 # update profile - await self.data.profile.put_profile( + self.data.profile.put_profile( user_id, self.version, { @@ -1409,8 +1391,7 @@ class IDACSeason2(IDACBase): # 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 = await self.data.item.get_car(user_id, self.version, style_car_id) - used_car = used_car._asdict() + 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 @@ -1418,7 +1399,7 @@ class IDACSeason2(IDACBase): used_car["car_mileage"] = car_mileage # save the used car in database - await self.data.item.put_car(user_id, self.version, used_car) + 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 @@ -1427,12 +1408,12 @@ class IDACSeason2(IDACBase): skill_level_exp = data.pop("skill_level_exp") # get the course data - course = await self.data.item.get_course(user_id, course_id) + course = self.data.item.get_course(user_id, course_id) if course: # update run_counts run_counts = course["run_counts"] + 1 - await self.data.item.put_course( + self.data.item.put_course( user_id, { "course_id": course_id, @@ -1444,12 +1425,12 @@ class IDACSeason2(IDACBase): 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 = await self.data.item.get_time_trial_ranking_by_course( + 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 = await self.data.item.get_time_trial_ranking_by_course( + car_course_rank = self.data.item.get_time_trial_ranking_by_course( self.version, course_id, style_car_id, limit=None ) car_course_rank = ( @@ -1460,7 +1441,7 @@ class IDACSeason2(IDACBase): if data.get("goal_time") > 0: # get the current best goal time best_time_trial = ( - await self.data.item.get_time_trial_user_best_time_by_course_car( + self.data.item.get_time_trial_user_best_time_by_course_car( self.version, user_id, course_id, style_car_id ) ) @@ -1471,10 +1452,10 @@ class IDACSeason2(IDACBase): ): # now finally save the time trial with updated timestamp data["play_dt"] = datetime.now() - await self.data.item.put_time_trial(self.version, user_id, data) + self.data.item.put_time_trial(self.version, user_id, data) # update the timetrial event points - await self.data.item.put_timetrial_event( + self.data.item.put_timetrial_event( user_id, self.timetrial_event_id, event_point ) @@ -1491,7 +1472,7 @@ class IDACSeason2(IDACBase): }, } - async def handle_user_updatestoryresult_request(self, data: Dict, headers: Dict): + def handle_user_updatestoryresult_request(self, data: Dict, headers: Dict): user_id = headers["session"] stock_data: Dict = data.pop("stock_obj") @@ -1502,15 +1483,15 @@ class IDACSeason2(IDACBase): # stamp_event_data = data.pop("stamp_event_data") # save stock data in database - await self._save_stock_data(user_id, stock_data) + self._save_stock_data(user_id, stock_data) # save tickets in database for ticket in ticket_data: - await self.data.item.put_ticket(user_id, ticket) + self.data.item.put_ticket(user_id, ticket) # save mode rank data in database rank_data.update(reward_dist_data) - await self.data.profile.put_profile_rank(user_id, self.version, rank_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") @@ -1521,7 +1502,7 @@ class IDACSeason2(IDACBase): play_status = data.get("play_status") # get the current loop from the database - story_data = await self.data.item.get_story(user_id, chapter_id) + story_data = self.data.item.get_story(user_id, chapter_id) # 1 = active, 2+ = cleared? loop_count = 1 if story_data: @@ -1535,13 +1516,13 @@ class IDACSeason2(IDACBase): # 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 = await self.data.item.get_story_episode(user_id, episode_id) + 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 - await self.data.item.put_story_episode( + self.data.item.put_story_episode( user_id, chapter_id, { @@ -1555,9 +1536,9 @@ class IDACSeason2(IDACBase): loop_count += 1 # for the current chapter set all episode play_status back to 1 - await self.data.item.put_story_episode_play_status(user_id, chapter_id, 1) + self.data.item.put_story_episode_play_status(user_id, chapter_id, 1) - await self.data.item.put_story( + self.data.item.put_story( user_id, { "story_type": data.get("story_type"), @@ -1567,7 +1548,7 @@ class IDACSeason2(IDACBase): ) # save the current episode difficulty progress in database - await self.data.item.put_story_episode_difficulty( + self.data.item.put_story_episode_difficulty( user_id, episode_id, { @@ -1582,8 +1563,7 @@ class IDACSeason2(IDACBase): # 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 = await self.data.item.get_car(user_id, self.version, style_car_id) - used_car = used_car._asdict() + 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 @@ -1591,14 +1571,14 @@ class IDACSeason2(IDACBase): used_car["car_mileage"] = car_mileage # save the used car in database - await self.data.item.put_car(user_id, self.version, used_car) + 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 = await self.data.profile.get_profile(user_id, self.version) + profile = self.data.profile.get_profile(user_id, self.version) total_play = profile["total_play"] + 1 # save user profile in database - await self.data.profile.put_profile( + self.data.profile.put_profile( user_id, self.version, { @@ -1617,14 +1597,12 @@ class IDACSeason2(IDACBase): return { "status_code": "0", - "story_data": await self._generate_story_data(user_id), + "story_data": self._generate_story_data(user_id), "car_use_count": [], "maker_use_count": [], } - async def handle_user_updatespecialmoderesult_request( - self, data: Dict, headers: Dict - ): + def handle_user_updatespecialmoderesult_request(self, data: Dict, headers: Dict): user_id = headers["session"] stock_data: Dict = data.pop("stock_obj") @@ -1638,11 +1616,11 @@ class IDACSeason2(IDACBase): # get the vs use count from database and update it style_car_id = data.pop("style_car_id") - car_data = await self.data.item.get_car(user_id, self.version, 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 - await self.data.item.put_car( + self.data.item.put_car( user_id, self.version, { @@ -1653,11 +1631,11 @@ class IDACSeason2(IDACBase): ) # get the profile data, update total_play and daily_play, and save it - profile = await self.data.profile.get_profile(user_id, self.version) + profile = self.data.profile.get_profile(user_id, self.version) total_play = profile["total_play"] + 1 # save user profile in database - await self.data.profile.put_profile( + self.data.profile.put_profile( user_id, self.version, { @@ -1675,29 +1653,27 @@ class IDACSeason2(IDACBase): ) # save stock data in database - await self._save_stock_data(user_id, stock_data) + self._save_stock_data(user_id, stock_data) # save ticket data in database for ticket in ticket_data: - await self.data.item.put_ticket(user_id, ticket) + self.data.item.put_ticket(user_id, ticket) # save mode_rank and reward_dist data in database rank_data.update(reward_dist_data) - await self.data.profile.put_profile_rank(user_id, self.version, rank_data) + self.data.profile.put_profile_rank(user_id, self.version, rank_data) # finally save the special mode with story_type=4 in database - await self.data.item.put_challenge(user_id, data) + self.data.item.put_challenge(user_id, data) return { "status_code": "0", - "special_mode_data": await self._generate_special_data(user_id), + "special_mode_data": self._generate_special_data(user_id), "car_use_count": [], "maker_use_count": [], } - async def handle_user_updatechallengemoderesult_request( - self, data: Dict, headers: Dict - ): + def handle_user_updatechallengemoderesult_request(self, data: Dict, headers: Dict): user_id = headers["session"] stock_data: Dict = data.pop("stock_obj") @@ -1708,11 +1684,11 @@ class IDACSeason2(IDACBase): # get the vs use count from database and update it style_car_id = data.get("style_car_id") - car_data = await self.data.item.get_car(user_id, self.version, 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 - await self.data.item.put_car( + self.data.item.put_car( user_id, self.version, { @@ -1723,11 +1699,11 @@ class IDACSeason2(IDACBase): ) # get the profile data, update total_play and daily_play, and save it - profile = await self.data.profile.get_profile(user_id, self.version) + profile = self.data.profile.get_profile(user_id, self.version) total_play = profile["total_play"] + 1 # save user profile in database - await self.data.profile.put_profile( + self.data.profile.put_profile( user_id, self.version, { @@ -1745,18 +1721,18 @@ class IDACSeason2(IDACBase): ) # save stock data in database - await self._save_stock_data(user_id, stock_data) + self._save_stock_data(user_id, stock_data) # save ticket data in database for ticket in ticket_data: - await self.data.item.put_ticket(user_id, ticket) + self.data.item.put_ticket(user_id, ticket) # save mode_rank and reward_dist data in database rank_data.update(reward_dist_data) - await self.data.profile.put_profile_rank(user_id, self.version, rank_data) + self.data.profile.put_profile_rank(user_id, self.version, rank_data) # get the challenge mode data from database - challenge_data = await self.data.item.get_challenge( + challenge_data = self.data.item.get_challenge( user_id, data.get("vs_type"), data.get("play_difficulty") ) @@ -1766,22 +1742,20 @@ class IDACSeason2(IDACBase): data["play_count"] = play_count # finally save the challenge mode with story_type=3 in database - await self.data.item.put_challenge(user_id, data) + self.data.item.put_challenge(user_id, data) return { "status_code": "0", - "challenge_mode_data": await self._generate_challenge_data(user_id), + "challenge_mode_data": self._generate_challenge_data(user_id), "car_use_count": [], "maker_use_count": [], } - async def _generate_time_trial_data( - self, season_id: int, user_id: int - ) -> List[Dict]: + 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 = await self.data.item.get_courses(user_id) + courses = self.data.item.get_courses(user_id) if courses is None or len(courses) == 0: return {"status_code": "0", "timetrial_data": timetrial_data} @@ -1791,7 +1765,7 @@ class IDACSeason2(IDACBase): skill_level_exp = course["skill_level_exp"] # get the best time for the current course for the current user - best_trial = await self.data.item.get_time_trial_best_ranking_by_course( + best_trial = self.data.item.get_time_trial_best_ranking_by_course( season_id, user_id, course_id ) if not best_trial: @@ -1799,7 +1773,7 @@ class IDACSeason2(IDACBase): goal_time = best_trial["goal_time"] # get the rank for the current course - course_rank = await self.data.item.get_time_trial_ranking_by_course( + course_rank = self.data.item.get_time_trial_ranking_by_course( season_id, course_id, limit=None ) course_rank = ( @@ -1819,14 +1793,12 @@ class IDACSeason2(IDACBase): return timetrial_data - async def handle_user_getpastseasontadata_request(self, data: Dict, headers: Dict): + 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 = await self._generate_time_trial_data( - season_id - 1, user_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?! @@ -1836,10 +1808,10 @@ class IDACSeason2(IDACBase): "past_season_timetrial_data": past_timetrial_data, } - async def handle_user_gettadata_request(self, data: Dict, headers: Dict): + def handle_user_gettadata_request(self, data: Dict, headers: Dict): user_id = headers["session"] - timetrial_data = await self._generate_time_trial_data(self.version, user_id) + 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?! @@ -1849,17 +1821,17 @@ class IDACSeason2(IDACBase): # "past_season_timetrial_data": timetrial_data, } - async def handle_user_updatecartune_request(self, data: Dict, headers: Dict): + 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 = await self.data.item.get_ticket(user_id, ticket_id=25) + ticket = self.data.item.get_ticket(user_id, ticket_id=25) # update the ticket in the database - await self.data.item.put_ticket( + self.data.item.put_ticket( user_id, { "ticket_id": ticket["ticket_id"], @@ -1870,22 +1842,22 @@ class IDACSeason2(IDACBase): # also set the tune_level to 16 (fully tuned) data["tune_level"] = 16 - await self.data.item.put_car(user_id, self.version, data) + self.data.item.put_car(user_id, self.version, data) return { "status_code": "0", - "story_data": await self._generate_story_data(user_id), + "story_data": self._generate_story_data(user_id), "car_use_count": [], "maker_use_count": [], } - async def handle_log_saveplaylog_request(self, data: Dict, headers: Dict): + def handle_log_saveplaylog_request(self, data: Dict, headers: Dict): pass - async def handle_log_saveendlog_request(self, data: Dict, headers: Dict): + def handle_log_saveendlog_request(self, data: Dict, headers: Dict): pass - async def handle_user_updatemoderesult_request(self, data: Dict, headers: Dict): + def handle_user_updatemoderesult_request(self, data: Dict, headers: Dict): user_id = headers["session"] config_data: Dict = data.pop("config_obj") @@ -1899,32 +1871,30 @@ class IDACSeason2(IDACBase): tips_list = data.pop("tips_list") # save stock data in database - await self._save_stock_data(user_id, stock_data) + self._save_stock_data(user_id, stock_data) # save tickets in database for ticket in ticket_data: - await self.data.item.put_ticket(user_id, ticket) + self.data.item.put_ticket(user_id, ticket) # save rank dist data in database - await self.data.profile.put_profile_rank( - user_id, self.version, reward_dist_data - ) + self.data.profile.put_profile_rank(user_id, self.version, reward_dist_data) # update profile data and config in database - await self.data.profile.put_profile(user_id, self.version, data) + self.data.profile.put_profile(user_id, self.version, data) config_data["config_id"] = config_data.pop("id") - await self.data.profile.put_profile_config(user_id, config_data) + self.data.profile.put_profile_config(user_id, config_data) return {"status_code": "0", "server_status": 1} - async def _generate_theory_rival_data( + 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 = await self.data.profile.get_profile(req_user_id, self.version) + profile = self.data.profile.get_profile(req_user_id, self.version) profile = profile._asdict() # set the name to CPU profile["username"] = f"CPU" @@ -1937,11 +1907,9 @@ class IDACSeason2(IDACBase): profile["stamp_key_assign_3"] = 3 profile["mytitle_id"] = 0 else: - profile = await self.data.profile.get_profile(user_id, self.version) + profile = self.data.profile.get_profile(user_id, self.version) - rank = await self.data.profile.get_profile_rank( - profile["user"], self.version - ) + rank = self.data.profile.get_profile_rank(profile["user"], self.version) avatars = [ { @@ -2009,23 +1977,21 @@ class IDACSeason2(IDACBase): if user_id == -1: # get a random avatar from the list and some random car from all users avatar = choice(avatars) - car = await self.data.item.get_random_car(self.version) + car = self.data.item.get_random_car(self.version) else: - avatar = await self.data.profile.get_profile_avatar(profile["user"]) - car = await self.data.item.get_random_user_car( - profile["user"], self.version - ) + 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 = await self.data.item.get_theory_course(profile["user"], course_id) + course = self.data.item.get_theory_course(profile["user"], course_id) powerhose_lv = 0 if course: powerhose_lv = course["powerhouse_lv"] - theory_running = await self.data.item.get_theory_running_by_course( + theory_running = self.data.item.get_theory_running_by_course( profile["user"], course_id ) @@ -2044,13 +2010,13 @@ class IDACSeason2(IDACBase): # get the time trial ranking medal eval_id = 0 - time_trial = await self.data.item.get_time_trial_best_ranking_by_course( + 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 = await self.data.arcade.get_arcade(profile["store"]) + arcade = self.data.arcade.get_arcade(profile["store"]) if arcade is None: arcade = {} arcade["name"] = self.core_cfg.server.name @@ -2112,7 +2078,7 @@ class IDACSeason2(IDACBase): return rival_data - async def handle_theory_matching_request(self, data: Dict, headers: Dict): + def handle_theory_matching_request(self, data: Dict, headers: Dict): user_id = headers["session"] course_id = data.pop("course_id") @@ -2127,7 +2093,7 @@ class IDACSeason2(IDACBase): powerhose_lv = data.pop("powerhouse_lv") # get random profiles for auto match - profiles = await self.data.profile.get_different_random_profiles( + profiles = self.data.profile.get_different_random_profiles( user_id, self.version, count=count_auto_match ) @@ -2136,12 +2102,10 @@ class IDACSeason2(IDACBase): while len(user_list) < count_auto_match: user_list.append(-1) - auto_match = await self._generate_theory_rival_data( - user_list, course_id, user_id - ) + 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 = await self.data.item.get_theory_course_by_powerhouse_lv( + 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] @@ -2150,9 +2114,7 @@ class IDACSeason2(IDACBase): while len(user_list) < count_power_match: user_list.append(-1) - power_match = await self._generate_theory_rival_data( - user_list, course_id, user_id - ) + power_match = self._generate_theory_rival_data(user_list, course_id, user_id) return { "status_code": "0", @@ -2163,7 +2125,7 @@ class IDACSeason2(IDACBase): }, } - async def handle_user_updatetheoryresult_request(self, data: Dict, headers: Dict): + def handle_user_updatetheoryresult_request(self, data: Dict, headers: Dict): user_id = headers["session"] stock_data: Dict = data.pop("stock_obj") @@ -2173,15 +2135,15 @@ class IDACSeason2(IDACBase): driver_debut_data: Dict = data.pop("driver_debut_obj") # save stock data in database - await self._save_stock_data(user_id, stock_data) + self._save_stock_data(user_id, stock_data) # save tickets in database for ticket in ticket_data: - await self.data.item.put_ticket(user_id, ticket) + self.data.item.put_ticket(user_id, ticket) # save rank dist data in database rank_data.update(reward_dist_data) - await self.data.profile.put_profile_rank(user_id, self.version, rank_data) + self.data.profile.put_profile_rank(user_id, self.version, rank_data) # save the profile theory data in database play_count = 1 @@ -2189,7 +2151,7 @@ class IDACSeason2(IDACBase): win_count = 0 win_count_multi = 0 - theory_data = await self.data.profile.get_profile_theory(user_id, self.version) + 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 @@ -2207,7 +2169,7 @@ class IDACSeason2(IDACBase): win_count += 1 win_count_multi += 1 - await self.data.profile.put_profile_theory( + self.data.profile.put_profile_theory( user_id, self.version, { @@ -2227,7 +2189,7 @@ class IDACSeason2(IDACBase): ) # save theory course in database - await self.data.item.put_theory_course( + self.data.item.put_theory_course( user_id, { "course_id": data.get("course_id"), @@ -2242,7 +2204,7 @@ class IDACSeason2(IDACBase): ) # save the theory partner in database - await self.data.item.put_theory_partner( + self.data.item.put_theory_partner( user_id, { "partner_id": data.get("partner_id"), @@ -2252,7 +2214,7 @@ class IDACSeason2(IDACBase): ) # save the theory running in database? - await self.data.item.put_theory_running( + self.data.item.put_theory_running( user_id, { "course_id": data.get("course_id"), @@ -2267,8 +2229,7 @@ class IDACSeason2(IDACBase): # 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 = await self.data.item.get_car(user_id, self.version, style_car_id) - used_car = used_car._asdict() + 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 @@ -2276,14 +2237,14 @@ class IDACSeason2(IDACBase): used_car["car_mileage"] = car_mileage # save the used car in database - await self.data.item.put_car(user_id, self.version, used_car) + 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 = await self.data.profile.get_profile(user_id, self.version) + profile = self.data.profile.get_profile(user_id, self.version) total_play = profile["total_play"] + 1 # save the profile in database - await self.data.profile.put_profile( + self.data.profile.put_profile( user_id, self.version, { @@ -2311,16 +2272,16 @@ class IDACSeason2(IDACBase): "win_count_multi": win_count_multi, } - async def handle_timetrial_getbestrecordprebattle_request( + def handle_timetrial_getbestrecordprebattle_request( self, data: Dict, headers: Dict ): user_id = headers["session"] course_pickup_car_best_data = [] - courses = await self.data.item.get_time_trial_courses(self.version) + courses = self.data.item.get_time_trial_courses(self.version) for course in courses: car_list = [] - best_cars = await self.data.item.get_time_trial_best_cars_by_course( + best_cars = self.data.item.get_time_trial_best_cars_by_course( self.version, course["course_id"], user_id ) @@ -2356,39 +2317,36 @@ class IDACSeason2(IDACBase): "course_pickup_car_best_data": course_pickup_car_best_data, } - async def handle_user_updateonlinebattle_request(self, data: Dict, headers: Dict): + def handle_user_updateonlinebattle_request(self, data: Dict, headers: Dict): return { "status_code": "0", "bothwin_penalty": 1, } - async def handle_user_updateonlinebattleresult_request( - self, data: Dict, headers: Dict - ): + 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 - await self._save_stock_data(user_id, stock_data) + self._save_stock_data(user_id, stock_data) ticket_data: List = data.pop("ticket_data") for ticket in ticket_data: - await self.data.item.put_ticket(user_id, ticket) + 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) - await self.data.profile.put_profile_rank(user_id, self.version, rank_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 = await self.data.item.get_car(user_id, self.version, style_car_id) - used_car = used_car._asdict() + 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 @@ -2396,14 +2354,14 @@ class IDACSeason2(IDACBase): used_car["car_mileage"] = car_mileage # save the used car in database - await self.data.item.put_car(user_id, self.version, used_car) + 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 = await self.data.profile.get_profile(user_id, self.version) + profile = self.data.profile.get_profile(user_id, self.version) total_play = profile["total_play"] + 1 # save the profile in database - await self.data.profile.put_profile( + self.data.profile.put_profile( user_id, self.version, { @@ -2420,7 +2378,7 @@ class IDACSeason2(IDACBase): }, ) - await self.data.item.put_vs_info(user_id, data) + self.data.item.put_vs_info(user_id, data) vs_info = { "battle_mode": 0, @@ -2457,9 +2415,7 @@ class IDACSeason2(IDACBase): "maker_use_count": [], } - async def handle_user_updatestorebattleresult_request( - self, data: Dict, headers: Dict - ): + def handle_user_updatestorebattleresult_request(self, data: Dict, headers: Dict): user_id = headers["session"] stock_data: Dict = data.pop("stock_obj") @@ -2474,21 +2430,20 @@ class IDACSeason2(IDACBase): gift_id = data.pop("gift_id") # save stock data in database - await self._save_stock_data(user_id, stock_data) + self._save_stock_data(user_id, stock_data) # save tickets in database for ticket in ticket_data: - await self.data.item.put_ticket(user_id, ticket) + self.data.item.put_ticket(user_id, ticket) # save rank dist data in database rank_data.update(reward_dist_data) - await self.data.profile.put_profile_rank(user_id, self.version, rank_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 = await self.data.item.get_car(user_id, self.version, style_car_id) - used_car = used_car._asdict() + 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 @@ -2496,14 +2451,14 @@ class IDACSeason2(IDACBase): used_car["car_mileage"] = car_mileage # save the used car in database - await self.data.item.put_car(user_id, self.version, used_car) + 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 = await self.data.profile.get_profile(user_id, self.version) + profile = self.data.profile.get_profile(user_id, self.version) total_play = profile["total_play"] + 1 # save the profile in database - await self.data.profile.put_profile( + self.data.profile.put_profile( user_id, self.version, { @@ -2521,7 +2476,7 @@ class IDACSeason2(IDACBase): ) # save vs_info in database - await self.data.item.put_vs_info(user_id, data) + self.data.item.put_vs_info(user_id, data) vs_info = { "battle_mode": 0, diff --git a/titles/idac/templates/idac_index.jinja b/titles/idac/templates/idac_index.jinja deleted file mode 100644 index caa7770..0000000 --- a/titles/idac/templates/idac_index.jinja +++ /dev/null @@ -1,134 +0,0 @@ -{% extends "core/templates/index.jinja" %} -{% block content %} -

頭文字D THE ARCADE

- -{% if sesh is defined and sesh["user_id"] > 0 %} -
-
- {% if profile is defined and profile is not none %} -
-
-

{{ sesh["username"] }}'s Profile

-
-
- - -
-
-
-
- -
-
-
-
-
Information
-
-
Username
-

{{ profile.username }}

-
Cash
-

{{ profile.cash }} D

-
Grade
-

- {% set grade = rank.grade %} - {% if grade >= 1 and grade <= 72 %} - {% set grade_number = (grade - 1) // 9 %} - {% set grade_letters = ['E', 'D', 'C', 'B', 'A', 'S', 'SS', 'X'] %} - {{ grade_letters[grade_number] }}{{ 9 - ((grade-1) % 9) }} - {% else %} - Unknown - {% endif %} -

-
-
-
-
-
- -
-
Statistics
-
-
-
-
Total Plays
-

{{ profile.total_play }}

-
-
-
Last Played
-

{{ profile.last_play_date }}

-
-
-
Mileage
-

{{ profile.mileage / 1000}} km

-
-
- {% if tickets is defined and tickets|length > 0 %} -
Tokens/Tickets
-
-
-
-
Avatar Tokens
-

{{ tickets.avatar_points }}/30

-
-
-
Car Dressup Tokens
-

{{ tickets.car_dressup_points }}/30

-
-
-
FullTune Tickets
-

{{ tickets.full_tune_tickets }}/99

-
-
-
FullTune Fragments
-

{{ tickets.full_tune_fragments }}/10

-
-
- {% endif %} -
-
-
-
- {% else %} - - {% endif %} - -
-
-{% else %} - -{% endif %} - - - - - -{% endblock content %} \ No newline at end of file diff --git a/titles/idac/templates/js/idac_scripts.js b/titles/idac/templates/js/idac_scripts.js deleted file mode 100644 index 111fea6..0000000 --- a/titles/idac/templates/js/idac_scripts.js +++ /dev/null @@ -1,10 +0,0 @@ -$(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/idz/__init__.py b/titles/idz/__init__.py index 10959ca..958d08a 100644 --- a/titles/idz/__init__.py +++ b/titles/idz/__init__.py @@ -1,7 +1,8 @@ +from titles.idz.index import IDZServlet from titles.idz.const import IDZConstants from titles.idz.database import IDZData -from titles.idz.index import IDZServlet 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 index b30ed82..f7af4fd 100644 --- a/titles/idz/config.py +++ b/titles/idz/config.py @@ -1,4 +1,4 @@ -from typing import Dict, List +from typing import List, Dict from core.config import CoreConfig diff --git a/titles/idz/database.py b/titles/idz/database.py index d366372..525b1c1 100644 --- a/titles/idz/database.py +++ b/titles/idz/database.py @@ -1,5 +1,5 @@ -from core.config import CoreConfig from core.data import Data +from core.config import CoreConfig class IDZData(Data): diff --git a/titles/idz/echo.py b/titles/idz/echo.py index 92177e5..979fd19 100644 --- a/titles/idz/echo.py +++ b/titles/idz/echo.py @@ -1,10 +1,19 @@ +from twisted.internet.protocol import DatagramProtocol import logging +from core.config import CoreConfig +from .config import IDZConfig -class IDZEcho: - def connection_made(self, transport): - self.transport = transport - def datagram_received(self, data, addr): - logging.getLogger("idz").debug(f"Received echo from {addr}") - self.transport.sendto(data, addr) +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 index 8a6fda1..17213ba 100644 --- a/titles/idz/handlers/__init__.py +++ b/titles/idz/handlers/__init__.py @@ -1,25 +1,47 @@ -# ruff: noqa: F401 from .base import IDZHandlerBase -from .check_team_names import IDZHandlerCheckTeamName -from .create_auto_team import IDZHandlerCreateAutoTeam -from .create_profile import IDZHandlerCreateProfile -from .discover_profile import IDZHandlerDiscoverProfile -from .load_2on2 import IDZHandlerLoad2on2A, IDZHandlerLoad2on2B -from .load_config import IDZHandlerLoadConfigA, IDZHandlerLoadConfigB -from .load_ghost import IDZHandlerLoadGhost -from .load_profile import IDZHandlerLoadProfile -from .load_reward_table import IDZHandlerLoadRewardTable + from .load_server_info import IDZHandlerLoadServerInfo -from .load_team_ranking import IDZHandlerLoadTeamRankingA, IDZHandlerLoadTeamRankingB + +from .load_ghost import IDZHandlerLoadGhost + +from .load_config import IDZHandlerLoadConfigA, IDZHandlerLoadConfigB + from .load_top_ten import IDZHandlerLoadTopTen -from .lock_profile import IDZHandlerLockProfile -from .save_expedition import IDZHandlerSaveExpedition -from .save_profile import IDZHandlerSaveProfile -from .save_time_attack import IDZHandlerSaveTimeAttack -from .save_topic import IDZHandlerSaveTopic -from .unknown import IDZHandlerUnknown -from .unlock_profile import IDZHandlerUnlockProfile -from .update_provisional_store_rank import IDZHandlerUpdateProvisionalStoreRank + 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 index 1fd8229..6b1e5d5 100644 --- a/titles/idz/handlers/base.py +++ b/titles/idz/handlers/base.py @@ -1,9 +1,7 @@ import logging import struct - -from core.config import CoreConfig from core.data import Data - +from core.config import CoreConfig from ..config import IDZConfig from ..const import IDZConstants diff --git a/titles/idz/handlers/check_team_names.py b/titles/idz/handlers/check_team_names.py index 81d5981..e9a255d 100644 --- a/titles/idz/handlers/check_team_names.py +++ b/titles/idz/handlers/check_team_names.py @@ -1,9 +1,8 @@ import struct -from core.config import CoreConfig - -from ..config import IDZConfig from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig class IDZHandlerCheckTeamName(IDZHandlerBase): diff --git a/titles/idz/handlers/create_auto_team.py b/titles/idz/handlers/create_auto_team.py index 41b75cd..4c581fc 100644 --- a/titles/idz/handlers/create_auto_team.py +++ b/titles/idz/handlers/create_auto_team.py @@ -1,11 +1,11 @@ -import struct from operator import indexOf -from random import choice +import struct +import json +from random import choice, randrange -from core.config import CoreConfig - -from ..config import IDZConfig from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig AUTO_TEAM_NAMES = ["スピードスターズ", "レッドサンズ", "ナイトキッズ"] FULL_WIDTH_NUMS = [ diff --git a/titles/idz/handlers/create_profile.py b/titles/idz/handlers/create_profile.py index 422b949..d037ce1 100644 --- a/titles/idz/handlers/create_profile.py +++ b/titles/idz/handlers/create_profile.py @@ -2,10 +2,9 @@ import json import struct from datetime import datetime -from core.config import CoreConfig - -from ..config import IDZConfig from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig class IDZHandlerCreateProfile(IDZHandlerBase): diff --git a/titles/idz/handlers/discover_profile.py b/titles/idz/handlers/discover_profile.py index 8ab4d1e..9d40284 100644 --- a/titles/idz/handlers/discover_profile.py +++ b/titles/idz/handlers/discover_profile.py @@ -1,9 +1,9 @@ import struct +from typing import Tuple, List, Dict -from core.config import CoreConfig - -from ..config import IDZConfig from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig class IDZHandlerDiscoverProfile(IDZHandlerBase): diff --git a/titles/idz/handlers/load_2on2.py b/titles/idz/handlers/load_2on2.py index 58b22b8..8f1767f 100644 --- a/titles/idz/handlers/load_2on2.py +++ b/titles/idz/handlers/load_2on2.py @@ -1,8 +1,9 @@ -from core.config import CoreConfig +import struct +from .base import IDZHandlerBase +from core.config import CoreConfig from ..config import IDZConfig from ..const import IDZConstants -from .base import IDZHandlerBase class IDZHandlerLoad2on2A(IDZHandlerBase): diff --git a/titles/idz/handlers/load_config.py b/titles/idz/handlers/load_config.py index 51fd0c6..b3ceb0d 100644 --- a/titles/idz/handlers/load_config.py +++ b/titles/idz/handlers/load_config.py @@ -1,10 +1,9 @@ import struct +from .base import IDZHandlerBase from core.config import CoreConfig - from ..config import IDZConfig from ..const import IDZConstants -from .base import IDZHandlerBase class IDZHandlerLoadConfigA(IDZHandlerBase): diff --git a/titles/idz/handlers/load_ghost.py b/titles/idz/handlers/load_ghost.py index 71a0f6f..2f7e102 100644 --- a/titles/idz/handlers/load_ghost.py +++ b/titles/idz/handlers/load_ghost.py @@ -1,9 +1,8 @@ import struct -from core.config import CoreConfig - -from ..config import IDZConfig from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig class IDZHandlerLoadGhost(IDZHandlerBase): diff --git a/titles/idz/handlers/load_profile.py b/titles/idz/handlers/load_profile.py index c98c517..821cc24 100644 --- a/titles/idz/handlers/load_profile.py +++ b/titles/idz/handlers/load_profile.py @@ -1,10 +1,9 @@ import struct +from .base import IDZHandlerBase from core.config import CoreConfig - from ..config import IDZConfig from ..const import IDZConstants -from .base import IDZHandlerBase class IDZHandlerLoadProfile(IDZHandlerBase): diff --git a/titles/idz/handlers/load_reward_table.py b/titles/idz/handlers/load_reward_table.py index ddf3ed7..4f9da92 100644 --- a/titles/idz/handlers/load_reward_table.py +++ b/titles/idz/handlers/load_reward_table.py @@ -1,7 +1,8 @@ -from core.config import CoreConfig +import struct -from ..config import IDZConfig from .base import IDZHandlerBase +from core.config import CoreConfig +from ..config import IDZConfig class IDZHandlerLoadRewardTable(IDZHandlerBase): diff --git a/titles/idz/handlers/load_server_info.py b/titles/idz/handlers/load_server_info.py index e6298c3..4c60dd7 100644 --- a/titles/idz/handlers/load_server_info.py +++ b/titles/idz/handlers/load_server_info.py @@ -1,11 +1,10 @@ import struct -from core.config import CoreConfig from core.utils import Utils - +from .base import IDZHandlerBase +from core.config import CoreConfig from ..config import IDZConfig from ..const import IDZConstants -from .base import IDZHandlerBase class IDZHandlerLoadServerInfo(IDZHandlerBase): @@ -22,15 +21,13 @@ class IDZHandlerLoadServerInfo(IDZHandlerBase): 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.server.hostname}:{t_port}/idz/news/news80**.txt" - ) - err_str = f"http://{self.core_config.server.hostname}:{t_port}/idz/error" + 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.server.hostname) + len_hostname = len(self.core_config.title.hostname) len_news = len(news_str) len_error = len(err_str) @@ -39,7 +36,7 @@ class IDZHandlerLoadServerInfo(IDZHandlerBase): f"{len_hostname}s", ret, 0x4 + offset, - self.core_config.server.hostname.encode(), + self.core_config.title.hostname.encode(), ) struct.pack_into(" List[Route]: - return [ - Route("/idz/news/{endpoint:str}", self.render_GET), - Route("/idz/error", self.render_GET), + + 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]: + + def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: hostname = ( - self.core_cfg.server.hostname + self.core_cfg.title.hostname if not self.game_cfg.server.hostname else self.game_cfg.server.hostname ) @@ -139,42 +136,37 @@ class IDZServlet(BaseServlet): except AttributeError as e: continue - loop = asyncio.get_running_loop() - IDZUserDB(self.core_cfg, self.game_cfg, self.rsa_keys, handler_map).start() - asyncio.create_task( - loop.create_datagram_endpoint( - lambda: IDZEcho(), - local_addr=( - self.core_cfg.server.listen_address, - self.game_cfg.ports.echo, - ), - ) - ) - asyncio.create_task( - loop.create_datagram_endpoint( - lambda: IDZEcho(), - local_addr=( - self.core_cfg.server.listen_address, - self.game_cfg.ports.match, - ), - ) - ) - asyncio.create_task( - loop.create_datagram_endpoint( - lambda: IDZEcho(), - local_addr=( - self.core_cfg.server.listen_address, - self.game_cfg.ports.userdb + 1, - ), - ) + 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) ) - async def render_GET(self, request: Request) -> bytes: - url_path = request.path_params.get("endpoint", "") - if not url_path: - return Response() + 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 @@ -184,8 +176,4 @@ class IDZServlet(BaseServlet): news += "\r\n" news = "1979/01/01 00:00:00 2099/12/31 23:59:59 " + news - return PlainTextResponse( - news, - media_type="text/plain; charset=utf-8", - headers={"Last-Modified": "Sun, 23 Apr 2023 05:33:20 GMT"}, - ) + return news.encode() diff --git a/titles/idz/userdb.py b/titles/idz/userdb.py index cd495a2..fd555a1 100644 --- a/titles/idz/userdb.py +++ b/titles/idz/userdb.py @@ -1,12 +1,18 @@ -import asyncio -import logging -import random +from twisted.internet.protocol import Factory, Protocol +import logging, coloredlogs +from Crypto.Cipher import AES import struct -from typing import Dict, List, Optional, Type +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 Crypto.Cipher import AES - +from .database import IDZData from .config import IDZConfig from .const import IDZConstants from .handlers import IDZHandlerBase @@ -22,7 +28,7 @@ class IDZKey: self.hashN = hashN -class IDZUserDB: +class IDZUserDBProtocol(Protocol): def __init__( self, core_cfg: CoreConfig, @@ -40,16 +46,6 @@ class IDZUserDB: self.version_internal = None self.skip_next = False - def start(self) -> None: - self.logger.info(f"Start on port {self.game_config.ports.userdb}") - asyncio.create_task( - asyncio.start_server( - self.connection_cb, - self.core_config.server.listen_address, - self.game_config.ports.userdb, - ) - ) - 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}" + ) - self.logger.debug(f"Send handshake {result.hex()}") - - writer.write(result) - await writer.drain() - - data: bytes = await reader.read(4096) - if len(data) == 0: - self.logger.debug("Connection closed") - return - - await self.dataReceived(data, reader, writer) - await writer.drain() - - except ConnectionResetError as e: - self.logger.debug("Connection reset, disconnecting") - return - - def dataReceived( - self, data: bytes, reader: asyncio.StreamReader, writer: asyncio.StreamWriter - ) -> None: + def dataReceived(self, data: bytes) -> None: self.logger.debug(f"Receive data {data.hex()}") - client_ip = writer.get_extra_info("peername")[0] 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 {client_ip} because {e} - {data.hex()}" - ) - + 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 85a038e..a3c178e 100644 --- a/titles/mai2/__init__.py +++ b/titles/mai2/__init__.py @@ -1,6 +1,6 @@ +from titles.mai2.index import Mai2Servlet from titles.mai2.const import Mai2Constants from titles.mai2.database import Mai2Data -from titles.mai2.index import Mai2Servlet from titles.mai2.read import Mai2Reader index = Mai2Servlet @@ -16,3 +16,4 @@ game_codes = [ Mai2Constants.GAME_CODE_GREEN, Mai2Constants.GAME_CODE, ] +current_schema_version = 9 diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 8f242f0..2f40176 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -1,16 +1,15 @@ +from datetime import datetime, timedelta +from typing import Any, Dict, List import logging from base64 import b64decode -from datetime import datetime, timedelta -from os import path, remove, stat -from typing import Any, Dict, List +from os import path, stat, remove +from PIL import ImageFile import pytz from core.config import CoreConfig from core.utils import Utils -from PIL import ImageFile - -from .config import Mai2Config from .const import Mai2Constants +from .config import Mai2Config from .database import Mai2Data @@ -26,23 +25,15 @@ class Mai2Base: 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.server.hostname}:{Utils.get_title_port(cfg)}/197/MaimaiServlet/" + 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.server.hostname}/197/MaimaiServlet/" - ) + self.old_server = f"http://{self.core_config.title.hostname}/197/MaimaiServlet/" - async def handle_get_game_setting_api_request(self, data: Dict): + def handle_get_game_setting_api_request(self, data: Dict): # 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 == "" - ): + 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 ) @@ -51,34 +42,21 @@ class Mai2Base: ) else: # get current datetime in JST - current_jst = datetime.now(pytz.timezone("Asia/Tokyo")).date() + 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" - ) + 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"), - ) + 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 { "isDevelop": False, "isAouAccession": False, @@ -89,28 +67,22 @@ class Mai2Base: "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 "", + "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 "", + "usbDlServerUri": self.old_server + "usbdl/" if self.can_deliver and self.game_config.deliver.udbdl_enable else "", }, } - async def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: + def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: return {"length": 0, "gameRankingList": []} - async def handle_get_game_tournament_info_api_request(self, data: Dict) -> Dict: + def handle_get_game_tournament_info_api_request(self, data: Dict) -> Dict: # TODO: Tournament support return {"length": 0, "gameTournamentInfoList": []} - async def handle_get_game_event_api_request(self, data: Dict) -> Dict: - events = await self.data.static.get_enabled_events(self.version) + 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 or not events: self.logger.warning("No enabled events, did you run the reader?") @@ -136,11 +108,11 @@ class Mai2Base: "gameEventList": events_lst, } - async def handle_get_game_ng_music_id_api_request(self, data: Dict) -> Dict: + def handle_get_game_ng_music_id_api_request(self, data: Dict) -> Dict: return {"length": 0, "musicIdList": []} - async def handle_get_game_charge_api_request(self, data: Dict) -> Dict: - game_charge_list = await self.data.static.get_enabled_tickets(self.version, 1) + 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": []} @@ -158,23 +130,21 @@ class Mai2Base: return {"length": len(charge_list), "gameChargeList": charge_list} - async def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: + def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "UpsertClientSettingApi"} - async def handle_upsert_client_upload_api_request(self, data: Dict) -> Dict: + def handle_upsert_client_upload_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "UpsertClientUploadApi"} - async def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: + def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "UpsertClientBookkeepingApi"} - async def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict: + def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "UpsertClientTestmodeApi"} - async def handle_get_user_preview_api_request(self, data: Dict) -> Dict: - p = await self.data.profile.get_profile_detail( - data["userId"], self.version, False - ) - w = await self.data.profile.get_web_option(data["userId"], self.version) + def handle_get_user_preview_api_request(self, data: Dict) -> Dict: + 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() @@ -199,84 +169,75 @@ class Mai2Base: "totalLv": profile["totalLv"], } - async def handle_user_login_api_request(self, data: Dict) -> Dict: - profile = await self.data.profile.get_profile_detail( - data["userId"], self.version - ) - consec = await self.data.profile.get_consec_login(data["userId"], self.version) + 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"] loginCt = profile["playCount"] if "regionId" in data: - await self.data.profile.put_profile_region( - data["userId"], data["regionId"] - ) + self.data.profile.put_profile_region(data["userId"], data["regionId"]) 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() - ) + 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 - await self.data.profile.add_consec_login(data["userId"], self.version) - + consec_ct = consec['logins'] + 1 + self.data.profile.add_consec_login(data["userId"], self.version) + elif lastlogindate_ < yesterday_midnight: consec_ct = 1 - await self.data.profile.reset_consec_login(data["userId"], self.version) + self.data.profile.reset_consec_login(data["userId"], self.version) else: - consec_ct = consec["logins"] + consec_ct = consec['logins'] + return { "returnCode": 1, "lastLoginDate": lastLoginDate, "loginCount": loginCt, - "consecutiveLoginCount": consec_ct, # Number of consecutive days we've logged in. + "consecutiveLoginCount": consec_ct, # Number of consecutive days we've logged in. } - async def handle_upload_user_playlog_api_request(self, data: Dict) -> Dict: + def handle_upload_user_playlog_api_request(self, data: Dict) -> Dict: user_id = data["userId"] playlog = data["userPlaylog"] - await self.data.score.put_playlog(user_id, playlog) + self.data.score.put_playlog(user_id, playlog) return {"returnCode": 1, "apiName": "UploadUserPlaylogApi"} - async def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: + 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", "") - await self.data.item.put_charge( + 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 + 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"} - async def handle_upsert_user_all_api_request(self, data: Dict) -> Dict: + 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"} @@ -285,82 +246,91 @@ class Mai2Base: upsert["userData"][0].pop("accessCode") upsert["userData"][0].pop("userId") - await self.data.profile.put_profile_detail( + self.data.profile.put_profile_detail( user_id, self.version, upsert["userData"][0], False ) - - if "userWebOption" in upsert and len(upsert["userWebOption"]) > 0: + + if "userWebOption" in upsert and len(upsert["userWebOption"]) > 0: upsert["userWebOption"][0]["isNetMember"] = True - await self.data.profile.put_web_option( + self.data.profile.put_web_option( user_id, self.version, upsert["userWebOption"][0] ) if "userGradeStatusList" in upsert and len(upsert["userGradeStatusList"]) > 0: - await self.data.profile.put_grade_status( + self.data.profile.put_grade_status( user_id, upsert["userGradeStatusList"][0] ) if "userBossList" in upsert and len(upsert["userBossList"]) > 0: - await self.data.profile.put_boss_list(user_id, 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"]: - await self.data.score.put_playlog(user_id, playlog, False) + self.data.score.put_playlog( + user_id, playlog, False + ) if "userExtend" in upsert and len(upsert["userExtend"]) > 0: - await self.data.profile.put_profile_extend( + self.data.profile.put_profile_extend( user_id, self.version, upsert["userExtend"][0] ) if "userGhost" in upsert: for ghost in upsert["userGhost"]: - await self.data.profile.put_profile_ghost(user_id, self.version, ghost) + self.data.profile.put_profile_ghost(user_id, self.version, ghost) if "userRecentRatingList" in upsert: - await self.data.profile.put_recent_rating( - user_id, upsert["userRecentRatingList"] - ) + self.data.profile.put_recent_rating(user_id, upsert["userRecentRatingList"]) if "userOption" in upsert and len(upsert["userOption"]) > 0: upsert["userOption"][0].pop("userId") - await self.data.profile.put_profile_option( + self.data.profile.put_profile_option( user_id, self.version, upsert["userOption"][0], False ) if "userRatingList" in upsert and len(upsert["userRatingList"]) > 0: - await self.data.profile.put_profile_rating( + self.data.profile.put_profile_rating( user_id, self.version, upsert["userRatingList"][0] ) if "userActivityList" in upsert and len(upsert["userActivityList"]) > 0: for act in upsert["userActivityList"]: - await self.data.profile.put_profile_activity(user_id, act) + 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", "") - await self.data.item.put_charge( + self.data.item.put_charge( user_id, charge["chargeId"], charge["stock"], charge["purchaseDate"], - charge["validDate"], + charge["validDate"] ) if "userCharacterList" in upsert and len(upsert["userCharacterList"]) > 0: for char in upsert["userCharacterList"]: - await self.data.item.put_character_(user_id, char) + self.data.item.put_character_( + user_id, + char + ) if "userItemList" in upsert and len(upsert["userItemList"]) > 0: for item in upsert["userItemList"]: - await self.data.item.put_item( - user_id, int(item["itemKind"]), item["itemId"], item["stock"], True + self.data.item.put_item( + user_id, + int(item["itemKind"]), + item["itemId"], + item["stock"], + True ) if "userLoginBonusList" in upsert and len(upsert["userLoginBonusList"]) > 0: for login_bonus in upsert["userLoginBonusList"]: - await self.data.item.put_login_bonus( + self.data.item.put_login_bonus( user_id, login_bonus["bonusId"], login_bonus["point"], @@ -370,7 +340,7 @@ class Mai2Base: if "userMapList" in upsert and len(upsert["userMapList"]) > 0: for map in upsert["userMapList"]: - await self.data.item.put_map( + self.data.item.put_map( user_id, map["mapId"], map["distance"], @@ -381,17 +351,15 @@ class Mai2Base: if "userMusicDetailList" in upsert and len(upsert["userMusicDetailList"]) > 0: for music in upsert["userMusicDetailList"]: - await self.data.score.put_best_score(user_id, music, False) + self.data.score.put_best_score(user_id, music, False) if "userCourseList" in upsert and len(upsert["userCourseList"]) > 0: for course in upsert["userCourseList"]: - await self.data.score.put_course(user_id, course) + self.data.score.put_course(user_id, course) if "userFavoriteList" in upsert and len(upsert["userFavoriteList"]) > 0: for fav in upsert["userFavoriteList"]: - await self.data.item.put_favorite( - user_id, fav["kind"], fav["itemIdList"] - ) + self.data.item.put_favorite(user_id, fav["kind"], fav["itemIdList"]) if ( "userFriendSeasonRankingList" in upsert @@ -403,17 +371,15 @@ class Mai2Base: fsr["recordDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0" ), ) - await self.data.item.put_friend_season_ranking(user_id, fsr) + self.data.item.put_friend_season_ranking(user_id, fsr) return {"returnCode": 1, "apiName": "UpsertUserAllApi"} - async def handle_user_logout_api_request(self, data: Dict) -> Dict: + def handle_user_logout_api_request(self, data: Dict) -> Dict: return {"returnCode": 1} - async def handle_get_user_data_api_request(self, data: Dict) -> Dict: - profile = await self.data.profile.get_profile_detail( - data["userId"], self.version, False - ) + def handle_get_user_data_api_request(self, data: Dict) -> Dict: + profile = self.data.profile.get_profile_detail(data["userId"], self.version, False) if profile is None: return @@ -424,10 +390,8 @@ class Mai2Base: return {"userId": data["userId"], "userData": profile_dict} - async def handle_get_user_extend_api_request(self, data: Dict) -> Dict: - extend = await self.data.profile.get_profile_extend( - data["userId"], self.version - ) + 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 @@ -438,10 +402,8 @@ class Mai2Base: return {"userId": data["userId"], "userExtend": extend_dict} - async def handle_get_user_option_api_request(self, data: Dict) -> Dict: - options = await self.data.profile.get_profile_option( - data["userId"], self.version, False - ) + def handle_get_user_option_api_request(self, data: Dict) -> Dict: + options = self.data.profile.get_profile_option(data["userId"], self.version, False) if options is None: return @@ -452,8 +414,8 @@ class Mai2Base: return {"userId": data["userId"], "userOption": options_dict} - async def handle_get_user_card_api_request(self, data: Dict) -> Dict: - user_cards = await self.data.item.get_cards(data["userId"]) + 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": []} @@ -486,8 +448,8 @@ class Mai2Base: "userCardList": card_list[start_idx:end_idx], } - async def handle_get_user_charge_api_request(self, data: Dict) -> Dict: - user_charges = await self.data.item.get_charges(data["userId"]) + 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": []} @@ -505,33 +467,29 @@ class Mai2Base: "userChargeList": user_charge_list, } - async def handle_get_user_present_api_request(self, data: Dict) -> Dict: - return {"userId": data.get("userId", 0), "length": 0, "userPresentList": []} - - async def handle_get_transfer_friend_api_request(self, data: Dict) -> Dict: + 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 {} - async def handle_get_user_present_event_api_request(self, data: Dict) -> Dict: - return { - "userId": data.get("userId", 0), - "length": 0, - "userPresentEventList": [], - } - - async def handle_get_user_boss_api_request(self, data: Dict) -> Dict: - b = await self.data.profile.get_boss_list(data["userId"]) + 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": {}} + 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} + return { "userId": data.get("userId", 0), "userBossData": boss_lst} - async def handle_get_user_item_api_request(self, data: Dict) -> Dict: + def handle_get_user_item_api_request(self, data: Dict) -> Dict: kind = int(data["nextIndex"] / 10000000000) next_idx = int(data["nextIndex"] % 10000000000) - user_item_list = await self.data.item.get_items(data["userId"], kind) + 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)): @@ -556,8 +514,8 @@ class Mai2Base: "userItemList": items, } - async def handle_get_user_character_api_request(self, data: Dict) -> Dict: - characters = await self.data.item.get_characters(data["userId"]) + 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: @@ -570,8 +528,8 @@ class Mai2Base: return {"userId": data["userId"], "userCharacterList": chara_list} - async def handle_get_user_favorite_api_request(self, data: Dict) -> Dict: - favorites = await self.data.item.get_favorites(data["userId"], data["itemKind"]) + 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 @@ -587,8 +545,8 @@ class Mai2Base: return {"userId": data["userId"], "userFavoriteData": userFavs} - async def handle_get_user_ghost_api_request(self, data: Dict) -> Dict: - ghost = await self.data.profile.get_profile_ghost(data["userId"], self.version) + 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 @@ -599,24 +557,18 @@ class Mai2Base: return {"userId": data["userId"], "userGhost": ghost_dict} - async def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict: - rating = await self.data.profile.get_recent_rating(data["userId"]) + 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} - return { - "userId": data["userId"], - "length": len(lst), - "userRecentRatingList": lst, - } - - async def handle_get_user_rating_api_request(self, data: Dict) -> Dict: - rating = await self.data.profile.get_profile_rating( - data["userId"], self.version - ) + 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 @@ -627,12 +579,12 @@ class Mai2Base: return {"userId": data["userId"], "userRating": rating_dict} - async def handle_get_user_activity_api_request(self, data: Dict) -> Dict: + def handle_get_user_activity_api_request(self, data: Dict) -> Dict: """ kind 1 is playlist, kind 2 is music list """ - playlist = await self.data.profile.get_profile_activity(data["userId"], 1) - musiclist = await self.data.profile.get_profile_activity(data["userId"], 2) + 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 @@ -655,8 +607,8 @@ class Mai2Base: return {"userActivity": {"playList": plst, "musicList": mlst}} - async def handle_get_user_course_api_request(self, data: Dict) -> Dict: - user_courses = await self.data.score.get_courses(data["userId"]) + 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": []} @@ -669,16 +621,12 @@ class Mai2Base: return {"userId": data["userId"], "nextIndex": 0, "userCourseList": course_list} - async def handle_get_user_portrait_api_request(self, data: Dict) -> Dict: + def handle_get_user_portrait_api_request(self, data: Dict) -> Dict: # No support for custom pfps return {"length": 0, "userPortraitList": []} - async def handle_get_user_friend_season_ranking_api_request( - self, data: Dict - ) -> Dict: - friend_season_ranking = await self.data.item.get_friend_season_ranking( - data["userId"] - ) + 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"], @@ -713,8 +661,8 @@ class Mai2Base: "userFriendSeasonRankingList": friend_season_ranking_list, } - async def handle_get_user_map_api_request(self, data: Dict) -> Dict: - maps = await self.data.item.get_maps(data["userId"]) + 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"], @@ -746,8 +694,8 @@ class Mai2Base: "userMapList": map_list, } - async def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: - login_bonuses = await self.data.item.get_login_bonuses(data["userId"]) + 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"], @@ -779,67 +727,53 @@ class Mai2Base: "userLoginBonusList": login_bonus_list, } - async def handle_get_user_region_api_request(self, data: Dict) -> Dict: + def handle_get_user_region_api_request(self, data: Dict) -> Dict: return {"userId": data["userId"], "length": 0, "userRegionList": []} - - async def handle_get_user_web_option_api_request(self, data: Dict) -> Dict: - w = await self.data.profile.get_web_option(data["userId"], self.version) - if w is None: + + 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 = w._asdict() web_opt.pop("id") web_opt.pop("user") web_opt.pop("version") return {"userId": data["userId"], "userWebOption": web_opt} - async def handle_get_user_survival_api_request(self, data: Dict) -> Dict: + def handle_get_user_survival_api_request(self, data: Dict) -> Dict: return {"userId": data["userId"], "length": 0, "userSurvivalList": []} - async def handle_get_user_grade_api_request(self, data: Dict) -> Dict: - g = await self.data.profile.get_grade_status(data["userId"]) + 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": [], - } + 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": [], - } + return {"userId": data["userId"], "userGradeStatus": grade_stat, "length": 0, "userGradeList": []} - async def handle_get_user_music_api_request(self, data: Dict) -> Dict: - user_id = data.get("userId", 0) + 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" - ) + self.logger.warning("handle_get_user_music_api_request: Could not find userid in data, or userId is 0") return {} - - songs = await self.data.score.get_best_scores(user_id, is_dx=False) + + 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!" - ) + self.logger.debug("handle_get_user_music_api_request: get_best_scores returned None!") return { - "userId": data["userId"], - "nextIndex": 0, - "userMusicList": [], - } + "userId": data["userId"], + "nextIndex": 0, + "userMusicList": [], + } num_user_songs = len(songs) @@ -852,35 +786,26 @@ class Mai2Base: 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})" - ) + 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}], } - async def handle_upload_user_portrait_api_request(self, data: Dict) -> Dict: + def handle_upload_user_portrait_api_request(self, data: Dict) -> Dict: self.logger.debug(data) - async 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"} + 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"} - + 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)) @@ -890,95 +815,72 @@ class Mai2Base: 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 - ): + 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"} - + 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"} + 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"} - + 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"} + 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"} - + 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 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"} - + 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"} - + 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"} - + 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"} + 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" - ) + self.logger.error(f"Failed to delete {out_name}.bin, please remove it manually") - return {"returnCode": ret_code, "apiName": "UploadUserPhotoApi"} + return {'returnCode': ret_code, 'apiName': 'UploadUserPhotoApi'} diff --git a/titles/mai2/buddies.py b/titles/mai2/buddies.py index 38049a1..ccc73bb 100644 --- a/titles/mai2/buddies.py +++ b/titles/mai2/buddies.py @@ -11,8 +11,8 @@ class Mai2Buddies(Mai2FestivalPlus): super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_BUDDIES - async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: - user_data = await super().handle_cm_get_user_preview_api_request(data) + 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.40.00" diff --git a/titles/mai2/config.py b/titles/mai2/config.py index 8f28585..850e0c1 100644 --- a/titles/mai2/config.py +++ b/titles/mai2/config.py @@ -19,7 +19,6 @@ class Mai2ServerConfig: ) ) - class Mai2DeliverConfig: def __init__(self, parent: "Mai2Config") -> None: self.__config = parent @@ -35,18 +34,17 @@ class Mai2DeliverConfig: 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( diff --git a/titles/mai2/database.py b/titles/mai2/database.py index 336d810..be9e518 100644 --- a/titles/mai2/database.py +++ b/titles/mai2/database.py @@ -1,10 +1,10 @@ -from core.config import CoreConfig from core.data import Data +from core.config import CoreConfig from titles.mai2.schema import ( Mai2ItemData, Mai2ProfileData, - Mai2ScoreData, Mai2StaticData, + Mai2ScoreData, ) diff --git a/titles/mai2/dx.py b/titles/mai2/dx.py index 983f37c..b1890ae 100644 --- a/titles/mai2/dx.py +++ b/titles/mai2/dx.py @@ -1,5 +1,8 @@ -from datetime import datetime -from typing import Any, Dict, List +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 @@ -12,35 +15,27 @@ class Mai2DX(Mai2Base): super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX - async def handle_get_game_setting_api_request(self, data: Dict): + 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", + "rebootEndTime": "2020-01-01 07:00:00.1", "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 "" - ), + "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 "" - ), + "usbDlServerUri": self.old_server + "usbdl/" if self.can_deliver and self.game_config.deliver.udbdl_enable else "", "rebootInterval": 0, }, "isAouAccession": False, } - async def handle_get_user_preview_api_request(self, data: Dict) -> Dict: - p = await self.data.profile.get_profile_detail(data["userId"], self.version) - o = await self.data.profile.get_profile_option(data["userId"], self.version) + 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() @@ -69,26 +64,26 @@ class Mai2DX(Mai2Base): "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+ + "banState": profile["banState"] + if profile["banState"] is not None + else 0, # New with uni+ } - async def handle_upload_user_playlog_api_request(self, data: Dict) -> Dict: + def handle_upload_user_playlog_api_request(self, data: Dict) -> Dict: user_id = data["userId"] playlog = data["userPlaylog"] - await self.data.score.put_playlog(user_id, playlog) + self.data.score.put_playlog(user_id, playlog) return {"returnCode": 1, "apiName": "UploadUserPlaylogApi"} - async def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: + 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", "") - await self.data.item.put_charge( + self.data.item.put_charge( user_id, charge["chargeId"], charge["stock"], @@ -98,10 +93,10 @@ class Mai2DX(Mai2Base): return {"returnCode": 1, "apiName": "UpsertUserChargelogApi"} - async def handle_upsert_user_all_api_request(self, data: Dict) -> Dict: + 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"} @@ -109,39 +104,39 @@ class Mai2DX(Mai2Base): if "userData" in upsert and len(upsert["userData"]) > 0: upsert["userData"][0]["isNetMember"] = 1 upsert["userData"][0].pop("accessCode") - await self.data.profile.put_profile_detail( + self.data.profile.put_profile_detail( user_id, self.version, upsert["userData"][0] ) if "userExtend" in upsert and len(upsert["userExtend"]) > 0: - await self.data.profile.put_profile_extend( + self.data.profile.put_profile_extend( user_id, self.version, upsert["userExtend"][0] ) if "userGhost" in upsert: for ghost in upsert["userGhost"]: - await self.data.profile.put_profile_ghost(user_id, self.version, ghost) + self.data.profile.put_profile_ghost(user_id, self.version, ghost) if "userOption" in upsert and len(upsert["userOption"]) > 0: - await self.data.profile.put_profile_option( + self.data.profile.put_profile_option( user_id, self.version, upsert["userOption"][0] ) if "userRatingList" in upsert and len(upsert["userRatingList"]) > 0: - await self.data.profile.put_profile_rating( + 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: - await self.data.profile.put_profile_activity(user_id, act) + 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", "") - await self.data.item.put_charge( + self.data.item.put_charge( user_id, charge["chargeId"], charge["stock"], @@ -155,7 +150,7 @@ class Mai2DX(Mai2Base): if "userCharacterList" in upsert and len(upsert["userCharacterList"]) > 0: for char in upsert["userCharacterList"]: - await self.data.item.put_character( + self.data.item.put_character( user_id, char["characterId"], char["level"], @@ -165,7 +160,7 @@ class Mai2DX(Mai2Base): if "userItemList" in upsert and len(upsert["userItemList"]) > 0: for item in upsert["userItemList"]: - await self.data.item.put_item( + self.data.item.put_item( user_id, int(item["itemKind"]), item["itemId"], @@ -175,7 +170,7 @@ class Mai2DX(Mai2Base): if "userLoginBonusList" in upsert and len(upsert["userLoginBonusList"]) > 0: for login_bonus in upsert["userLoginBonusList"]: - await self.data.item.put_login_bonus( + self.data.item.put_login_bonus( user_id, login_bonus["bonusId"], login_bonus["point"], @@ -185,7 +180,7 @@ class Mai2DX(Mai2Base): if "userMapList" in upsert and len(upsert["userMapList"]) > 0: for map in upsert["userMapList"]: - await self.data.item.put_map( + self.data.item.put_map( user_id, map["mapId"], map["distance"], @@ -196,17 +191,15 @@ class Mai2DX(Mai2Base): if "userMusicDetailList" in upsert and len(upsert["userMusicDetailList"]) > 0: for music in upsert["userMusicDetailList"]: - await self.data.score.put_best_score(user_id, music) + self.data.score.put_best_score(user_id, music) if "userCourseList" in upsert and len(upsert["userCourseList"]) > 0: for course in upsert["userCourseList"]: - await self.data.score.put_course(user_id, course) + self.data.score.put_course(user_id, course) if "userFavoriteList" in upsert and len(upsert["userFavoriteList"]) > 0: for fav in upsert["userFavoriteList"]: - await self.data.item.put_favorite( - user_id, fav["kind"], fav["itemIdList"] - ) + self.data.item.put_favorite(user_id, fav["kind"], fav["itemIdList"]) if ( "userFriendSeasonRankingList" in upsert @@ -218,17 +211,12 @@ class Mai2DX(Mai2Base): fsr["recordDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0" ), ) - await self.data.item.put_friend_season_ranking(user_id, fsr) - - if "user2pPlaylog" in upsert: - await self.data.score.put_2p_playlog(user_id, upsert["user2pPlaylog"]) + self.data.item.put_friend_season_ranking(user_id, fsr) return {"returnCode": 1, "apiName": "UpsertUserAllApi"} - async def handle_get_user_data_api_request(self, data: Dict) -> Dict: - profile = await self.data.profile.get_profile_detail( - data["userId"], self.version - ) + 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 @@ -239,10 +227,8 @@ class Mai2DX(Mai2Base): return {"userId": data["userId"], "userData": profile_dict} - async def handle_get_user_extend_api_request(self, data: Dict) -> Dict: - extend = await self.data.profile.get_profile_extend( - data["userId"], self.version - ) + 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 @@ -253,10 +239,8 @@ class Mai2DX(Mai2Base): return {"userId": data["userId"], "userExtend": extend_dict} - async def handle_get_user_option_api_request(self, data: Dict) -> Dict: - options = await self.data.profile.get_profile_option( - data["userId"], self.version - ) + 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 @@ -267,8 +251,8 @@ class Mai2DX(Mai2Base): return {"userId": data["userId"], "userOption": options_dict} - async def handle_get_user_card_api_request(self, data: Dict) -> Dict: - user_cards = await self.data.item.get_cards(data["userId"]) + 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": []} @@ -301,10 +285,35 @@ class Mai2DX(Mai2Base): "userCardList": card_list[start_idx:end_idx], } - async def handle_get_user_item_api_request(self, data: Dict) -> Dict: + 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 = await self.data.item.get_items(data["userId"], kind) + 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)): @@ -329,8 +338,8 @@ class Mai2DX(Mai2Base): "userItemList": items, } - async def handle_get_user_character_api_request(self, data: Dict) -> Dict: - characters = await self.data.item.get_characters(data["userId"]) + 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: @@ -341,8 +350,8 @@ class Mai2DX(Mai2Base): return {"userId": data["userId"], "userCharacterList": chara_list} - async def handle_get_user_favorite_api_request(self, data: Dict) -> Dict: - favorites = await self.data.item.get_favorites(data["userId"], data["itemKind"]) + 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 @@ -358,8 +367,8 @@ class Mai2DX(Mai2Base): return {"userId": data["userId"], "userFavoriteData": userFavs} - async def handle_get_user_ghost_api_request(self, data: Dict) -> Dict: - ghost = await self.data.profile.get_profile_ghost(data["userId"], self.version) + 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 @@ -370,10 +379,8 @@ class Mai2DX(Mai2Base): return {"userId": data["userId"], "userGhost": ghost_dict} - async def handle_get_user_rating_api_request(self, data: Dict) -> Dict: - rating = await self.data.profile.get_profile_rating( - data["userId"], self.version - ) + 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 @@ -384,12 +391,12 @@ class Mai2DX(Mai2Base): return {"userId": data["userId"], "userRating": rating_dict} - async def handle_get_user_activity_api_request(self, data: Dict) -> Dict: + def handle_get_user_activity_api_request(self, data: Dict) -> Dict: """ kind 1 is playlist, kind 2 is music list """ - playlist = await self.data.profile.get_profile_activity(data["userId"], 1) - musiclist = await self.data.profile.get_profile_activity(data["userId"], 2) + 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 @@ -412,8 +419,8 @@ class Mai2DX(Mai2Base): return {"userActivity": {"playList": plst, "musicList": mlst}} - async def handle_get_user_course_api_request(self, data: Dict) -> Dict: - user_courses = await self.data.score.get_courses(data["userId"]) + 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": []} @@ -426,16 +433,12 @@ class Mai2DX(Mai2Base): return {"userId": data["userId"], "nextIndex": 0, "userCourseList": course_list} - async def handle_get_user_portrait_api_request(self, data: Dict) -> Dict: + def handle_get_user_portrait_api_request(self, data: Dict) -> Dict: # No support for custom pfps return {"length": 0, "userPortraitList": []} - async def handle_get_user_friend_season_ranking_api_request( - self, data: Dict - ) -> Dict: - friend_season_ranking = await self.data.item.get_friend_season_ranking( - data["userId"] - ) + 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"], @@ -470,8 +473,8 @@ class Mai2DX(Mai2Base): "userFriendSeasonRankingList": friend_season_ranking_list, } - async def handle_get_user_map_api_request(self, data: Dict) -> Dict: - maps = await self.data.item.get_maps(data["userId"]) + 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"], @@ -503,8 +506,8 @@ class Mai2DX(Mai2Base): "userMapList": map_list, } - async def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: - login_bonuses = await self.data.item.get_login_bonuses(data["userId"]) + 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"], @@ -536,7 +539,7 @@ class Mai2DX(Mai2Base): "userLoginBonusList": login_bonus_list, } - async def handle_get_user_region_api_request(self, data: Dict) -> Dict: + def handle_get_user_region_api_request(self, data: Dict) -> Dict: """ class UserRegionList: regionId: int @@ -545,7 +548,7 @@ class Mai2DX(Mai2Base): """ return {"userId": data["userId"], "length": 0, "userRegionList": []} - async def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict: + def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict: user_id = data["userId"] rival_id = data["rivalId"] @@ -556,7 +559,7 @@ class Mai2DX(Mai2Base): """ return {"userId": user_id, "userRivalData": {}} - async def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict: + def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict: user_id = data["userId"] rival_id = data["rivalId"] next_idx = data["nextIndex"] @@ -574,29 +577,25 @@ class Mai2DX(Mai2Base): """ return {"userId": user_id, "nextIndex": 0, "userRivalMusicList": []} - async def handle_get_user_music_api_request(self, data: Dict) -> Dict: - user_id = data.get("userId", 0) + 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" - ) + self.logger.warning("handle_get_user_music_api_request: Could not find userid in data, or userId is 0") return {} - - songs = await self.data.score.get_best_scores(user_id) + + 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!" - ) + self.logger.debug("handle_get_user_music_api_request: get_best_scores returned None!") return { - "userId": data["userId"], - "nextIndex": 0, - "userMusicList": [], - } + "userId": data["userId"], + "nextIndex": 0, + "userMusicList": [], + } num_user_songs = len(songs) @@ -609,23 +608,17 @@ class Mai2DX(Mai2Base): 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})" - ) + 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}], } - async def handle_user_login_api_request(self, data: Dict) -> Dict: - ret = await super().handle_user_login_api_request(data) + 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) + ret['loginId'] = ret.get('loginCount', 0) return ret diff --git a/titles/mai2/dxplus.py b/titles/mai2/dxplus.py index bd3b3a1..9062ff5 100644 --- a/titles/mai2/dxplus.py +++ b/titles/mai2/dxplus.py @@ -1,7 +1,12 @@ +from typing import Any, List, Dict +from datetime import datetime, timedelta +import pytz +import json + from core.config import CoreConfig +from titles.mai2.dx import Mai2DX from titles.mai2.config import Mai2Config from titles.mai2.const import Mai2Constants -from titles.mai2.dx import Mai2DX class Mai2DXPlus(Mai2DX): diff --git a/titles/mai2/festival.py b/titles/mai2/festival.py index 4e9846f..145fa71 100644 --- a/titles/mai2/festival.py +++ b/titles/mai2/festival.py @@ -1,9 +1,9 @@ from typing import Dict from core.config import CoreConfig -from titles.mai2.config import Mai2Config -from titles.mai2.const import Mai2Constants from titles.mai2.universeplus import Mai2UniversePlus +from titles.mai2.const import Mai2Constants +from titles.mai2.config import Mai2Config class Mai2Festival(Mai2UniversePlus): @@ -11,30 +11,26 @@ class Mai2Festival(Mai2UniversePlus): super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_FESTIVAL - async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: - user_data = await super().handle_cm_get_user_preview_api_request(data) + 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 - async def handle_user_login_api_request(self, data: Dict) -> Dict: - user_login = await super().handle_user_login_api_request(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 - async def handle_get_user_recommend_rate_music_api_request( - self, data: Dict - ) -> Dict: + def handle_get_user_recommend_rate_music_api_request(self, data: Dict) -> Dict: """ userRecommendRateMusicIdList: list[int] """ return {"userId": data["userId"], "userRecommendRateMusicIdList": []} - async def handle_get_user_recommend_select_music_api_request( - self, data: Dict - ) -> Dict: + def handle_get_user_recommend_select_music_api_request(self, data: Dict) -> Dict: """ userRecommendSelectionMusicIdList: list[int] """ diff --git a/titles/mai2/festivalplus.py b/titles/mai2/festivalplus.py index ee5088d..7deeb98 100644 --- a/titles/mai2/festivalplus.py +++ b/titles/mai2/festivalplus.py @@ -1,9 +1,9 @@ from typing import Dict from core.config import CoreConfig -from titles.mai2.config import Mai2Config -from titles.mai2.const import Mai2Constants from titles.mai2.festival import Mai2Festival +from titles.mai2.const import Mai2Constants +from titles.mai2.config import Mai2Config class Mai2FestivalPlus(Mai2Festival): @@ -11,14 +11,14 @@ class Mai2FestivalPlus(Mai2Festival): super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS - async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: - user_data = await super().handle_cm_get_user_preview_api_request(data) + 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 - async def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict: + 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) diff --git a/titles/mai2/finale.py b/titles/mai2/finale.py index a6bb3c6..f9f1d88 100644 --- a/titles/mai2/finale.py +++ b/titles/mai2/finale.py @@ -1,3 +1,8 @@ +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 diff --git a/titles/mai2/index.py b/titles/mai2/index.py index a82df92..b4cb228 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -1,23 +1,21 @@ +from twisted.web.http import Request +from twisted.web.server import NOT_DONE_YET import json -import logging -import zlib -from logging.handlers import TimedRotatingFileHandler -from os import mkdir, path -from typing import List, Tuple - -import coloredlogs import inflection import yaml -from core.config import CoreConfig -from core.title import BaseServlet -from core.utils import Utils -from starlette.requests import Request -from starlette.responses import JSONResponse, Response -from starlette.routing import Route +import string +import logging, coloredlogs +import zlib +from logging.handlers import TimedRotatingFileHandler +from os import path, mkdir +from typing import Tuple, List, Dict -from .base import Mai2Base +from core.config import CoreConfig +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 @@ -102,65 +100,36 @@ class Mai2Servlet(BaseServlet): if not game_cfg.server.enable: return False - + return True - - def get_routes(self) -> List[Route]: - return [ - Route( - "/{version:int}/MaimaiServlet/api/movie/{endpoint:str}", - self.handle_movie, - methods=["GET", "POST"], - ), - Route( - "/{version:int}/MaimaiServlet/old/{endpoint:str}", self.handle_old_srv - ), - Route( - "/{version:int}/MaimaiServlet/old/{endpoint:str}/{placeid:str}/{keychip:str}/{userid:int}", - self.handle_old_srv_userdata, - ), - Route( - "/{version:int}/MaimaiServlet/old/{endpoint:str}/{userid:int}", - self.handle_old_srv_userdata, - ), - Route( - "/{version:int}/MaimaiServlet/old/{endpoint:str}/{userid:int}", - self.handle_old_srv_userdata, - ), - Route( - "/{version:int}/MaimaiServlet/usbdl/{endpoint:str}", self.handle_usbdl - ), - Route( - "/{version:int}/MaimaiServlet/deliver/{endpoint:str}", - self.handle_deliver, - ), - Route( - "/{version:int}/MaimaiServlet/{endpoint:str}", - self.handle_mai, - methods=["POST"], - ), - Route( - "/{game:str}/{version:int}/Maimai2Servlet/{endpoint:str}", - self.handle_mai2, - methods=["POST"], - ), - ] - - 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 - ): + + 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.server.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_code}/{game_ver}/", - f"{self.core_cfg.server.hostname}", + f"http://{self.core_cfg.title.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_ver}/", + f"{self.core_cfg.title.hostname}", ) return ( - f"http://{self.core_cfg.server.hostname}/{game_code}/{game_ver}/", - f"{self.core_cfg.server.hostname}", + f"http://{self.core_cfg.title.hostname}/{game_ver}/", + f"{self.core_cfg.title.hostname}", ) def setup(self): @@ -188,25 +157,16 @@ class Mai2Servlet(BaseServlet): f"Failed to make movie upload directory at {self.game_cfg.uploads.movies_dir}" ) - async def handle_movie(self, request: Request): - return JSONResponse() - - async def handle_usbdl(self, request: Request): - return Response("OK") - - async def handle_deliver(self, request: Request): - return Response(status_code=404) - - async def handle_mai(self, request: Request) -> bytes: - endpoint: str = request.path_params.get("endpoint") - version: int = request.path_params.get("version") + def handle_mai(self, request: Request, game_code: str, matchers: Dict) -> bytes: + endpoint = matchers['endpoint'] + version = int(matchers['version']) if endpoint.lower() == "ping": - return Response(zlib.compress(b'{"returnCode": "1"}')) - - req_raw = await request.body() + return zlib.compress(b'{"returnCode": "1"}') + + req_raw = request.content.getvalue() internal_ver = 0 client_ip = Utils.get_ip_addr(request) - + if version < 110: # 1.0 internal_ver = Mai2Constants.VER_MAIMAI elif version >= 110 and version < 120: # Plus @@ -241,7 +201,7 @@ class Mai2Servlet(BaseServlet): self.logger.error( f"Failed to decompress v{version} {endpoint} request -> {e}" ) - return Response(zlib.compress(b'{"stat": "0"}')) + return zlib.compress(b'{"stat": "0"}') req_data = json.loads(unzip) @@ -258,29 +218,26 @@ class Mai2Servlet(BaseServlet): else: try: handler = getattr(handler_cls, func_to_find) - resp = await handler(req_data) + resp = handler(req_data) except Exception as e: self.logger.error(f"Error handling v{version} method {endpoint} - {e}") - return Response(zlib.compress(b'{"returnCode": "0"}')) + return zlib.compress(b'{"returnCode": "0"}') if resp == None: resp = {"returnCode": 1} self.logger.debug(f"Response {resp}") - return Response( - zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) - ) + return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) - async def handle_mai2(self, request: Request) -> bytes: - endpoint: str = request.path_params.get("endpoint") - version: int = request.path_params.get("version") - game_code: str = request.path_params.get("game") + def handle_mai2(self, request: Request, game_code: str, matchers: Dict) -> bytes: + endpoint = matchers['endpoint'] + version = int(matchers['version']) if endpoint.lower() == "ping": - return Response(zlib.compress(b'{"returnCode": "1"}')) + return zlib.compress(b'{"returnCode": "1"}') - req_raw = await request.body() + req_raw = request.content.getvalue() internal_ver = 0 client_ip = Utils.get_ip_addr(request) if version < 105: # 1.0 @@ -303,17 +260,17 @@ class Mai2Servlet(BaseServlet): internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES if ( - request.headers.get("Mai-Encoding") is not None - or request.headers.get("X-Mai-Encoding") is not None + 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.headers.get("Mai-Encoding") + enc_ver = request.getHeader("Mai-Encoding") if enc_ver is None: - enc_ver = request.headers.get("X-Mai-Encoding") + enc_ver = request.getHeader("X-Mai-Encoding") self.logger.debug( - f"Encryption v{enc_ver} - User-Agent: {request.headers.get('User-Agent')}" + f"Encryption v{enc_ver} - User-Agent: {request.getHeader('User-Agent')}" ) try: @@ -323,7 +280,7 @@ class Mai2Servlet(BaseServlet): self.logger.error( f"Failed to decompress v{version} {endpoint} request -> {e}" ) - return Response(zlib.compress(b'{"stat": "0"}')) + return zlib.compress(b'{"stat": "0"}') req_data = json.loads(unzip) @@ -340,29 +297,80 @@ class Mai2Servlet(BaseServlet): else: try: handler = getattr(handler_cls, func_to_find) - resp = await handler(req_data) + resp = handler(req_data) except Exception as e: self.logger.error(f"Error handling v{version} method {endpoint} - {e}") - return Response(zlib.compress(b'{"stat": "0"}')) + return zlib.compress(b'{"stat": "0"}') if resp == None: resp = {"returnCode": 1} self.logger.debug(f"Response {resp}") - return Response( - zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) - ) + return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) - async def handle_old_srv(self, request: Request) -> bytes: - endpoint = request.path_params.get("endpoint") - version = request.path_params.get("version") + 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 Response(zlib.compress(b"ok")) + return zlib.compress(b"ok") - async def handle_old_srv_userdata(self, request: Request) -> bytes: - endpoint = request.path_params.get("endpoint") - version = request.path_params.get("version") + 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 Response(zlib.compress(b"{}")) + 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 45604be..cc4f678 100644 --- a/titles/mai2/read.py +++ b/titles/mai2/read.py @@ -1,12 +1,15 @@ -import codecs +from decimal import Decimal +import logging import os import re import xml.etree.ElementTree as ET +from typing import Any, Dict, List, Optional +from Crypto.Cipher import AES import zlib -from typing import Dict, List, Optional +import codecs from core.config import CoreConfig -from Crypto.Cipher import AES +from core.data import Data from read import BaseReader from titles.mai2.const import Mai2Constants from titles.mai2.database import Mai2Data @@ -32,7 +35,7 @@ class Mai2Reader(BaseReader): self.logger.error(f"Invalid maimai DX version {version}") exit(1) - async def read(self) -> None: + def read(self) -> None: data_dirs = [] if self.version >= Mai2Constants.VER_MAIMAI_DX: if self.bin_dir is not None: @@ -43,84 +46,64 @@ class Mai2Reader(BaseReader): for dir in data_dirs: self.logger.info(f"Read from {dir}") - await self.get_events(f"{dir}/event") - await self.disable_events(f"{dir}/information", f"{dir}/scoreRanking") - await self.read_music(f"{dir}/music") - await self.read_tickets(f"{dir}/ticket") - + 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") + 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" - ) + 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 - ) - - await self.read_old_events(evt_table) - await self.read_old_music(score_table, txt_table) - + + 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 - ) + 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) - await self.read_old_events(evt_table) - await self.read_old_music(score_table, txt_table) + 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]]]: + + 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" - ) + self.logger.warning(f"file {file} does not exist in directory {dir}, skipping") return - + 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_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() @@ -135,31 +118,31 @@ class Mai2Reader(BaseReader): 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]) + 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(".")] + + name = file[:file.index(".")] if "_" in name: - name = name[: file.index("_")] - + 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 @@ -167,36 +150,36 @@ class Mai2Reader(BaseReader): 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('"') + 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 - + line_dict[f'item_{y}'] = stripped + if comment: - line_dict["comment"] = 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 - async def get_events(self, base_dir: str) -> None: + def get_events(self, base_dir: str) -> None: self.logger.info(f"Reading events from {base_dir}...") for root, dirs, files in os.walk(base_dir): @@ -209,12 +192,12 @@ class Mai2Reader(BaseReader): id = int(troot.find("name").find("id").text) event_type = int(troot.find("infoType").text) - await self.data.static.put_game_event( + self.data.static.put_game_event( self.version, event_type, id, name ) self.logger.info(f"Added event {id}...") - async def disable_events( + 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}...") @@ -227,7 +210,7 @@ class Mai2Reader(BaseReader): event_id = int(troot.find("name").find("id").text) - await self.data.static.toggle_game_event( + self.data.static.toggle_game_event( self.version, event_id, toggle=False ) self.logger.info(f"Disabled event {event_id}...") @@ -240,7 +223,7 @@ class Mai2Reader(BaseReader): event_id = int(troot.find("eventName").find("id").text) - await self.data.static.toggle_game_event( + self.data.static.toggle_game_event( self.version, event_id, toggle=False ) self.logger.info(f"Disabled event {event_id}...") @@ -269,12 +252,10 @@ class Mai2Reader(BaseReader): 22091518, 22091519, ]: - await self.data.static.toggle_game_event( - self.version, event_id, toggle=False - ) + self.data.static.toggle_game_event(self.version, event_id, toggle=False) self.logger.info(f"Disabled event {event_id}...") - async def read_music(self, base_dir: str) -> None: + 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): @@ -304,7 +285,7 @@ class Mai2Reader(BaseReader): dif.find("notesDesigner").find("str").text ) - await self.data.static.put_game_music( + self.data.static.put_game_music( self.version, song_id, chart_id, @@ -321,7 +302,7 @@ class Mai2Reader(BaseReader): f"Added music id {song_id} chart {chart_id}" ) - async def read_tickets(self, base_dir: str) -> None: + def read_tickets(self, base_dir: str) -> None: self.logger.info(f"Reading tickets from {base_dir}...") for root, dirs, files in os.walk(base_dir): @@ -335,32 +316,28 @@ class Mai2Reader(BaseReader): ticket_type = int(troot.find("ticketKind").find("id").text) price = int(troot.find("creditNum").text) - await self.data.static.put_game_ticket( + self.data.static.put_game_ticket( self.version, id, ticket_type, price, name ) self.logger.info(f"Added ticket {id}...") - async def read_old_events(self, events: Optional[List[Dict[str, str]]]) -> None: + 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}") - - await self.data.static.put_game_event(self.version, 0, evt_id, name) + 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): - await self.data.static.toggle_game_event(self.version, evt_id, False) - - async def read_old_music( - self, - scores: Optional[List[Dict[str, str]]], - text: Optional[List[Dict[str, str]]], - ) -> None: + 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 daf6413..7a8c060 100644 --- a/titles/mai2/schema/__init__.py +++ b/titles/mai2/schema/__init__.py @@ -1,6 +1,6 @@ -from titles.mai2.schema.item import Mai2ItemData from titles.mai2.schema.profile import Mai2ProfileData -from titles.mai2.schema.score import Mai2ScoreData +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] diff --git a/titles/mai2/schema/item.py b/titles/mai2/schema/item.py index 0ace32e..a6ed876 100644 --- a/titles/mai2/schema/item.py +++ b/titles/mai2/schema/item.py @@ -1,13 +1,13 @@ -from datetime import datetime -from typing import Dict, List, Optional - from core.data.schema import BaseData, metadata -from sqlalchemy import Column, Table, UniqueConstraint, and_ + +from datetime import datetime +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON +from sqlalchemy.schema import ForeignKey +from sqlalchemy.sql import func, select from sqlalchemy.dialects.mysql import insert from sqlalchemy.engine import Row -from sqlalchemy.schema import ForeignKey -from sqlalchemy.sql import func -from sqlalchemy.types import JSON, TIMESTAMP, Boolean, Integer, String character = Table( "mai2_item_character", @@ -186,7 +186,7 @@ print_detail = Table( class Mai2ItemData(BaseData): - async def put_item( + def put_item( self, user_id: int, item_kind: int, item_id: int, stock: int, is_valid: bool ) -> None: sql = insert(item).values( @@ -202,7 +202,7 @@ class Mai2ItemData(BaseData): isValid=is_valid, ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning( f"put_item: failed to insert item! user_id: {user_id}, item_kind: {item_kind}, item_id: {item_id}" @@ -210,9 +210,7 @@ class Mai2ItemData(BaseData): return None return result.lastrowid - async def get_items( - self, user_id: int, item_kind: int = None - ) -> Optional[List[Row]]: + 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: @@ -220,14 +218,12 @@ class Mai2ItemData(BaseData): and_(item.c.user == user_id, item.c.itemKind == item_kind) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_item( - self, user_id: int, item_kind: int, item_id: int - ) -> Optional[Row]: + def get_item(self, user_id: int, item_kind: int, item_id: int) -> Optional[Row]: sql = item.select( and_( item.c.user == user_id, @@ -236,12 +232,12 @@ class Mai2ItemData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def put_login_bonus( + def put_login_bonus( self, user_id: int, bonus_id: int, @@ -263,7 +259,7 @@ class Mai2ItemData(BaseData): isComplete=is_complete, ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning( f"put_login_bonus: failed to insert item! user_id: {user_id}, bonus_id: {bonus_id}, point: {point}" @@ -271,25 +267,25 @@ class Mai2ItemData(BaseData): return None return result.lastrowid - async def get_login_bonuses(self, user_id: int) -> Optional[List[Row]]: + def get_login_bonuses(self, user_id: int) -> Optional[List[Row]]: sql = login_bonus.select(login_bonus.c.user == user_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_login_bonus(self, user_id: int, bonus_id: int) -> Optional[Row]: + 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) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def put_map( + def put_map( self, user_id: int, map_id: int, @@ -314,7 +310,7 @@ class Mai2ItemData(BaseData): isComplete=is_complete, ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning( f"put_map: failed to insert item! user_id: {user_id}, map_id: {map_id}, distance: {distance}" @@ -322,28 +318,28 @@ class Mai2ItemData(BaseData): return None return result.lastrowid - async def get_maps(self, user_id: int) -> Optional[List[Row]]: + def get_maps(self, user_id: int) -> Optional[List[Row]]: sql = map.select(map.c.user == user_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_map(self, user_id: int, map_id: int) -> Optional[Row]: + def get_map(self, user_id: int, map_id: int) -> Optional[Row]: sql = map.select(and_(map.c.user == user_id, map.c.mapId == map_id)) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def put_character_(self, user_id: int, char_data: Dict) -> Optional[int]: + 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 = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning( f"put_character_: failed to insert item! user_id: {user_id}" @@ -351,7 +347,7 @@ class Mai2ItemData(BaseData): return None return result.lastrowid - async def put_character( + def put_character( self, user_id: int, character_id: int, @@ -373,7 +369,7 @@ class Mai2ItemData(BaseData): useCount=use_count, ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning( f"put_character: failed to insert item! user_id: {user_id}, character_id: {character_id}, level: {level}" @@ -381,33 +377,33 @@ class Mai2ItemData(BaseData): return None return result.lastrowid - async def get_characters(self, user_id: int) -> Optional[List[Row]]: + def get_characters(self, user_id: int) -> Optional[List[Row]]: sql = character.select(character.c.user == user_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_character(self, user_id: int, character_id: int) -> Optional[Row]: + 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) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_friend_season_ranking(self, user_id: int) -> Optional[Row]: + def get_friend_season_ranking(self, user_id: int) -> Optional[Row]: sql = friend_season_ranking.select(friend_season_ranking.c.user == user_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_friend_season_ranking( + def put_friend_season_ranking( self, aime_id: int, friend_season_ranking_data: Dict ) -> Optional[int]: sql = insert(friend_season_ranking).values( @@ -415,7 +411,7 @@ class Mai2ItemData(BaseData): ) conflict = sql.on_duplicate_key_update(**friend_season_ranking_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning( @@ -425,7 +421,7 @@ class Mai2ItemData(BaseData): return None return result.lastrowid - async def put_favorite( + def put_favorite( self, user_id: int, kind: int, item_id_list: List[int] ) -> Optional[int]: sql = insert(favorite).values( @@ -434,7 +430,7 @@ class Mai2ItemData(BaseData): conflict = sql.on_duplicate_key_update(item_id_list=item_id_list) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning( f"put_favorite: failed to insert item! user_id: {user_id}, kind: {kind}" @@ -442,7 +438,7 @@ class Mai2ItemData(BaseData): return None return result.lastrowid - async def get_favorites(self, user_id: int, kind: int = None) -> Optional[Row]: + 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: @@ -450,12 +446,12 @@ class Mai2ItemData(BaseData): and_(favorite.c.user == user_id, favorite.c.itemKind == kind) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_card( + def put_card( self, user_id: int, card_type_id: int, @@ -479,7 +475,7 @@ class Mai2ItemData(BaseData): charaId=chara_id, mapId=map_id, startDate=start_date, endDate=end_date ) - result = await self.execute(conflict) + 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}" @@ -487,7 +483,7 @@ class Mai2ItemData(BaseData): return None return result.lastrowid - async def get_cards(self, user_id: int, kind: int = None) -> Optional[Row]: + 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: @@ -495,12 +491,12 @@ class Mai2ItemData(BaseData): sql = sql.order_by(card.c.startDate.desc()) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_charge( + def put_charge( self, user_id: int, charge_id: int, @@ -520,7 +516,7 @@ class Mai2ItemData(BaseData): stock=stock, purchaseDate=purchase_date, validDate=valid_date ) - result = await self.execute(conflict) + 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}" @@ -528,15 +524,15 @@ class Mai2ItemData(BaseData): return None return result.lastrowid - async def get_charges(self, user_id: int) -> Optional[Row]: + def get_charges(self, user_id: int) -> Optional[Row]: sql = charge.select(charge.c.user == user_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_user_print_detail( + def put_user_print_detail( self, aime_id: int, serial_id: str, user_print_data: Dict ) -> Optional[int]: sql = insert(print_detail).values( @@ -544,7 +540,7 @@ class Mai2ItemData(BaseData): ) conflict = sql.on_duplicate_key_update(**user_print_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning( diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index 93e7e2d..a8e439c 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -1,15 +1,15 @@ -from datetime import datetime -from typing import Dict, List, Optional - from core.data.schema import BaseData, metadata -from sqlalchemy import Column, Table, UniqueConstraint, and_ -from sqlalchemy.dialects.mysql import insert -from sqlalchemy.engine import Row -from sqlalchemy.schema import ForeignKey -from sqlalchemy.sql import select -from sqlalchemy.types import JSON, BigInteger, Boolean, Integer, String from titles.mai2.const import Mai2Constants +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, BigInteger +from sqlalchemy.schema import ForeignKey +from sqlalchemy.sql import func, select +from sqlalchemy.engine import Row +from sqlalchemy.dialects.mysql import insert +from datetime import datetime + detail = Table( "mai2_profile_detail", metadata, @@ -219,7 +219,7 @@ extend = Table( Column("isPhotoAgree", Boolean), Column("isGotoCodeRead", Boolean), Column("selectResultDetails", Boolean), - Column("selectResultScoreViewType", Integer), # new with fes+ + Column("selectResultScoreViewType", Integer), # new with fes+ Column("sortCategorySetting", Integer), Column("sortMusicSetting", Integer), Column("selectedCardList", JSON), @@ -493,7 +493,7 @@ consec_logins = Table( class Mai2ProfileData(BaseData): - async def put_profile_detail( + def put_profile_detail( self, user_id: int, version: int, detail_data: Dict, is_dx: bool = True ) -> Optional[Row]: detail_data["user"] = user_id @@ -506,7 +506,7 @@ class Mai2ProfileData(BaseData): conflict = sql.on_duplicate_key_update(**detail_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning( f"put_profile: Failed to create profile! user_id {user_id} is_dx {is_dx}" @@ -514,7 +514,7 @@ class Mai2ProfileData(BaseData): return None return result.lastrowid - async def get_profile_detail( + def get_profile_detail( self, user_id: int, version: int, is_dx: bool = True ) -> Optional[Row]: if is_dx: @@ -533,12 +533,12 @@ class Mai2ProfileData(BaseData): .order_by(detail_old.c.version.desc()) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def put_profile_ghost( + def put_profile_ghost( self, user_id: int, version: int, ghost_data: Dict ) -> Optional[int]: ghost_data["user"] = user_id @@ -547,25 +547,25 @@ class Mai2ProfileData(BaseData): sql = insert(ghost).values(**ghost_data) conflict = sql.on_duplicate_key_update(**ghost_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning(f"put_profile_ghost: failed to update! {user_id}") return None return result.lastrowid - async def get_profile_ghost(self, user_id: int, version: int) -> Optional[Row]: + 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)) .order_by(ghost.c.version.desc()) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def put_profile_extend( + def put_profile_extend( self, user_id: int, version: int, extend_data: Dict ) -> Optional[int]: extend_data["user"] = user_id @@ -574,25 +574,25 @@ class Mai2ProfileData(BaseData): sql = insert(extend).values(**extend_data) conflict = sql.on_duplicate_key_update(**extend_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning(f"put_profile_extend: failed to update! {user_id}") return None return result.lastrowid - async def get_profile_extend(self, user_id: int, version: int) -> Optional[Row]: + 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)) .order_by(extend.c.version.desc()) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def put_profile_option( + def put_profile_option( self, user_id: int, version: int, option_data: Dict, is_dx: bool = True ) -> Optional[int]: option_data["user"] = user_id @@ -604,7 +604,7 @@ class Mai2ProfileData(BaseData): sql = insert(option_old).values(**option_data) conflict = sql.on_duplicate_key_update(**option_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning( f"put_profile_option: failed to update! {user_id} is_dx {is_dx}" @@ -612,7 +612,7 @@ class Mai2ProfileData(BaseData): return None return result.lastrowid - async def get_profile_option( + def get_profile_option( self, user_id: int, version: int, is_dx: bool = True ) -> Optional[Row]: if is_dx: @@ -630,12 +630,12 @@ class Mai2ProfileData(BaseData): .order_by(option_old.c.version.desc()) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def put_profile_rating( + def put_profile_rating( self, user_id: int, version: int, rating_data: Dict ) -> Optional[int]: rating_data["user"] = user_id @@ -644,25 +644,25 @@ class Mai2ProfileData(BaseData): sql = insert(rating).values(**rating_data) conflict = sql.on_duplicate_key_update(**rating_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning(f"put_profile_rating: failed to update! {user_id}") return None return result.lastrowid - async def get_profile_rating(self, user_id: int, version: int) -> Optional[Row]: + 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)) .order_by(rating.c.version.desc()) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def put_profile_region(self, user_id: int, region_id: int) -> Optional[int]: + def put_profile_region(self, user_id: int, region_id: int) -> Optional[int]: sql = insert(region).values( user=user_id, regionId=region_id, @@ -671,23 +671,21 @@ class Mai2ProfileData(BaseData): conflict = sql.on_duplicate_key_update(playCount=region.c.playCount + 1) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning(f"put_region: failed to update! {user_id}") return None return result.lastrowid - async def get_regions(self, user_id: int) -> Optional[List[Dict]]: + def get_regions(self, user_id: int) -> Optional[List[Dict]]: sql = select(region).where(region.c.user == user_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_profile_activity( - self, user_id: int, activity_data: Dict - ) -> Optional[int]: + 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") @@ -698,7 +696,7 @@ class Mai2ProfileData(BaseData): conflict = sql.on_duplicate_key_update(**activity_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning( f"put_profile_activity: failed to update! user_id: {user_id}" @@ -706,7 +704,7 @@ class Mai2ProfileData(BaseData): return None return result.lastrowid - async def get_profile_activity( + def get_profile_activity( self, user_id: int, kind: int = None ) -> Optional[List[Row]]: sql = activity.select( @@ -716,12 +714,12 @@ class Mai2ProfileData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_web_option( + def put_web_option( self, user_id: int, version: int, web_opts: Dict ) -> Optional[int]: web_opts["user"] = user_id @@ -730,29 +728,29 @@ class Mai2ProfileData(BaseData): conflict = sql.on_duplicate_key_update(**web_opts) - result = await self.execute(conflict) + 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 - async def get_web_option(self, user_id: int, version: int) -> Optional[Row]: + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def put_grade_status(self, user_id: int, grade_stat: Dict) -> Optional[int]: + 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 = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning( f"put_grade_status: failed to update! user_id: {user_id}" @@ -760,40 +758,40 @@ class Mai2ProfileData(BaseData): return None return result.lastrowid - async def get_grade_status(self, user_id: int) -> Optional[Row]: + def get_grade_status(self, user_id: int) -> Optional[Row]: sql = grade_status.select(grade_status.c.user == user_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def put_boss_list(self, user_id: int, boss_stat: Dict) -> Optional[int]: + 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 = await self.execute(conflict) + 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 - async def get_boss_list(self, user_id: int) -> Optional[Row]: + def get_boss_list(self, user_id: int) -> Optional[Row]: sql = boss.select(boss.c.user == user_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def put_recent_rating(self, user_id: int, rr: Dict) -> Optional[int]: + 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 = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning( f"put_recent_rating: failed to update! user_id: {user_id}" @@ -801,26 +799,26 @@ class Mai2ProfileData(BaseData): return None return result.lastrowid - async def get_recent_rating(self, user_id: int) -> Optional[Row]: + def get_recent_rating(self, user_id: int) -> Optional[Row]: sql = recent_rating.select(recent_rating.c.user == user_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def add_consec_login(self, user_id: int, version: int) -> None: + 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 = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.error( f"Failed to update consecutive login count for user {user_id} version {version}" ) - async def get_consec_login(self, user_id: int, version: int) -> Optional[Row]: + def get_consec_login(self, user_id: int, version: int) -> Optional[Row]: sql = select(consec_logins).where( and_( consec_logins.c.user == user_id, @@ -828,12 +826,12 @@ class Mai2ProfileData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def reset_consec_login(self, user_id: int, version: int) -> Optional[Row]: + def reset_consec_login(self, user_id: int, version: int) -> Optional[Row]: sql = consec_logins.update( and_( consec_logins.c.user == user_id, @@ -841,7 +839,7 @@ class Mai2ProfileData(BaseData): ) ).values(logins=1) - result = await self.execute(sql) + 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 bac7e55..234c53c 100644 --- a/titles/mai2/schema/score.py +++ b/titles/mai2/schema/score.py @@ -1,12 +1,13 @@ from typing import Dict, List, Optional - -from core.data import cached -from core.data.schema import BaseData, metadata -from sqlalchemy import Column, Table, UniqueConstraint, and_ -from sqlalchemy.dialects.mysql import insert -from sqlalchemy.engine import Row +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, BigInteger from sqlalchemy.schema import ForeignKey -from sqlalchemy.types import BigInteger, Boolean, Integer, String, JSON +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.data import cached best_score = Table( "mai2_score_best", @@ -145,30 +146,11 @@ playlog = Table( Column("extNum1", Integer), Column("extNum2", Integer), Column("extNum4", Integer, server_default="0"), - Column("extBool1", Boolean), # new with bud + Column("extBool1", Boolean), # new with bud Column("trialPlayAchievement", Integer), mysql_charset="utf8mb4", ) -playlog_2p = Table( # new with buddies, the in-game name is 2pPlaylog - "mai2_playlog_2p", - metadata, - Column("id", Integer, primary_key=True, nullable=False), - Column( - "user", - ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), - nullable=False, - ), - Column("userId1", BigInteger), - Column("userId2", BigInteger), - Column("userName1", String(255)), - Column("userName2", String(255)), - Column("regionId", Integer), - Column("placeId", Integer), - Column("user2pPlaylogDetailList", JSON), - mysql_charset="utf8mb4", -) - course = Table( "mai2_score_course", metadata, @@ -291,11 +273,8 @@ best_score_old = Table( mysql_charset="utf8mb4", ) - class Mai2ScoreData(BaseData): - async def put_best_score( - self, user_id: int, score_data: Dict, is_dx: bool = True - ) -> Optional[int]: + 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: @@ -304,7 +283,7 @@ class Mai2ScoreData(BaseData): sql = insert(best_score_old).values(**score_data) conflict = sql.on_duplicate_key_update(**score_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.error( f"put_best_score: Failed to insert best score! user_id {user_id} is_dx {is_dx}" @@ -313,9 +292,7 @@ class Mai2ScoreData(BaseData): return result.lastrowid @cached(2) - async def get_best_scores( - self, user_id: int, song_id: int = None, is_dx: bool = True - ) -> Optional[List[Row]]: + 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_( @@ -327,20 +304,16 @@ class Mai2ScoreData(BaseData): sql = best_score_old.select( and_( best_score_old.c.user == user_id, - ( - (best_score_old.c.song_id == song_id) - if song_id is not None - else True - ), + (best_score_old.c.song_id == song_id) if song_id is not None else True, ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_best_score( + def get_best_score( self, user_id: int, song_id: int, chart_id: int ) -> Optional[Row]: sql = best_score.select( @@ -351,14 +324,12 @@ class Mai2ScoreData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def put_playlog( - self, user_id: int, playlog_data: Dict, is_dx: bool = True - ) -> Optional[int]: + def put_playlog(self, user_id: int, playlog_data: Dict, is_dx: bool = True) -> Optional[int]: playlog_data["user"] = user_id if is_dx: @@ -368,44 +339,28 @@ class Mai2ScoreData(BaseData): conflict = sql.on_duplicate_key_update(**playlog_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: - self.logger.error( - f"put_playlog: Failed to insert! user_id {user_id} is_dx {is_dx}" - ) + self.logger.error(f"put_playlog: Failed to insert! user_id {user_id} is_dx {is_dx}") return None return result.lastrowid - async def put_2p_playlog( - self, user_id: int, playlog_2p_data: Dict - ) -> Optional[int]: - playlog_2p_data["user"] = user_id - sql = insert(playlog_2p).values(**playlog_2p_data) - - conflict = sql.on_duplicate_key_update(**playlog_2p_data) - - result = await self.execute(conflict) - if result is None: - self.logger.error(f"put_2p_playlog: Failed to insert! user_id {user_id}") - return None - return result.lastrowid - - async def put_course(self, user_id: int, course_data: Dict) -> Optional[int]: + 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) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.error(f"put_course: Failed to insert! user_id {user_id}") return None return result.lastrowid - async def get_courses(self, user_id: int) -> Optional[List[Row]]: + def get_courses(self, user_id: int) -> Optional[List[Row]]: sql = course.select(course.c.user == user_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() diff --git a/titles/mai2/schema/static.py b/titles/mai2/schema/static.py index 28aa6c1..0f5bfad 100644 --- a/titles/mai2/schema/static.py +++ b/titles/mai2/schema/static.py @@ -1,11 +1,12 @@ -from typing import List, Optional - from core.data.schema.base import BaseData, metadata -from sqlalchemy import Column, Table, UniqueConstraint, and_ -from sqlalchemy.dialects.mysql import insert -from sqlalchemy.engine import Row + +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, Float +from sqlalchemy.schema import ForeignKey from sqlalchemy.sql import func, select -from sqlalchemy.types import TIMESTAMP, Boolean, Float, Integer, String +from sqlalchemy.engine import Row +from sqlalchemy.dialects.mysql import insert event = Table( "mai2_static_event", @@ -71,7 +72,7 @@ cards = Table( class Mai2StaticData(BaseData): - async def put_game_event( + def put_game_event( self, version: int, type: int, event_id: int, name: str ) -> Optional[int]: sql = insert(event).values( @@ -83,46 +84,46 @@ class Mai2StaticData(BaseData): conflict = sql.on_duplicate_key_update(eventId=event_id) - result = await self.execute(conflict) + 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}" ) return result.lastrowid - async def get_game_events(self, version: int) -> Optional[List[Row]]: + def get_game_events(self, version: int) -> Optional[List[Row]]: sql = event.select(event.c.version == version) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_enabled_events(self, version: int) -> Optional[List[Row]]: + def get_enabled_events(self, version: int) -> Optional[List[Row]]: sql = select(event).where( and_(event.c.version == version, event.c.enabled == True) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def toggle_game_event( + 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 = await self.execute(sql) + 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() - async def put_game_music( + def put_game_music( self, version: int, song_id: int, @@ -158,13 +159,13 @@ class Mai2StaticData(BaseData): noteDesigner=note_designer, ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning(f"Failed to insert song {song_id} chart {chart_id}") return None return result.lastrowid - async def put_game_ticket( + def put_game_ticket( self, version: int, ticket_id: int, @@ -184,15 +185,13 @@ class Mai2StaticData(BaseData): conflict = sql.on_duplicate_key_update(price=ticket_price) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: - self.logger.warning( - f"Failed to insert charge {ticket_id} type {ticket_type}" - ) + self.logger.warning(f"Failed to insert charge {ticket_id} type {ticket_type}") return None return result.lastrowid - async def get_enabled_tickets( + def get_enabled_tickets( self, version: int, kind: int = None ) -> Optional[List[Row]]: if kind is not None: @@ -208,12 +207,12 @@ class Mai2StaticData(BaseData): and_(ticket.c.version == version, ticket.c.enabled == True) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_music_chart( + def get_music_chart( self, version: int, song_id: int, chart_id: int ) -> Optional[List[Row]]: sql = select(music).where( @@ -224,30 +223,28 @@ class Mai2StaticData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def put_card( - self, version: int, card_id: int, card_name: str, **card_data - ) -> int: + 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 = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning(f"Failed to insert card {card_id}") return None return result.lastrowid - async def get_enabled_cards(self, version: int) -> Optional[List[Row]]: + def get_enabled_cards(self, version: int) -> Optional[List[Row]]: sql = cards.select(and_(cards.c.version == version, cards.c.enabled == True)) - result = await self.execute(sql) + 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 3784f58..026e3ae 100644 --- a/titles/mai2/splash.py +++ b/titles/mai2/splash.py @@ -1,7 +1,12 @@ +from typing import Any, List, Dict +from datetime import datetime, timedelta +import pytz +import json + from core.config import CoreConfig +from titles.mai2.dxplus import Mai2DXPlus from titles.mai2.config import Mai2Config from titles.mai2.const import Mai2Constants -from titles.mai2.dxplus import Mai2DXPlus class Mai2Splash(Mai2DXPlus): diff --git a/titles/mai2/splashplus.py b/titles/mai2/splashplus.py index 314d21c..438941f 100644 --- a/titles/mai2/splashplus.py +++ b/titles/mai2/splashplus.py @@ -1,7 +1,12 @@ +from typing import Any, List, Dict +from datetime import datetime, timedelta +import pytz +import json + from core.config import CoreConfig +from titles.mai2.splash import Mai2Splash from titles.mai2.config import Mai2Config from titles.mai2.const import Mai2Constants -from titles.mai2.splash import Mai2Splash class Mai2SplashPlus(Mai2Splash): diff --git a/titles/mai2/universe.py b/titles/mai2/universe.py index 08080b0..d25a295 100644 --- a/titles/mai2/universe.py +++ b/titles/mai2/universe.py @@ -1,11 +1,13 @@ -from datetime import datetime, timedelta +from typing import Any, List, Dict from random import randint -from typing import Dict, List +from datetime import datetime, timedelta +import pytz +import json from core.config import CoreConfig -from titles.mai2.config import Mai2Config -from titles.mai2.const import Mai2Constants from titles.mai2.splashplus import Mai2SplashPlus +from titles.mai2.const import Mai2Constants +from titles.mai2.config import Mai2Config class Mai2Universe(Mai2SplashPlus): @@ -13,8 +15,8 @@ class Mai2Universe(Mai2SplashPlus): super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE - async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: - p = await self.data.profile.get_profile_detail(data["userId"], self.version) + 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 {} @@ -28,11 +30,11 @@ class Mai2Universe(Mai2SplashPlus): "isExistSellingCard": True, } - async def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: + def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: # user already exists, because the preview checks that already - p = await self.data.profile.get_profile_detail(data["userId"], self.version) + p = self.data.profile.get_profile_detail(data["userId"], self.version) - cards = await self.data.card.get_user_cards(data["userId"]) + cards = self.data.card.get_user_cards(data["userId"]) if cards is None or len(cards) == 0: # This should never happen self.logger.error( @@ -50,14 +52,14 @@ class Mai2Universe(Mai2SplashPlus): return {"userId": data["userId"], "userData": user_data} - async def handle_cm_login_api_request(self, data: Dict) -> Dict: + def handle_cm_login_api_request(self, data: Dict) -> Dict: return {"returnCode": 1} - async def handle_cm_logout_api_request(self, data: Dict) -> Dict: + def handle_cm_logout_api_request(self, data: Dict) -> Dict: return {"returnCode": 1} - async def handle_cm_get_selling_card_api_request(self, data: Dict) -> Dict: - selling_cards = await self.data.static.get_enabled_cards(self.version) + 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": []} @@ -86,8 +88,8 @@ class Mai2Universe(Mai2SplashPlus): return {"length": len(selling_card_list), "sellingCardList": selling_card_list} - async def handle_cm_get_user_card_api_request(self, data: Dict) -> Dict: - user_cards = await self.data.item.get_cards(data["userId"]) + 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": []} @@ -122,11 +124,11 @@ class Mai2Universe(Mai2SplashPlus): "userCardList": card_list[start_idx:end_idx], } - async def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: - await super().handle_get_user_item_api_request(data) + def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: + super().handle_get_user_item_api_request(data) - async def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: - characters = await self.data.item.get_characters(data["userId"]) + 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: @@ -151,10 +153,10 @@ class Mai2Universe(Mai2SplashPlus): "userCharacterList": chara_list, } - async def handle_cm_get_user_card_print_error_api_request(self, data: Dict) -> Dict: + def handle_cm_get_user_card_print_error_api_request(self, data: Dict) -> Dict: return {"length": 0, "userPrintDetailList": []} - async def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: + def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: user_id = data["userId"] upsert = data["userPrintDetail"] @@ -166,7 +168,7 @@ class Mai2Universe(Mai2SplashPlus): end_date = datetime.utcnow() + timedelta(days=15) user_card = upsert["userCard"] - await self.data.item.put_card( + self.data.item.put_card( user_id, user_card["cardId"], user_card["cardTypeId"], @@ -178,7 +180,7 @@ class Mai2Universe(Mai2SplashPlus): ) # get the profile extend to save the new bought card - extend = await self.data.profile.get_profile_extend(user_id, self.version) + extend = self.data.profile.get_profile_extend(user_id, self.version) if extend: extend = extend._asdict() # parse the selectedCardList @@ -190,14 +192,14 @@ class Mai2Universe(Mai2SplashPlus): selected_cards.insert(0, user_card["cardTypeId"]) extend["selectedCardList"] = selected_cards - await self.data.profile.put_profile_extend(user_id, self.version, extend) + self.data.profile.put_profile_extend(user_id, self.version, extend) # properly format userPrintDetail for the database upsert.pop("userCard") upsert.pop("serialId") upsert["printDate"] = datetime.strptime(upsert["printDate"], "%Y-%m-%d") - await self.data.item.put_user_print_detail(user_id, serial_id, upsert) + self.data.item.put_user_print_detail(user_id, serial_id, upsert) return { "returnCode": 1, @@ -207,12 +209,12 @@ class Mai2Universe(Mai2SplashPlus): "endDate": datetime.strftime(end_date, Mai2Constants.DATE_TIME_FORMAT), } - async def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: + def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: return { "returnCode": 1, "orderId": 0, "serialId": data["userPrintlog"]["serialId"], } - async def handle_cm_upsert_buy_card_api_request(self, data: Dict) -> Dict: + 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 99ac3f9..e45c719 100644 --- a/titles/mai2/universeplus.py +++ b/titles/mai2/universeplus.py @@ -1,9 +1,9 @@ from typing import Dict from core.config import CoreConfig -from titles.mai2.config import Mai2Config -from titles.mai2.const import Mai2Constants from titles.mai2.universe import Mai2Universe +from titles.mai2.const import Mai2Constants +from titles.mai2.config import Mai2Config class Mai2UniversePlus(Mai2Universe): @@ -11,8 +11,8 @@ class Mai2UniversePlus(Mai2Universe): super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS - async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: - user_data = await super().handle_cm_get_user_preview_api_request(data) + 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" diff --git a/titles/ongeki/__init__.py b/titles/ongeki/__init__.py index 0eb1b8e..f12343d 100644 --- a/titles/ongeki/__init__.py +++ b/titles/ongeki/__init__.py @@ -1,11 +1,12 @@ +from titles.ongeki.index import OngekiServlet from titles.ongeki.const import OngekiConstants from titles.ongeki.database import OngekiData -from titles.ongeki.frontend import OngekiFrontend -from titles.ongeki.index import OngekiServlet from titles.ongeki.read import OngekiReader +from titles.ongeki.frontend import OngekiFrontend index = OngekiServlet database = OngekiData reader = OngekiReader frontend = OngekiFrontend game_codes = [OngekiConstants.GAME_CODE] +current_schema_version = 6 diff --git a/titles/ongeki/base.py b/titles/ongeki/base.py index 08b76de..596fb22 100644 --- a/titles/ongeki/base.py +++ b/titles/ongeki/base.py @@ -1,15 +1,16 @@ +from datetime import date, datetime, timedelta +from typing import Any, Dict, List import json import logging -from datetime import datetime, timedelta from enum import Enum -from typing import Any, Dict, List import pytz from core.config import CoreConfig from core.data.cache import cached -from titles.ongeki.config import OngekiConfig from titles.ongeki.const import OngekiConstants +from titles.ongeki.config import OngekiConfig from titles.ongeki.database import OngekiData +from titles.ongeki.config import OngekiConfig class OngekiBattleGrade(Enum): @@ -102,12 +103,9 @@ class OngekiBase: self.game = OngekiConstants.GAME_CODE self.version = OngekiConstants.VER_ONGEKI - async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: # 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 == "" - ): + 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 ) @@ -116,29 +114,15 @@ class OngekiBase: ) else: # get current datetime in JST - current_jst = datetime.now(pytz.timezone("Asia/Tokyo")).date() + 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" - ) + 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"), - ) + 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) @@ -164,7 +148,7 @@ class OngekiBase: "isAou": "true", } - async def handle_get_game_idlist_api_request(self, data: Dict) -> Dict: + def handle_get_game_idlist_api_request(self, data: Dict) -> Dict: """ Gets lists of song IDs, either disabled songs or recomended songs depending on type? """ @@ -172,9 +156,9 @@ class OngekiBase: # id - int return {"type": data["type"], "length": 0, "gameIdlistList": []} - async def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: - game_ranking_list = await self.data.static.get_ranking_list(self.version) - + def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: + game_ranking_list = self.data.static.get_ranking_list(self.version) + ranking_list = [] for music in game_ranking_list: tmp = music._asdict() @@ -187,21 +171,21 @@ class OngekiBase: "gameRankingList": ranking_list, } - async def handle_get_game_point_api_request(self, data: Dict) -> Dict: - get_game_point = await self.data.static.get_static_game_point() + def handle_get_game_point_api_request(self, data: Dict) -> Dict: + 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") - await self.data.static.put_static_game_point_defaults() - get_game_point = await self.data.static.get_static_game_point() + 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) @@ -209,18 +193,18 @@ class OngekiBase: "length": len(game_point), "gamePointList": game_point, } - - async def handle_game_login_api_request(self, data: Dict) -> Dict: + + def handle_game_login_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "gameLogin"} - async def handle_game_logout_api_request(self, data: Dict) -> Dict: + def handle_game_logout_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "gameLogout"} - async def handle_extend_lock_time_api_request(self, data: Dict) -> Dict: + def handle_extend_lock_time_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "ExtendLockTimeApi"} - async def handle_get_game_reward_api_request(self, data: Dict) -> Dict: - get_game_rewards = await self.data.static.get_reward_list(self.version) + def handle_get_game_reward_api_request(self, data: Dict) -> Dict: + get_game_rewards = self.data.static.get_reward_list(self.version) reward_list = [] for reward in get_game_rewards: @@ -237,8 +221,8 @@ class OngekiBase: "gameRewardList": reward_list, } - async def handle_get_game_present_api_request(self, data: Dict) -> Dict: - get_present = await self.data.static.get_present_list(self.version) + def handle_get_game_present_api_request(self, data: Dict) -> Dict: + get_present = self.data.static.get_present_list(self.version) present_list = [] for present in get_present: @@ -254,14 +238,14 @@ class OngekiBase: "gamePresentList": present_list, } - async def handle_get_game_message_api_request(self, data: Dict) -> Dict: + def handle_get_game_message_api_request(self, data: Dict) -> Dict: return {"length": 0, "gameMessageList": []} - async def handle_get_game_sale_api_request(self, data: Dict) -> Dict: + def handle_get_game_sale_api_request(self, data: Dict) -> Dict: return {"length": 0, "gameSaleList": []} - async def handle_get_game_tech_music_api_request(self, data: Dict) -> Dict: - music_list = await self.data.static.get_tech_music(self.version) + def handle_get_game_tech_music_api_request(self, data: Dict) -> Dict: + music_list = self.data.static.get_tech_music(self.version) prep_music_list = [] for music in music_list: @@ -278,43 +262,41 @@ class OngekiBase: "gameTechMusicList": prep_music_list, } - async def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: + 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 = await self.data.arcade.get_machine(client_id) + cab = self.data.arcade.get_machine(client_id) if cab is not None: - await self.data.static.put_client_setting_data( - cab["id"], client_setting_data - ) + self.data.static.put_client_setting_data(cab['id'], client_setting_data) return {"returnCode": 1, "apiName": "UpsertClientSettingApi"} - async def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict: + 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"] - await self.data.static.put_client_testmode_data(region_id, client_testmode_data) + self.data.static.put_client_testmode_data(region_id, client_testmode_data) return {"returnCode": 1, "apiName": "UpsertClientTestmodeApi"} - async def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: + def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "upsertClientBookkeeping"} - async def handle_upsert_client_develop_api_request(self, data: Dict) -> Dict: + def handle_upsert_client_develop_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "upsertClientDevelop"} - async def handle_upsert_client_error_api_request(self, data: Dict) -> Dict: + def handle_upsert_client_error_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "upsertClientError"} - async def handle_upsert_user_gplog_api_request(self, data: Dict) -> Dict: + def handle_upsert_user_gplog_api_request(self, data: Dict) -> Dict: user = data["userId"] if user >= 200000000000000: # Account for guest play user = None - await self.data.log.put_gp_log( + self.data.log.put_gp_log( user, data["usedCredit"], data["placeName"], @@ -327,18 +309,18 @@ class OngekiBase: return {"returnCode": 1, "apiName": "UpsertUserGplogApi"} - async def handle_extend_lock_time_api_request(self, data: Dict) -> Dict: + def handle_extend_lock_time_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "ExtendLockTimeApi"} - async def handle_get_game_event_api_request(self, data: Dict) -> Dict: - evts = await self.data.static.get_enabled_events(self.version) + 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": [], - } + "type": data["type"], + "length": 0, + "gameEventList": [], + } evt_list = [] for event in evts: @@ -348,23 +330,19 @@ class OngekiBase: "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" - ), + "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, } - async def handle_get_game_id_list_api_request(self, data: Dict) -> Dict: + def handle_get_game_id_list_api_request(self, data: Dict) -> Dict: game_idlist: List[str, Any] = [] # 1 to 230 & 8000 to 8050 if data["type"] == 1: @@ -384,13 +362,11 @@ class OngekiBase: "gameIdlistList": game_idlist, } - async def handle_get_user_region_api_request(self, data: Dict) -> Dict: + def handle_get_user_region_api_request(self, data: Dict) -> Dict: return {"userId": data["userId"], "length": 0, "userRegionList": []} - async def handle_get_user_preview_api_request(self, data: Dict) -> Dict: - profile = await self.data.profile.get_profile_preview( - data["userId"], self.version - ) + 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: return { @@ -441,12 +417,12 @@ class OngekiBase: "isWarningConfirmed": True, } - async def handle_get_user_tech_count_api_request(self, data: Dict) -> Dict: + def handle_get_user_tech_count_api_request(self, data: Dict) -> Dict: """ Gets the number of AB and ABPs a player has per-difficulty (7, 7+, 8, etc) The game sends this in upsert so we don't have to calculate it all out thankfully """ - utcl = await self.data.score.get_tech_count(data["userId"]) + utcl = self.data.score.get_tech_count(data["userId"]) userTechCountList = [] for tc in utcl: @@ -460,10 +436,8 @@ class OngekiBase: "userTechCountList": userTechCountList, } - async def handle_get_user_tech_event_api_request(self, data: Dict) -> Dict: - user_tech_event_list = await self.data.item.get_tech_event( - self.version, data["userId"] - ) + def handle_get_user_tech_event_api_request(self, data: Dict) -> Dict: + user_tech_event_list = self.data.item.get_tech_event(self.version, data["userId"]) if user_tech_event_list is None: return {} @@ -481,16 +455,14 @@ class OngekiBase: "userTechEventList": tech_evt, } - async def handle_get_user_tech_event_ranking_api_request(self, data: Dict) -> Dict: - user_tech_event_ranks = await self.data.item.get_tech_event_ranking( - self.version, data["userId"] - ) - if user_tech_event_ranks is None: + def handle_get_user_tech_event_ranking_api_request(self, data: Dict) -> Dict: + 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": [], - } + "userId": data["userId"], + "length": 0, + "userTechEventRankingList": [], + } # collect the whole table and clear other players, to preserve proper ranking evt_ranking = [] @@ -509,8 +481,8 @@ class OngekiBase: "userTechEventRankingList": evt_ranking, } - async def handle_get_user_kop_api_request(self, data: Dict) -> Dict: - kop_list = await self.data.profile.get_kop(data["userId"]) + 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 {} @@ -524,8 +496,8 @@ class OngekiBase: "userKopList": kop_list, } - async def handle_get_user_music_api_request(self, data: Dict) -> Dict: - song_list = await self.util_generate_music_list(data["userId"]) + def handle_get_user_music_api_request(self, data: Dict) -> Dict: + song_list = self.util_generate_music_list(data["userId"]) max_ct = data["maxCount"] next_idx = data["nextIndex"] start_idx = next_idx @@ -544,9 +516,9 @@ class OngekiBase: "userMusicList": song_list[start_idx:end_idx], } - async def handle_get_user_item_api_request(self, data: Dict) -> Dict: + def handle_get_user_item_api_request(self, data: Dict) -> Dict: kind = data["nextIndex"] / 10000000000 - p = await self.data.item.get_items(data["userId"], kind) + p = self.data.item.get_items(data["userId"], kind) if p is None: return { @@ -580,8 +552,8 @@ class OngekiBase: "userItemList": items, } - async def handle_get_user_option_api_request(self, data: Dict) -> Dict: - o = await self.data.profile.get_profile_options(data["userId"]) + 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 {} @@ -594,12 +566,12 @@ class OngekiBase: return {"userId": data["userId"], "userOption": user_opts} - async def handle_get_user_data_api_request(self, data: Dict) -> Dict: - p = await self.data.profile.get_profile_data(data["userId"], self.version) + 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 {} - cards = await self.data.card.get_user_cards(data["userId"]) + cards = self.data.card.get_user_cards(data["userId"]) if cards is None or len(cards) == 0: # This should never happen self.logger.error( @@ -622,14 +594,12 @@ class OngekiBase: return {"userId": data["userId"], "userData": user_data} - async def handle_get_user_event_ranking_api_request(self, data: Dict) -> Dict: - user_event_ranking_list = await self.data.item.get_ranking_event_ranks( - self.version, data["userId"] - ) + def handle_get_user_event_ranking_api_request(self, data: Dict) -> Dict: + user_event_ranking_list = self.data.item.get_ranking_event_ranks(self.version, data["userId"]) if user_event_ranking_list is None: return {} - # We collect the whole ranking table, and clear out any not needed data, this way we preserve the proper ranking + # 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: @@ -647,8 +617,8 @@ class OngekiBase: "userEventRankingList": prep_event_ranking, } - async def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: - user_login_bonus_list = await self.data.item.get_login_bonuses(data["userId"]) + 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 {} @@ -665,8 +635,8 @@ class OngekiBase: "userLoginBonusList": login_bonuses, } - async def handle_get_user_bp_base_request(self, data: Dict) -> Dict: - p = await self.data.profile.get_profile( + 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: @@ -678,10 +648,8 @@ class OngekiBase: "userBpBaseList": profile["userBpBaseList"], } - async def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict: - recent_rating = await self.data.profile.get_profile_recent_rating( - data["userId"] - ) + 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 { "userId": data["userId"], @@ -697,10 +665,8 @@ class OngekiBase: "userRecentRatingList": userRecentRatingList, } - async def handle_get_user_activity_api_request(self, data: Dict) -> Dict: - activity = await self.data.profile.get_profile_activity( - data["userId"], data["kind"] - ) + 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 {} @@ -726,8 +692,8 @@ class OngekiBase: "userActivityList": user_activity, } - async def handle_get_user_story_api_request(self, data: Dict) -> Dict: - user_stories = await self.data.item.get_stories(data["userId"]) + 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 {} @@ -744,8 +710,8 @@ class OngekiBase: "userStoryList": story_list, } - async def handle_get_user_chapter_api_request(self, data: Dict) -> Dict: - user_chapters = await self.data.item.get_chapters(data["userId"]) + 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 {} @@ -762,17 +728,15 @@ class OngekiBase: "userChapterList": chapter_list, } - async def handle_get_user_training_room_by_key_api_request( - self, data: Dict - ) -> Dict: + def handle_get_user_training_room_by_key_api_request(self, data: Dict) -> Dict: return { "userId": data["userId"], "length": 0, "userTrainingRoomList": [], } - async def handle_get_user_character_api_request(self, data: Dict) -> Dict: - user_characters = await self.data.item.get_characters(data["userId"]) + 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 {} @@ -789,8 +753,8 @@ class OngekiBase: "userCharacterList": character_list, } - async def handle_get_user_card_api_request(self, data: Dict) -> Dict: - user_cards = await self.data.item.get_cards(data["userId"]) + 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 {} @@ -807,9 +771,9 @@ class OngekiBase: "userCardList": card_list, } - async def handle_get_user_deck_by_key_api_request(self, data: Dict) -> Dict: + 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 = await self.data.item.get_decks(data["userId"]) + decks = self.data.item.get_decks(data["userId"]) if decks is None: return {} @@ -826,8 +790,8 @@ class OngekiBase: "userDeckList": deck_list, } - async def handle_get_user_trade_item_api_request(self, data: Dict) -> Dict: - user_trade_items = await self.data.item.get_trade_items(data["userId"]) + 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 {} @@ -844,8 +808,8 @@ class OngekiBase: "userTradeItemList": trade_item_list, } - async def handle_get_user_scenario_api_request(self, data: Dict) -> Dict: - user_scenerio = await self.data.item.get_scenerios(data["userId"]) + 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 {} @@ -862,8 +826,8 @@ class OngekiBase: "userScenarioList": scenerio_list, } - async def handle_get_user_ratinglog_api_request(self, data: Dict) -> Dict: - rating_log = await self.data.profile.get_profile_rating_log(data["userId"]) + 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 {} @@ -880,10 +844,8 @@ class OngekiBase: "userRatinglogList": userRatinglogList, } - async def handle_get_user_mission_point_api_request(self, data: Dict) -> Dict: - user_mission_point_list = await self.data.item.get_mission_points( - self.version, data["userId"] - ) + def handle_get_user_mission_point_api_request(self, data: Dict) -> Dict: + user_mission_point_list = self.data.item.get_mission_points(self.version, data["userId"]) if user_mission_point_list is None: return {} @@ -895,14 +857,15 @@ class OngekiBase: tmp.pop("version") mission_point_list.append(tmp) + return { "userId": data["userId"], "length": len(mission_point_list), "userMissionPointList": mission_point_list, } - async def handle_get_user_event_point_api_request(self, data: Dict) -> Dict: - user_event_point_list = await self.data.item.get_event_points(data["userId"]) + 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 {} @@ -923,8 +886,8 @@ class OngekiBase: "userEventPointList": event_point_list, } - async def handle_get_user_music_item_api_request(self, data: Dict) -> Dict: - user_music_item_list = await self.data.item.get_music_items(data["userId"]) + 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 {} @@ -941,8 +904,8 @@ class OngekiBase: "userMusicItemList": music_item_list, } - async def handle_get_user_event_music_api_request(self, data: Dict) -> Dict: - user_evt_music_list = await self.data.item.get_event_music(data["userId"]) + 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 {} @@ -959,8 +922,8 @@ class OngekiBase: "userEventMusicList": evt_music_list, } - async def handle_get_user_boss_api_request(self, data: Dict) -> Dict: - p = await self.data.item.get_bosses(data["userId"]) + def handle_get_user_boss_api_request(self, data: Dict) -> Dict: + p = self.data.item.get_bosses(data["userId"]) if p is None: return {} @@ -977,29 +940,27 @@ class OngekiBase: "userBossList": boss_list, } - async def handle_upsert_user_all_api_request(self, data: Dict) -> Dict: + def handle_upsert_user_all_api_request(self, data: Dict) -> Dict: upsert = data["upsertUserAll"] user_id = data["userId"] # 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: - await self.data.profile.put_profile_data( + self.data.profile.put_profile_data( user_id, self.version, upsert["userData"][0] ) if "userOption" in upsert and len(upsert["userOption"]) > 0: - await self.data.profile.put_profile_options( - user_id, upsert["userOption"][0] - ) + self.data.profile.put_profile_options(user_id, upsert["userOption"][0]) if "userPlaylogList" in upsert: for playlog in upsert["userPlaylogList"]: - await self.data.score.put_playlog(user_id, playlog) + self.data.score.put_playlog(user_id, playlog) if "userActivityList" in upsert: for act in upsert["userActivityList"]: - await self.data.profile.put_profile_activity( + self.data.profile.put_profile_activity( user_id, act["kind"], act["id"], @@ -1011,131 +972,111 @@ class OngekiBase: ) if "userRecentRatingList" in upsert: - await self.data.profile.put_profile_recent_rating( + self.data.profile.put_profile_recent_rating( user_id, upsert["userRecentRatingList"] ) if "userBpBaseList" in upsert: - await self.data.profile.put_profile_bp_list( - user_id, upsert["userBpBaseList"] - ) + self.data.profile.put_profile_bp_list(user_id, upsert["userBpBaseList"]) if "userMusicDetailList" in upsert: for x in upsert["userMusicDetailList"]: - await self.data.score.put_best_score(user_id, x) + self.data.score.put_best_score(user_id, x) if "userCharacterList" in upsert: for x in upsert["userCharacterList"]: - await self.data.item.put_character(user_id, x) + self.data.item.put_character(user_id, x) if "userCardList" in upsert: for x in upsert["userCardList"]: - await self.data.item.put_card(user_id, x) + self.data.item.put_card(user_id, x) if "userDeckList" in upsert: for x in upsert["userDeckList"]: - await self.data.item.put_deck(user_id, x) + self.data.item.put_deck(user_id, x) if "userTrainingRoomList" in upsert: for x in upsert["userTrainingRoomList"]: - await self.data.profile.put_training_room(user_id, x) + self.data.profile.put_training_room(user_id, x) if "userStoryList" in upsert: for x in upsert["userStoryList"]: - await self.data.item.put_story(user_id, x) + self.data.item.put_story(user_id, x) if "userChapterList" in upsert: for x in upsert["userChapterList"]: - await self.data.item.put_chapter(user_id, x) + self.data.item.put_chapter(user_id, x) if "userMemoryChapterList" in upsert: for x in upsert["userMemoryChapterList"]: - await self.data.item.put_memorychapter(user_id, x) + self.data.item.put_memorychapter(user_id, x) if "userItemList" in upsert: for x in upsert["userItemList"]: - await self.data.item.put_item(user_id, x) + self.data.item.put_item(user_id, x) if "userMusicItemList" in upsert: for x in upsert["userMusicItemList"]: - await self.data.item.put_music_item(user_id, x) + self.data.item.put_music_item(user_id, x) if "userLoginBonusList" in upsert: for x in upsert["userLoginBonusList"]: - await self.data.item.put_login_bonus(user_id, x) + self.data.item.put_login_bonus(user_id, x) if "userEventPointList" in upsert: for x in upsert["userEventPointList"]: - await self.data.item.put_event_point(user_id, self.version, x) + self.data.item.put_event_point(user_id, self.version, x) if "userMissionPointList" in upsert: for x in upsert["userMissionPointList"]: - await self.data.item.put_mission_point(user_id, self.version, x) + self.data.item.put_mission_point(user_id, self.version, x) if "userRatinglogList" in upsert: for x in upsert["userRatinglogList"]: - await self.data.profile.put_profile_rating_log( + self.data.profile.put_profile_rating_log( user_id, x["dataVersion"], x["highestRating"] ) if "userBossList" in upsert: for x in upsert["userBossList"]: - await self.data.item.put_boss(user_id, x) + self.data.item.put_boss(user_id, x) if "userTechCountList" in upsert: for x in upsert["userTechCountList"]: - await self.data.score.put_tech_count(user_id, x) + self.data.score.put_tech_count(user_id, x) if "userScenerioList" in upsert: for x in upsert["userScenerioList"]: - await self.data.item.put_scenerio(user_id, x) + self.data.item.put_scenerio(user_id, x) if "userTradeItemList" in upsert: for x in upsert["userTradeItemList"]: - await self.data.item.put_trade_item(user_id, x) + self.data.item.put_trade_item(user_id, x) if "userEventMusicList" in upsert: for x in upsert["userEventMusicList"]: - await self.data.item.put_event_music(user_id, x) + self.data.item.put_event_music(user_id, x) if "userTechEventList" in upsert: for x in upsert["userTechEventList"]: - await self.data.item.put_tech_event(user_id, self.version, 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 - await self.data.item.put_tech_event_ranking(user_id, self.version, x) + self.data.item.put_tech_event_ranking(user_id, self.version, x) if "userKopList" in upsert: for x in upsert["userKopList"]: - await self.data.profile.put_kop(user_id, x) - - for rating_type in { - "userRatingBaseBestList", - "userRatingBaseBestNewList", - "userRatingBaseHotList", - "userRatingBaseNextList", - "userRatingBaseNextNewList", - "userRatingBaseHotNextList", - }: - if rating_type not in upsert: - continue - - await self.data.profile.put_profile_rating( - user_id, - self.version, - rating_type, - upsert[rating_type], - ) + self.data.profile.put_kop(user_id, x) return {"returnCode": 1, "apiName": "upsertUserAll"} - async def handle_get_user_rival_api_request(self, data: Dict) -> Dict: + def handle_get_user_rival_api_request(self, data: Dict) -> Dict: """ Added in Bright """ rival_list = [] - user_rivals = await self.data.profile.get_rivals(data["userId"]) + user_rivals = self.data.profile.get_rivals(data["userId"]) for rival in user_rivals: tmp = {} tmp["rivalUserId"] = rival[0] @@ -1153,13 +1094,13 @@ class OngekiBase: "userRivalList": rival_list, } - async def handle_get_user_rival_data_api_request(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 = await self.data.profile.get_profile_name( + name = self.data.profile.get_profile_name( rival["rivalUserId"], self.version ) if name is None: @@ -1171,7 +1112,7 @@ class OngekiBase: "userRivalDataList": rivals, } - async def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict: + def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict: """ Added in Bright """ @@ -1194,8 +1135,8 @@ class OngekiBase: } @cached(2) - async def util_generate_music_list(self, user_id: int) -> List: - music_detail = await self.data.score.get_best_scores(user_id) + def util_generate_music_list(self, user_id: int) -> List: + music_detail = self.data.score.get_best_scores(user_id) song_list = [] for md in music_detail: diff --git a/titles/ongeki/bright.py b/titles/ongeki/bright.py index 1eec9ca..49d6216 100644 --- a/titles/ongeki/bright.py +++ b/titles/ongeki/bright.py @@ -1,11 +1,13 @@ -from datetime import datetime +from datetime import date, datetime, timedelta +from typing import Any, Dict from random import randint -from typing import Dict +import pytz +import json from core.config import CoreConfig from titles.ongeki.base import OngekiBase -from titles.ongeki.config import OngekiConfig from titles.ongeki.const import OngekiConstants +from titles.ongeki.config import OngekiConfig class OngekiBright(OngekiBase): @@ -13,19 +15,19 @@ class OngekiBright(OngekiBase): super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_BRIGHT - async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = await super().handle_get_game_setting_api_request(data) + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.30.00" ret["gameSetting"]["onlineDataVersion"] = "1.30.00" return ret - async def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: + def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: # check for a bright profile - p = await self.data.profile.get_profile_data(data["userId"], self.version) + p = self.data.profile.get_profile_data(data["userId"], self.version) if p is None: return {} - cards = await self.data.card.get_user_cards(data["userId"]) + cards = self.data.card.get_user_cards(data["userId"]) if cards is None or len(cards) == 0: # This should never happen self.logger.error( @@ -53,14 +55,14 @@ class OngekiBright(OngekiBase): return {"userId": data["userId"], "userData": user_data} - async def handle_printer_login_api_request(self, data: Dict): + def handle_printer_login_api_request(self, data: Dict): return {"returnCode": 1} - async def handle_printer_logout_api_request(self, data: Dict): + def handle_printer_logout_api_request(self, data: Dict): return {"returnCode": 1} - async def handle_cm_get_user_card_api_request(self, data: Dict) -> Dict: - user_cards = await self.data.item.get_cards(data["userId"]) + 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 {} @@ -88,8 +90,8 @@ class OngekiBright(OngekiBase): "userCardList": card_list[start_idx:end_idx], } - async def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: - user_characters = await self.data.item.get_characters(data["userId"]) + 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"], @@ -122,8 +124,8 @@ class OngekiBright(OngekiBase): "userCharacterList": character_list[start_idx:end_idx], } - async def handle_get_user_gacha_api_request(self, data: Dict) -> Dict: - user_gachas = await self.data.item.get_user_gachas(data["userId"]) + 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": []} @@ -141,14 +143,12 @@ class OngekiBright(OngekiBase): "userGachaList": user_gacha_list, } - async def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: - return await self.handle_get_user_item_api_request(data) + def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: + return self.handle_get_user_item_api_request(data) - async def handle_cm_get_user_gacha_supply_api_request(self, data: Dict) -> Dict: + 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 = await self.data.item.get_user_gacha_supplies( - data["userId"] - ) + user_gacha_supplies = self.data.item.get_user_gacha_supplies(data["userId"]) if user_gacha_supplies is None: return {"supplyId": 1, "length": 0, "supplyCardList": []} @@ -160,7 +160,7 @@ class OngekiBright(OngekiBase): "supplyCardList": supply_list, } - async def handle_get_game_gacha_api_request(self, data: Dict) -> Dict: + 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 @@ -168,7 +168,7 @@ class OngekiBright(OngekiBase): 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 = await self.data.static.get_gacha(self.version, gacha_id) + game_gacha = self.data.static.get_gacha(self.version, gacha_id) if game_gacha: game_gachas.append(game_gacha) @@ -207,7 +207,7 @@ class OngekiBright(OngekiBase): "registIdList": [], } - async def handle_roll_gacha_api_request(self, data: Dict) -> Dict: + def handle_roll_gacha_api_request(self, data: Dict) -> Dict: """ Handle a gacha roll API request """ @@ -265,26 +265,26 @@ class OngekiBright(OngekiBase): return self.handle_roll_gacha_api_request(data) # get a list of cards for each rarity - cards_r = await self.data.static.get_cards_by_rarity(self.version, 1) + 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 = await self.data.static.get_gacha_cards(gacha_id) + 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 = await self.data.static.get_cards_by_rarity(self.version, 2) - cards_ssr = await self.data.static.get_cards_by_rarity(self.version, 3) + 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 = await self.data.static.get_gacha_cards(gacha_id) + 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: @@ -323,7 +323,7 @@ class OngekiBright(OngekiBase): "gameGachaCardList": game_gacha_card_list, } - async def handle_cm_upsert_user_gacha_api_request(self, data: Dict): + def handle_cm_upsert_user_gacha_api_request(self, data: Dict): upsert = data["cmUpsertUserGacha"] user_id = data["userId"] @@ -339,7 +339,7 @@ class OngekiBright(OngekiBase): daily_gacha_date = datetime.strptime("2000-01-01", "%Y-%m-%d") # check if the user previously rolled the exact same gacha - user_gacha = await self.data.item.get_user_gacha(user_id, gacha_id) + 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"] @@ -358,7 +358,7 @@ class OngekiBright(OngekiBase): daily_gacha_date = play_date daily_gacha_cnt = 0 - await self.data.item.put_user_gacha( + self.data.item.put_user_gacha( user_id, gacha_id, totalGachaCnt=total_gacha_count + gacha_count, @@ -375,29 +375,29 @@ class OngekiBright(OngekiBase): if "userData" in upsert and len(upsert["userData"]) > 0: # check if the profile is a bright memory profile - p = await self.data.profile.get_profile_data(data["userId"], self.version) + p = self.data.profile.get_profile_data(data["userId"], self.version) if p is not None: # save the bright memory profile - await self.data.profile.put_profile_data( + self.data.profile.put_profile_data( user_id, self.version, upsert["userData"][0] ) else: # save the bright profile - await self.data.profile.put_profile_data( + self.data.profile.put_profile_data( user_id, self.version, upsert["userData"][0] ) if "userCharacterList" in upsert: for x in upsert["userCharacterList"]: - await self.data.item.put_character(user_id, x) + self.data.item.put_character(user_id, x) if "userItemList" in upsert: for x in upsert["userItemList"]: - await self.data.item.put_item(user_id, x) + self.data.item.put_item(user_id, x) if "userCardList" in upsert: for x in upsert["userCardList"]: - await self.data.item.put_card(user_id, x) + self.data.item.put_card(user_id, x) # TODO? # if "gameGachaCardList" in upsert: @@ -405,35 +405,35 @@ class OngekiBright(OngekiBase): return {"returnCode": 1, "apiName": "CMUpsertUserGachaApi"} - async def handle_cm_upsert_user_select_gacha_api_request(self, data: Dict) -> Dict: + 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 = await self.data.profile.get_profile_data(data["userId"], self.version) + p = self.data.profile.get_profile_data(data["userId"], self.version) if p is not None: # save the bright memory profile - await self.data.profile.put_profile_data( + self.data.profile.put_profile_data( user_id, self.version, upsert["userData"][0] ) else: # save the bright profile - await self.data.profile.put_profile_data( + self.data.profile.put_profile_data( user_id, self.version, upsert["userData"][0] ) if "userCharacterList" in upsert: for x in upsert["userCharacterList"]: - await self.data.item.put_character(user_id, x) + self.data.item.put_character(user_id, x) if "userCardList" in upsert: for x in upsert["userCardList"]: - await self.data.item.put_card(user_id, x) + self.data.item.put_card(user_id, x) if "selectGachaLogList" in data: for x in data["selectGachaLogList"]: - await self.data.item.put_user_gacha( + self.data.item.put_user_gacha( user_id, x["gachaId"], selectPoint=0, @@ -442,8 +442,8 @@ class OngekiBright(OngekiBase): return {"returnCode": 1, "apiName": "cmUpsertUserSelectGacha"} - async def handle_get_game_gacha_card_by_id_api_request(self, data: Dict) -> Dict: - game_gacha_cards = await self.data.static.get_gacha_cards(data["gachaId"]) + 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 { @@ -522,7 +522,7 @@ class OngekiBright(OngekiBase): "ssrBookCalcList": [], } - async def handle_get_game_theater_api_request(self, data: Dict) -> Dict: + def handle_get_game_theater_api_request(self, data: Dict) -> Dict: """ shows a banner after every print, not sure what its used for """ @@ -548,7 +548,7 @@ class OngekiBright(OngekiBase): return {"length": 0, "gameTheaterList": [], "registIdList": []} - async def handle_cm_upsert_user_print_playlog_api_request(self, data: Dict) -> Dict: + def handle_cm_upsert_user_print_playlog_api_request(self, data: Dict) -> Dict: return { "returnCode": 1, "orderId": 0, @@ -556,7 +556,7 @@ class OngekiBright(OngekiBase): "apiName": "CMUpsertUserPrintPlaylogApi", } - async def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: + def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: return { "returnCode": 1, "orderId": 0, @@ -564,7 +564,7 @@ class OngekiBright(OngekiBase): "apiName": "CMUpsertUserPrintlogApi", } - async def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: + def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: user_print_detail = data["userPrintDetail"] # generate random serial id @@ -579,7 +579,7 @@ class OngekiBright(OngekiBase): ) # add the entry to the user print table with the random serialId - await self.data.item.put_user_print_detail( + self.data.item.put_user_print_detail( data["userId"], serial_id, user_print_detail ) @@ -589,27 +589,27 @@ class OngekiBright(OngekiBase): "apiName": "CMUpsertUserPrintApi", } - async def handle_cm_upsert_user_all_api_request(self, data: Dict) -> Dict: + 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 = await self.data.profile.get_profile_data(data["userId"], self.version) + p = self.data.profile.get_profile_data(data["userId"], self.version) if p is not None: # save the bright memory profile - await self.data.profile.put_profile_data( + self.data.profile.put_profile_data( user_id, self.version, upsert["userData"][0] ) else: # save the bright profile - await self.data.profile.put_profile_data( + self.data.profile.put_profile_data( user_id, self.version, upsert["userData"][0] ) if "userActivityList" in upsert: for act in upsert["userActivityList"]: - await self.data.profile.put_profile_activity( + self.data.profile.put_profile_activity( user_id, act["kind"], act["id"], @@ -622,10 +622,10 @@ class OngekiBright(OngekiBase): if "userItemList" in upsert: for x in upsert["userItemList"]: - await self.data.item.put_item(user_id, x) + self.data.item.put_item(user_id, x) if "userCardList" in upsert: for x in upsert["userCardList"]: - await self.data.item.put_card(user_id, x) + 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 index 7b41286..d7103a3 100644 --- a/titles/ongeki/brightmemory.py +++ b/titles/ongeki/brightmemory.py @@ -1,9 +1,13 @@ -from typing import Dict +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.config import OngekiConfig from titles.ongeki.const import OngekiConstants +from titles.ongeki.config import OngekiConfig class OngekiBrightMemory(OngekiBright): @@ -11,8 +15,8 @@ class OngekiBrightMemory(OngekiBright): super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY - async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = await super().handle_get_game_setting_api_request(data) + 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 @@ -23,8 +27,8 @@ class OngekiBrightMemory(OngekiBright): ret["gameSetting"]["maxCountRivalMusic"] = 300 return ret - async def handle_get_user_memory_chapter_api_request(self, data: Dict) -> Dict: - memories = await self.data.item.get_memorychapters(data["userId"]) + 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"], @@ -130,5 +134,5 @@ class OngekiBrightMemory(OngekiBright): "userMemoryChapterList": memory_chp, } - async def handle_get_game_music_release_state_api_request(self, data: Dict) -> Dict: + 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 5f60b6a..b952b2d 100644 --- a/titles/ongeki/config.py +++ b/titles/ongeki/config.py @@ -21,12 +21,12 @@ class OngekiServerConfig: 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 - ) + self.__config, "ongeki", "server", "use_https", default=False + ) class OngekiGachaConfig: @@ -54,7 +54,6 @@ class OngekiCardMakerVersionConfig: self.__config, "ongeki", "version", default={} ).get(version) - class OngekiCryptoConfig: def __init__(self, parent_config: "OngekiConfig") -> None: self.__config = parent_config @@ -76,7 +75,6 @@ class OngekiCryptoConfig: self.__config, "ongeki", "crypto", "encrypted_only", default=False ) - class OngekiConfig(dict): def __init__(self) -> None: self.server = OngekiServerConfig(self) diff --git a/titles/ongeki/const.py b/titles/ongeki/const.py index 520b311..a7658f7 100644 --- a/titles/ongeki/const.py +++ b/titles/ongeki/const.py @@ -1,3 +1,4 @@ +from typing import Final, Dict from enum import Enum diff --git a/titles/ongeki/database.py b/titles/ongeki/database.py index fd20939..89255c0 100644 --- a/titles/ongeki/database.py +++ b/titles/ongeki/database.py @@ -1,12 +1,7 @@ -from core.config import CoreConfig from core.data import Data -from titles.ongeki.schema import ( - OngekiItemData, - OngekiLogData, - OngekiProfileData, - OngekiScoreData, - OngekiStaticData, -) +from core.config import CoreConfig +from titles.ongeki.schema import OngekiItemData, OngekiProfileData, OngekiScoreData +from titles.ongeki.schema import OngekiStaticData, OngekiLogData class OngekiData(Data): diff --git a/titles/ongeki/frontend.py b/titles/ongeki/frontend.py index 665fd58..987776f 100644 --- a/titles/ongeki/frontend.py +++ b/titles/ongeki/frontend.py @@ -1,17 +1,17 @@ -from os import path -from typing import List - -import jinja2 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 core.frontend import FE_Base, UserSession -from starlette.requests import Request -from starlette.responses import RedirectResponse, Response -from starlette.routing import Route -from titles.ongeki.base import OngekiBase + 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): @@ -28,73 +28,60 @@ class OngekiFrontend(FE_Base): self.nav_name = "O.N.G.E.K.I." self.version_list = OngekiConstants.VERSION_NAMES - def get_routes(self) -> List[Route]: - return [Route("/", self.render_GET)] - - async def render_GET(self, request: Request) -> bytes: + def render_GET(self, request: Request) -> bytes: template = self.environment.get_template( - "titles/ongeki/templates/ongeki_index.jinja" + "titles/ongeki/frontend/ongeki_index.jinja" ) - usr_sesh = self.validate_session(request) - if not usr_sesh: - usr_sesh = UserSession() - + sesh: Session = request.getSession() + usr_sesh = IUserSession(sesh) self.version = usr_sesh.ongeki_version - if usr_sesh.user_id > 0: - profile_data = self.data.profile.get_profile_data( - usr_sesh.user_id, self.version - ) - rival_list = await self.data.profile.get_rivals(usr_sesh.user_id) - rival_data = {"userRivalList": rival_list, "userId": usr_sesh.user_id} + 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) - # Hay1tsme 01/09/2024: ?????????????????????????????????????????????????????????????? - rival_info = await OngekiBase.handle_get_user_rival_data_api_request( - self, rival_data - ) - - return Response( - 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), - ), - media_type="text/html; charset=utf-8", - ) + 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 RedirectResponse("/gate/", 303) - - async def render_POST(self, request: Request): + return redirectTo(b"/gate/", request) + + def render_POST(self, request: Request): uri = request.uri.decode() - usr_sesh = self.validate_session(request) - if not usr_sesh: - usr_sesh = UserSession() - - if usr_sesh.user_id > 0: + 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() - await self.data.profile.put_rival(usr_sesh.user_id, rival_id) - # self.logger.info(f"{usr_sesh.user_id} added a rival") - return RedirectResponse(b"/game/ongeki/", 303) - + 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() - await self.data.profile.delete_rival(usr_sesh.user_id, rival_id) + self.data.profile.delete_rival(usr_sesh.userId, rival_id) # self.logger.info(f"{response}") - return RedirectResponse(b"/game/ongeki/", 303) - + 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 RedirectResponse("/game/ongeki/", 303) - + 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: - Response("Something went wrong", status_code=500) + return b"Something went wrong" else: - return RedirectResponse("/gate/", 303) + return b"User is not logged in" diff --git a/titles/ongeki/index.py b/titles/ongeki/index.py index 318a989..baf1ee3 100644 --- a/titles/ongeki/index.py +++ b/titles/ongeki/index.py @@ -1,35 +1,32 @@ +from twisted.web.http import Request import json -import logging -import string -import zlib -from logging.handlers import TimedRotatingFileHandler -from os import path -from typing import Dict, List, Tuple - -import coloredlogs import inflection import yaml -from core.config import CoreConfig -from core.title import BaseServlet -from core.utils import Utils +import string +import logging +import coloredlogs +import zlib +from logging.handlers import TimedRotatingFileHandler from Crypto.Cipher import AES -from Crypto.Hash import SHA1 -from Crypto.Protocol.KDF import PBKDF2 from Crypto.Util.Padding import pad -from starlette.requests import Request -from starlette.responses import Response -from starlette.routing import Route +from Crypto.Protocol.KDF import PBKDF2 +from Crypto.Hash import SHA1 +from os import path +from typing import Tuple, Dict, List -from .base import OngekiBase -from .bright import OngekiBright -from .brightmemory import OngekiBrightMemory +from core.config import CoreConfig +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 .red import OngekiRed -from .redplus import OngekiRedPlus 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(BaseServlet): @@ -102,9 +99,7 @@ class OngekiServlet(BaseServlet): hmac_hash_module=SHA1, ) - hashed_name = hash.hex()[ - :32 - ] # truncate unused bytes like the game does + hashed_name = hash.hex()[:32] # truncate unused bytes like the game does self.hash_table[version][hashed_name] = method_fixed self.logger.debug( @@ -112,9 +107,7 @@ class OngekiServlet(BaseServlet): ) @classmethod - def is_game_enabled( - cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str - ) -> bool: + 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}"): @@ -124,43 +117,38 @@ class OngekiServlet(BaseServlet): if not game_cfg.server.enable: return False - + return True - - def get_routes(self) -> List[Route]: - return [ - Route( - "/SDDT/{version:int}/{endpoint:str}", self.render_POST, methods=["POST"] - ) - ] - - def get_allnet_info( - self, game_code: str, game_ver: int, keychip: str - ) -> Tuple[str, str]: + + 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" - ) + proto = "https" if self.game_cfg.server.use_https and game_ver >= 120 else "http" if proto == "https": - t_port = f":{title_port_ssl_int}" if title_port_ssl_int != 443 else "" - - else: - t_port = f":{title_port_int}" if title_port_int != 80 else "" + t_port = f":{title_port_ssl_int}" if title_port_ssl_int and not self.core_cfg.server.is_using_proxy else "" + + 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.server.hostname}{t_port}/{game_code}/{game_ver}/", - f"{self.core_cfg.server.hostname}{t_port}/", + f"{proto}://{self.core_cfg.title.hostname}{t_port}/{game_code}/{game_ver}/", + f"{self.core_cfg.title.hostname}{t_port}/", ) - async def render_POST(self, request: Request) -> bytes: - endpoint: str = request.path_params.get("endpoint", "") - version: int = request.path_params.get("version", 0) + def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes: + endpoint = matchers['endpoint'] + version = int(matchers['version']) if endpoint.lower() == "ping": - return Response(zlib.compress(b'{"returnCode": 1}')) + return zlib.compress(b'{"returnCode": 1}') - req_raw = await request.body() + req_raw = request.content.getvalue() encrtped = False internal_ver = 0 client_ip = Utils.get_ip_addr(request) @@ -179,7 +167,7 @@ class OngekiServlet(BaseServlet): internal_ver = OngekiConstants.VER_ONGEKI_RED_PLUS elif version >= 130 and version < 135: # Bright internal_ver = OngekiConstants.VER_ONGEKI_BRIGHT - elif version >= 135 and version < 145: # Bright Memory + 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: @@ -190,11 +178,13 @@ class OngekiServlet(BaseServlet): self.logger.error( f"v{version} does not support encryption or no keys entered" ) - return Response(zlib.compress(b'{"stat": "0"}')) + return zlib.compress(b'{"stat": "0"}') elif endpoint.lower() not in self.hash_table[internal_ver]: - self.logger.error(f"No hash found for v{version} endpoint {endpoint}") - return Response(zlib.compress(b'{"stat": "0"}')) + 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()] @@ -211,15 +201,19 @@ class OngekiServlet(BaseServlet): self.logger.error( f"Failed to decrypt v{version} request to {endpoint} -> {e}" ) - return Response(zlib.compress(b'{"stat": "0"}')) + return zlib.compress(b'{"stat": "0"}') encrtped = True - if not encrtped and self.game_cfg.crypto.encrypted_only and version >= 120: + 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 Response(zlib.compress(b'{"stat": "0"}')) + return zlib.compress(b'{"stat": "0"}') try: unzip = zlib.decompress(req_raw) @@ -228,26 +222,28 @@ class OngekiServlet(BaseServlet): self.logger.error( f"Failed to decompress v{version} {endpoint} request -> {e}" ) - return Response(zlib.compress(b'{"stat": "0"}')) + return zlib.compress(b'{"stat": "0"}') req_data = json.loads(unzip) - self.logger.info(f"v{version} {endpoint} request from {client_ip}") + 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 Response(zlib.compress(b'{"returnCode": 1}')) + return zlib.compress(b'{"returnCode": 1}') try: handler = getattr(self.versions[internal_ver], func_to_find) - resp = await handler(req_data) + resp = handler(req_data) except Exception as e: self.logger.error(f"Error handling v{version} method {endpoint} - {e}") - return Response(zlib.compress(b'{"stat": "0"}')) + return zlib.compress(b'{"stat": "0"}') if resp == None: resp = {"returnCode": 1} @@ -257,7 +253,7 @@ class OngekiServlet(BaseServlet): zipped = zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) if not encrtped or version < 120: - return Response(zipped) + return zipped padded = pad(zipped, 16) @@ -267,4 +263,4 @@ class OngekiServlet(BaseServlet): bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]), ) - return Response(crypt.encrypt(padded)) + return crypt.encrypt(padded) \ No newline at end of file diff --git a/titles/ongeki/plus.py b/titles/ongeki/plus.py index b1ce0ef..9168576 100644 --- a/titles/ongeki/plus.py +++ b/titles/ongeki/plus.py @@ -1,9 +1,9 @@ -from typing import Dict +from typing import Dict, Any from core.config import CoreConfig from titles.ongeki.base import OngekiBase -from titles.ongeki.config import OngekiConfig from titles.ongeki.const import OngekiConstants +from titles.ongeki.config import OngekiConfig class OngekiPlus(OngekiBase): @@ -11,8 +11,8 @@ class OngekiPlus(OngekiBase): super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_PLUS - async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = await super().handle_get_game_setting_api_request(data) + 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" ret["gameSetting"]["onlineDataVersion"] = "1.05.00" return ret diff --git a/titles/ongeki/read.py b/titles/ongeki/read.py index 832c09b..6e94094 100644 --- a/titles/ongeki/read.py +++ b/titles/ongeki/read.py @@ -1,11 +1,15 @@ +from decimal import Decimal +import logging import os +import re import xml.etree.ElementTree as ET -from typing import Optional +from typing import Any, Dict, List, Optional -from core.config import CoreConfig from read import BaseReader -from titles.ongeki.const import OngekiConstants +from core.config import CoreConfig from titles.ongeki.database import OngekiData +from titles.ongeki.const import OngekiConstants +from titles.ongeki.config import OngekiConfig class OngekiReader(BaseReader): @@ -28,7 +32,7 @@ class OngekiReader(BaseReader): self.logger.error(f"Invalid ongeki version {version}") exit(1) - async def read(self) -> None: + def read(self) -> None: data_dirs = [] if self.bin_dir is not None: data_dirs += self.get_data_directories(self.bin_dir) @@ -37,12 +41,12 @@ class OngekiReader(BaseReader): data_dirs += self.get_data_directories(self.opt_dir) for dir in data_dirs: - await self.read_events(f"{dir}/event") - await self.read_music(f"{dir}/music") - await self.read_card(f"{dir}/card") - await self.read_reward(f"{dir}/reward") + self.read_events(f"{dir}/event") + self.read_music(f"{dir}/music") + self.read_card(f"{dir}/card") + self.read_reward(f"{dir}/reward") - async def read_card(self, base_dir: str) -> None: + def read_card(self, base_dir: str) -> None: self.logger.info(f"Reading cards from {base_dir}...") version_ids = { @@ -66,7 +70,7 @@ class OngekiReader(BaseReader): # skip already existing cards if ( - await self.data.static.get_card( + self.data.static.get_card( OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY, card_id ) is not None @@ -96,7 +100,7 @@ class OngekiReader(BaseReader): version = version_ids[troot.find("VersionID").find("id").text] card_number = troot.find("CardNumberString").text - await self.data.static.put_card( + self.data.static.put_card( version, card_id, name=name, @@ -113,7 +117,7 @@ class OngekiReader(BaseReader): ) self.logger.info(f"Added card {card_id}") - async def read_events(self, base_dir: str) -> None: + def read_events(self, base_dir: str) -> None: self.logger.info(f"Reading events from {base_dir}...") for root, dirs, files in os.walk(base_dir): @@ -128,12 +132,10 @@ class OngekiReader(BaseReader): troot.find("EventType").text ].value - await self.data.static.put_event( - self.version, id, event_type, name - ) + self.data.static.put_event(self.version, id, event_type, name) self.logger.info(f"Added event {id}") - async def read_music(self, base_dir: str) -> None: + 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): @@ -166,36 +168,32 @@ class OngekiReader(BaseReader): f"{fumens_data.find('FumenConstIntegerPart').text}.{fumens_data.find('FumenConstFractionalPart').text}" ) - await self.data.static.put_chart( + 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}") - async def read_reward(self, base_dir: str) -> None: - self.logger.info(f"Reading rewards from {base_dir}...") + 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 = "" + 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() + with open(f"{root}/{dir}/Reward.xml", "r", encoding="utf-8") as f: + strdata = f.read() - troot = ET.fromstring(strdata) + troot = ET.fromstring(strdata) - if root is None: - continue + 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 + 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 - await self.data.static.put_reward( - self.version, rewardId, rewardname, itemKind, itemId - ) - self.logger.info(f"Added reward {rewardId}") + 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 4c3d2aa..52b9d59 100644 --- a/titles/ongeki/red.py +++ b/titles/ongeki/red.py @@ -1,9 +1,9 @@ -from typing import Dict +from typing import Dict, Any from core.config import CoreConfig from titles.ongeki.base import OngekiBase -from titles.ongeki.config import OngekiConfig from titles.ongeki.const import OngekiConstants +from titles.ongeki.config import OngekiConfig class OngekiRed(OngekiBase): @@ -11,8 +11,8 @@ class OngekiRed(OngekiBase): super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_RED - async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = await super().handle_get_game_setting_api_request(data) + 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" ret["gameSetting"]["onlineDataVersion"] = "1.20.00" return ret diff --git a/titles/ongeki/redplus.py b/titles/ongeki/redplus.py index 7b148d5..1f69690 100644 --- a/titles/ongeki/redplus.py +++ b/titles/ongeki/redplus.py @@ -1,9 +1,9 @@ -from typing import Dict +from typing import Dict, Any from core.config import CoreConfig from titles.ongeki.base import OngekiBase -from titles.ongeki.config import OngekiConfig from titles.ongeki.const import OngekiConstants +from titles.ongeki.config import OngekiConfig class OngekiRedPlus(OngekiBase): @@ -11,8 +11,8 @@ class OngekiRedPlus(OngekiBase): super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_RED_PLUS - async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = await super().handle_get_game_setting_api_request(data) + 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" ret["gameSetting"]["onlineDataVersion"] = "1.25.00" ret["gameSetting"]["maxCountCharacter"] = 50 diff --git a/titles/ongeki/schema/__init__.py b/titles/ongeki/schema/__init__.py index 239d42b..b93a16c 100644 --- a/titles/ongeki/schema/__init__.py +++ b/titles/ongeki/schema/__init__.py @@ -1,8 +1,8 @@ -from titles.ongeki.schema.item import OngekiItemData -from titles.ongeki.schema.log import OngekiLogData from titles.ongeki.schema.profile import OngekiProfileData -from titles.ongeki.schema.score import OngekiScoreData +from titles.ongeki.schema.item import OngekiItemData from titles.ongeki.schema.static import OngekiStaticData +from titles.ongeki.schema.score import OngekiScoreData +from titles.ongeki.schema.log import OngekiLogData __all__ = [ OngekiProfileData, diff --git a/titles/ongeki/schema/item.py b/titles/ongeki/schema/item.py index 9ed04cc..55b4c68 100644 --- a/titles/ongeki/schema/item.py +++ b/titles/ongeki/schema/item.py @@ -1,13 +1,13 @@ -from datetime import datetime -from typing import Dict, List, Optional +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 from core.data.schema import BaseData, metadata -from sqlalchemy import Column, Table, UniqueConstraint, and_ -from sqlalchemy.dialects.mysql import insert -from sqlalchemy.engine import Row -from sqlalchemy.schema import ForeignKey -from sqlalchemy.sql import func, select -from sqlalchemy.types import TIMESTAMP, Boolean, Integer, String card = Table( "ongeki_user_card", @@ -258,11 +258,7 @@ 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("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), @@ -342,153 +338,148 @@ print_detail = Table( mysql_charset="utf8mb4", ) - class OngekiItemData(BaseData): - async def put_card(self, aime_id: int, card_data: Dict) -> Optional[int]: + def put_card(self, aime_id: int, card_data: Dict) -> Optional[int]: card_data["user"] = aime_id sql = insert(card).values(**card_data) conflict = sql.on_duplicate_key_update(**card_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning(f"put_card: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - async def get_cards(self, aime_id: int) -> Optional[List[Dict]]: + def get_cards(self, aime_id: int) -> Optional[List[Dict]]: sql = select(card).where(card.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_character(self, aime_id: int, character_data: Dict) -> Optional[int]: + def put_character(self, aime_id: int, character_data: Dict) -> Optional[int]: character_data["user"] = aime_id sql = insert(character).values(**character_data) conflict = sql.on_duplicate_key_update(**character_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning(f"put_character: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - async def get_characters(self, aime_id: int) -> Optional[List[Dict]]: + def get_characters(self, aime_id: int) -> Optional[List[Dict]]: sql = select(character).where(character.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_deck(self, aime_id: int, deck_data: Dict) -> Optional[int]: + def put_deck(self, aime_id: int, deck_data: Dict) -> Optional[int]: deck_data["user"] = aime_id sql = insert(deck).values(**deck_data) conflict = sql.on_duplicate_key_update(**deck_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning(f"put_deck: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - async def get_deck(self, aime_id: int, deck_id: int) -> Optional[Dict]: + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_decks(self, aime_id: int) -> Optional[List[Dict]]: + def get_decks(self, aime_id: int) -> Optional[List[Dict]]: sql = select(deck).where(deck.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_boss(self, aime_id: int, boss_data: Dict) -> Optional[int]: + def put_boss(self, aime_id: int, boss_data: Dict) -> Optional[int]: boss_data["user"] = aime_id sql = insert(boss).values(**boss_data) conflict = sql.on_duplicate_key_update(**boss_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning(f"put_boss: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - async def put_story(self, aime_id: int, story_data: Dict) -> Optional[int]: + 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 = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning(f"put_story: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - async def get_stories(self, aime_id: int) -> Optional[List[Dict]]: + def get_stories(self, aime_id: int) -> Optional[List[Dict]]: sql = select(story).where(story.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_chapter(self, aime_id: int, chapter_data: Dict) -> Optional[int]: + def put_chapter(self, aime_id: int, chapter_data: Dict) -> Optional[int]: chapter_data["user"] = aime_id sql = insert(chapter).values(**chapter_data) conflict = sql.on_duplicate_key_update(**chapter_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning(f"put_chapter: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - async def get_chapters(self, aime_id: int) -> Optional[List[Dict]]: + def get_chapters(self, aime_id: int) -> Optional[List[Dict]]: sql = select(chapter).where(chapter.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_item(self, aime_id: int, item_data: Dict) -> Optional[int]: + def put_item(self, aime_id: int, item_data: Dict) -> Optional[int]: item_data["user"] = aime_id sql = insert(item).values(**item_data) conflict = sql.on_duplicate_key_update(**item_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning(f"put_item: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - async def get_item( - self, aime_id: int, item_id: int, item_kind: int - ) -> Optional[Dict]: + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_items( - self, aime_id: int, item_kind: int = None - ) -> Optional[List[Dict]]: + 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: @@ -496,89 +487,73 @@ class OngekiItemData(BaseData): and_(item.c.user == aime_id, item.c.itemKind == item_kind) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_music_item( - self, aime_id: int, music_item_data: Dict - ) -> Optional[int]: + def put_music_item(self, aime_id: int, music_item_data: Dict) -> Optional[int]: music_item_data["user"] = aime_id sql = insert(music_item).values(**music_item_data) conflict = sql.on_duplicate_key_update(**music_item_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning(f"put_music_item: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - async def get_music_items(self, aime_id: int) -> Optional[List[Dict]]: + def get_music_items(self, aime_id: int) -> Optional[List[Dict]]: sql = select(music_item).where(music_item.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_login_bonus( - self, aime_id: int, login_bonus_data: Dict - ) -> Optional[int]: + def put_login_bonus(self, aime_id: int, login_bonus_data: Dict) -> Optional[int]: login_bonus_data["user"] = aime_id sql = insert(login_bonus).values(**login_bonus_data) conflict = sql.on_duplicate_key_update(**login_bonus_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: - self.logger.warning( - 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 - async def get_login_bonuses(self, aime_id: int) -> Optional[List[Dict]]: + def get_login_bonuses(self, aime_id: int) -> Optional[List[Dict]]: sql = select(login_bonus).where(login_bonus.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_mission_point( - self, aime_id: int, version: 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) conflict = sql.on_duplicate_key_update(**mission_point_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: - self.logger.warning( - 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 - async 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 = await self.execute(sql) + 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 return result.fetchall() - async def put_event_point( - self, aime_id: int, version: 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 @@ -589,107 +564,95 @@ class OngekiItemData(BaseData): sql = insert(event_point).values(**event_point_data) conflict = sql.on_duplicate_key_update(**event_point_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: - self.logger.warning( - 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 - async def get_event_points(self, aime_id: int) -> Optional[List[Dict]]: + def get_event_points(self, aime_id: int) -> Optional[List[Dict]]: sql = select(event_point).where(event_point.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_scenerio(self, aime_id: int, scenerio_data: Dict) -> Optional[int]: + def put_scenerio(self, aime_id: int, scenerio_data: Dict) -> Optional[int]: scenerio_data["user"] = aime_id sql = insert(scenerio).values(**scenerio_data) conflict = sql.on_duplicate_key_update(**scenerio_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning(f"put_scenerio: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - async def get_scenerios(self, aime_id: int) -> Optional[List[Dict]]: + def get_scenerios(self, aime_id: int) -> Optional[List[Dict]]: sql = select(scenerio).where(scenerio.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_trade_item( - self, aime_id: int, trade_item_data: Dict - ) -> Optional[int]: + def put_trade_item(self, aime_id: int, trade_item_data: Dict) -> Optional[int]: trade_item_data["user"] = aime_id sql = insert(trade_item).values(**trade_item_data) conflict = sql.on_duplicate_key_update(**trade_item_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning(f"put_trade_item: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - async def get_trade_items(self, aime_id: int) -> Optional[List[Dict]]: + def get_trade_items(self, aime_id: int) -> Optional[List[Dict]]: sql = select(trade_item).where(trade_item.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_event_music( - self, aime_id: int, event_music_data: Dict - ) -> Optional[int]: + def put_event_music(self, aime_id: int, event_music_data: Dict) -> Optional[int]: event_music_data["user"] = aime_id sql = insert(event_music).values(**event_music_data) conflict = sql.on_duplicate_key_update(**event_music_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: - self.logger.warning( - 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 - async def get_event_music(self, aime_id: int) -> Optional[List[Dict]]: + def get_event_music(self, aime_id: int) -> Optional[List[Dict]]: sql = select(event_music).where(event_music.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_tech_event( - self, aime_id: int, version: 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 = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning(f"put_tech_event: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - async def put_tech_event_ranking( - self, aime_id: int, version: int, tech_event_data: Dict - ) -> Optional[int]: + 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") @@ -699,95 +662,87 @@ class OngekiItemData(BaseData): sql = insert(tech_ranking).values(**tech_event_data) conflict = sql.on_duplicate_key_update(**tech_event_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: - self.logger.warning( - f"put_tech_event_ranking: Failed to update ranking! aime_id {aime_id}" - ) + self.logger.warning(f"put_tech_event_ranking: Failed to update ranking! aime_id {aime_id}") return None return result.lastrowid - async 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 = await self.execute(sql) + 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 return result.fetchall() - async def get_bosses(self, aime_id: int) -> Optional[List[Dict]]: + def get_bosses(self, aime_id: int) -> Optional[List[Dict]]: sql = select(boss).where(boss.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_memorychapter( + 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 = await self.execute(conflict) + result = self.execute(conflict) if result is None: - self.logger.warning( - f"put_memorychapter: Failed to update! aime_id: {aime_id}" - ) + self.logger.warning(f"put_memorychapter: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - async def get_memorychapters(self, aime_id: int) -> Optional[List[Dict]]: + def get_memorychapters(self, aime_id: int) -> Optional[List[Dict]]: sql = select(memorychapter).where(memorychapter.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_user_gacha(self, aime_id: int, gacha_id: int) -> Optional[Row]: + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_user_gachas(self, aime_id: int) -> Optional[List[Row]]: + def get_user_gachas(self, aime_id: int) -> Optional[List[Row]]: sql = gacha.select(gacha.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_user_gacha_supplies(self, aime_id: int) -> Optional[List[Row]]: + def get_user_gacha_supplies(self, aime_id: int) -> Optional[List[Row]]: sql = gacha_supply.select(gacha_supply.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_user_gacha( - self, aime_id: int, gacha_id: int, **data - ) -> Optional[int]: + 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 = await self.execute(conflict) + 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 - async def put_user_print_detail( + def put_user_print_detail( self, aime_id: int, serial_id: str, user_print_data: Dict ) -> Optional[int]: sql = insert(print_detail).values( @@ -795,7 +750,7 @@ class OngekiItemData(BaseData): ) conflict = sql.on_duplicate_key_update(user=aime_id, **user_print_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning( @@ -804,52 +759,19 @@ class OngekiItemData(BaseData): return None return result.lastrowid - async def get_ranking_event_ranks( - self, version: int, aime_id: int - ) -> Optional[List[Dict]]: + + 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 = await self.execute(sql) + 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" - ) + self.logger.error(f"failed to rank aime_id: {aime_id} ranking event positions") return None return result.fetchall() - async 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 = await self.execute(sql) + 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 diff --git a/titles/ongeki/schema/log.py b/titles/ongeki/schema/log.py index 9561e9b..bd5b071 100644 --- a/titles/ongeki/schema/log.py +++ b/titles/ongeki/schema/log.py @@ -1,10 +1,11 @@ -from typing import Optional +from typing import Dict, Optional +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON +from sqlalchemy.schema import ForeignKey +from sqlalchemy.sql import func, select +from sqlalchemy.dialects.mysql import insert from core.data.schema import BaseData, metadata -from sqlalchemy import Column, Table -from sqlalchemy.dialects.mysql import insert -from sqlalchemy.schema import ForeignKey -from sqlalchemy.types import Boolean, Integer, String gp_log = Table( "ongeki_gp_log", @@ -38,7 +39,7 @@ session_log = Table( class OngekiLogData(BaseData): - async def put_gp_log( + def put_gp_log( self, aime_id: Optional[int], used_credit: int, @@ -60,7 +61,7 @@ class OngekiLogData(BaseData): currentGP=current_gp, ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: self.logger.warning( f"put_gp_log: Failed to insert GP log! aime_id: {aime_id} kind {kind} pattern {pattern} current_gp {current_gp}" diff --git a/titles/ongeki/schema/profile.py b/titles/ongeki/schema/profile.py index 937fc72..6071bad 100644 --- a/titles/ongeki/schema/profile.py +++ b/titles/ongeki/schema/profile.py @@ -1,14 +1,14 @@ from typing import Dict, List, Optional - -from core.config import CoreConfig -from core.data.schema import BaseData, metadata -from sqlalchemy import Column, Table, UniqueConstraint, and_ -from sqlalchemy.dialects.mysql import insert -from sqlalchemy.engine import Row +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 delete, select -from sqlalchemy.types import JSON, BigInteger, Boolean, Integer, String +from sqlalchemy.sql import func, select, delete +from sqlalchemy.engine import Row +from sqlalchemy.dialects.mysql import insert + +from core.data.schema import BaseData, metadata +from core.config import CoreConfig # Cammel case column names technically don't follow the other games but # it makes it way easier on me to not fuck with what the games has @@ -246,26 +246,6 @@ rival = Table( mysql_charset="utf8mb4", ) -rating = Table( - "ongeki_profile_rating", - 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("type", String(255), nullable=False), - Column("index", Integer, nullable=False), - Column("musicId", Integer), - Column("difficultId", Integer), - Column("romVersionCode", Integer), - Column("score", Integer), - UniqueConstraint("user", "version", "type", "index", name="ongeki_profile_rating_best_uk"), - mysql_charset="utf8mb4", -) - class OngekiProfileData(BaseData): def __init__(self, cfg: CoreConfig, conn: Connection) -> None: @@ -275,12 +255,12 @@ class OngekiProfileData(BaseData): ) self.date_time_format_short = "%Y-%m-%d" - async def get_profile_name(self, aime_id: int, version: int) -> Optional[str]: + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: return None @@ -289,20 +269,20 @@ class OngekiProfileData(BaseData): return None return row["userName"] - - async def get_profile_preview(self, aime_id: int, version: int) -> Optional[Row]: + + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_profile_data(self, aime_id: int, version: int) -> Optional[Row]: + def get_profile_data(self, aime_id: int, version: int) -> Optional[Row]: sql = select(profile).where( and_( profile.c.user == aime_id, @@ -310,40 +290,40 @@ class OngekiProfileData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_profile_options(self, aime_id: int) -> Optional[Row]: + def get_profile_options(self, aime_id: int) -> Optional[Row]: sql = select(option).where( and_( option.c.user == aime_id, ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_profile_recent_rating(self, aime_id: int) -> Optional[List[Row]]: + def get_profile_recent_rating(self, aime_id: int) -> Optional[List[Row]]: sql = select(recent_rating).where(recent_rating.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_profile_rating_log(self, aime_id: int) -> Optional[List[Row]]: + def get_profile_rating_log(self, aime_id: int) -> Optional[List[Row]]: sql = select(rating_log).where(rating_log.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_profile_activity( + def get_profile_activity( self, aime_id: int, kind: int = None ) -> Optional[List[Row]]: sql = select(activity).where( @@ -353,53 +333,47 @@ class OngekiProfileData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_kop(self, aime_id: int) -> Optional[List[Row]]: + def get_kop(self, aime_id: int) -> Optional[List[Row]]: sql = select(kop).where(kop.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_rivals(self, aime_id: int) -> Optional[List[Row]]: + def get_rivals(self, aime_id: int) -> Optional[List[Row]]: sql = select(rival.c.rivalUserId).where(rival.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_profile_data( - self, aime_id: int, version: int, data: Dict - ) -> Optional[int]: + def put_profile_data(self, aime_id: int, version: int, data: Dict) -> Optional[int]: data["user"] = aime_id data["version"] = version data.pop("accessCode") sql = insert(profile).values(**data) conflict = sql.on_duplicate_key_update(**data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: - self.logger.warning( - 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 - async def put_profile_options( - self, aime_id: int, options_data: Dict - ) -> Optional[int]: + def put_profile_options(self, aime_id: int, options_data: Dict) -> Optional[int]: options_data["user"] = aime_id sql = insert(option).values(**options_data) conflict = sql.on_duplicate_key_update(**options_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning( @@ -408,7 +382,7 @@ class OngekiProfileData(BaseData): return None return result.lastrowid - async def put_profile_recent_rating( + def put_profile_recent_rating( self, aime_id: int, recent_rating_data: List[Dict] ) -> Optional[int]: sql = insert(recent_rating).values( @@ -417,7 +391,7 @@ class OngekiProfileData(BaseData): conflict = sql.on_duplicate_key_update(recentRating=recent_rating_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning( f"put_profile_recent_rating: failed to update recent rating! aime_id {aime_id}" @@ -425,12 +399,12 @@ class OngekiProfileData(BaseData): return None return result.lastrowid - async def put_profile_bp_list( + def put_profile_bp_list( self, aime_id: int, bp_base_list: List[Dict] ) -> Optional[int]: pass - async def put_profile_rating_log( + def put_profile_rating_log( self, aime_id: int, data_version: str, highest_rating: int ) -> Optional[int]: sql = insert(rating_log).values( @@ -439,7 +413,7 @@ class OngekiProfileData(BaseData): conflict = sql.on_duplicate_key_update(highestRating=highest_rating) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: 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}" @@ -447,7 +421,7 @@ class OngekiProfileData(BaseData): return None return result.lastrowid - async def put_profile_activity( + def put_profile_activity( self, aime_id: int, kind: int, @@ -473,7 +447,7 @@ class OngekiProfileData(BaseData): sortNumber=sort_num, param1=p1, param2=p2, param3=p3, param4=p4 ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning( f"put_profile_activity: failed to put activity! aime_id {aime_id} kind {kind} activity_id {activity_id}" @@ -481,9 +455,7 @@ class OngekiProfileData(BaseData): return None return result.lastrowid - async def put_profile_region( - self, aime_id: int, region: int, date: str - ) -> Optional[int]: + 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 ) @@ -492,7 +464,7 @@ class OngekiProfileData(BaseData): playCount=activity.c.playCount + 1, ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning( f"put_profile_region: failed to update! aime_id {aime_id} region {region}" @@ -500,80 +472,46 @@ class OngekiProfileData(BaseData): return None return result.lastrowid - async def put_training_room(self, aime_id: int, room_detail: Dict) -> Optional[int]: + def put_training_room(self, aime_id: int, room_detail: Dict) -> Optional[int]: room_detail["user"] = aime_id sql = insert(training_room).values(**room_detail) conflict = sql.on_duplicate_key_update(**room_detail) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: - self.logger.warning( - 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 - async def put_kop(self, aime_id: int, kop_data: Dict) -> Optional[int]: + def put_kop(self, aime_id: int, kop_data: Dict) -> Optional[int]: kop_data["user"] = aime_id sql = insert(kop).values(**kop_data) conflict = sql.on_duplicate_key_update(**kop_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning(f"put_kop: Failed to add score! aime_id: {aime_id}") return None return result.lastrowid - async def put_rival(self, aime_id: int, rival_id: int) -> Optional[int]: + 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 = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning( f"put_rival: failed to update! aime_id: {aime_id}, rival_id: {rival_id}" ) return None return result.lastrowid - - async 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 = await self.execute(sql) + 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}" - ) + self.logger.error(f"delete_rival: failed to delete! aime_id: {aime_id}, rival_id: {rival_id}") else: - return result.rowcount -<<<<<<< Updated upstream - - async def put_profile_rating( - self, - aime_id: int, - version: int, - rating_type: str, - rating_data: List[Dict], - ): - inserted_values = [ - {"user": aime_id, "version": version, "type": rating_type, "index": i, **x} - for (i, x) in enumerate(rating_data) - ] - sql = insert(rating).values(inserted_values) - update_dict = {x.name: x for x in sql.inserted if x.name != "id"} - sql = sql.on_duplicate_key_update(**update_dict) - result = await self.execute(sql) - - if result is None: - self.logger.warn( - f"put_profile_rating_{rating_type}: Could not insert rating entries, aime_id: {aime_id}", - ) - return - - return result.lastrowid -======= ->>>>>>> Stashed changes + return result.rowcount \ No newline at end of file diff --git a/titles/ongeki/schema/score.py b/titles/ongeki/schema/score.py index 925ef2b..7c8ce15 100644 --- a/titles/ongeki/schema/score.py +++ b/titles/ongeki/schema/score.py @@ -1,11 +1,11 @@ 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.schema import ForeignKey +from sqlalchemy.sql import func, select +from sqlalchemy.dialects.mysql import insert from core.data.schema import BaseData, metadata -from sqlalchemy import Column, Table, UniqueConstraint -from sqlalchemy.dialects.mysql import insert -from sqlalchemy.schema import ForeignKey -from sqlalchemy.sql import select -from sqlalchemy.types import TIMESTAMP, Boolean, Float, Integer, String score_best = Table( "ongeki_score_best", @@ -128,59 +128,53 @@ tech_count = Table( class OngekiScoreData(BaseData): - async def get_tech_count(self, aime_id: int) -> Optional[List[Dict]]: + def get_tech_count(self, aime_id: int) -> Optional[List[Dict]]: return [] - async def put_tech_count( - self, aime_id: int, tech_count_data: Dict - ) -> Optional[int]: + def put_tech_count(self, aime_id: int, tech_count_data: Dict) -> Optional[int]: tech_count_data["user"] = aime_id sql = insert(tech_count).values(**tech_count_data) conflict = sql.on_duplicate_key_update(**tech_count_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning(f"put_tech_count: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - async def get_best_scores(self, aime_id: int) -> Optional[List[Dict]]: + def get_best_scores(self, aime_id: int) -> Optional[List[Dict]]: sql = select(score_best).where(score_best.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_best_score( + def get_best_score( self, aime_id: int, song_id: int, chart_id: int = None ) -> Optional[List[Dict]]: return [] - async def put_best_score(self, aime_id: int, music_detail: Dict) -> Optional[int]: + def put_best_score(self, aime_id: int, music_detail: Dict) -> Optional[int]: music_detail["user"] = aime_id sql = insert(score_best).values(**music_detail) conflict = sql.on_duplicate_key_update(**music_detail) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: - self.logger.warning( - 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 - async def put_playlog(self, aime_id: int, playlog_data: Dict) -> Optional[int]: + def put_playlog(self, aime_id: int, playlog_data: Dict) -> Optional[int]: playlog_data["user"] = aime_id sql = insert(playlog).values(**playlog_data) - result = await self.execute(sql) + result = self.execute(sql) if result is None: - self.logger.warning( - 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 diff --git a/titles/ongeki/schema/static.py b/titles/ongeki/schema/static.py index 20a7923..695d39a 100644 --- a/titles/ongeki/schema/static.py +++ b/titles/ongeki/schema/static.py @@ -1,12 +1,13 @@ 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.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.data.schema.arcade import machine -from sqlalchemy import Column, Table, UniqueConstraint, and_ -from sqlalchemy.dialects.mysql import insert -from sqlalchemy.engine import Row -from sqlalchemy.sql import func, select -from sqlalchemy.types import TIMESTAMP, Boolean, Float, Integer, String events = Table( "ongeki_static_events", @@ -180,40 +181,33 @@ game_point = Table( 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" - ), + 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): - async def put_card(self, version: int, card_id: int, **card_data) -> Optional[int]: + 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 = await self.execute(conflict) + 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 - async def get_card(self, version: int, card_id: int) -> Optional[Dict]: + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_card_by_card_number( - self, version: int, card_number: str - ) -> Optional[Dict]: + 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}" @@ -221,38 +215,36 @@ class OngekiStaticData(BaseData): and_(cards.c.version <= version, cards.c.cardNumber == card_number) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_card_by_name(self, version: int, name: str) -> Optional[Dict]: + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_cards(self, version: int) -> Optional[List[Dict]]: + def get_cards(self, version: int) -> Optional[List[Dict]]: sql = cards.select(cards.c.version <= version) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_cards_by_rarity( - self, version: int, rarity: int - ) -> Optional[List[Dict]]: + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_gacha( + def put_gacha( self, version: int, gacha_id: int, @@ -276,33 +268,33 @@ class OngekiStaticData(BaseData): **gacha_data, ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning(f"Failed to insert gacha! gacha_id {gacha_id}") return None return result.lastrowid - async def get_gacha(self, version: int, gacha_id: int) -> Optional[Dict]: + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_gachas(self, version: int) -> Optional[List[Dict]]: + def get_gachas(self, version: int) -> Optional[List[Dict]]: sql = gachas.select(gachas.c.version == version).order_by( gachas.c.gachaId.asc() ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_gacha_card( + 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) @@ -311,21 +303,21 @@ class OngekiStaticData(BaseData): gachaId=gacha_id, cardId=card_id, **gacha_card ) - result = await self.execute(conflict) + 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 - async def get_gacha_cards(self, gacha_id: int) -> Optional[List[Dict]]: + def get_gacha_cards(self, gacha_id: int) -> Optional[List[Dict]]: sql = gacha_cards.select(gacha_cards.c.gachaId == gacha_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_event( + def put_event( self, version: int, event_id: int, event_type: int, event_name: str ) -> Optional[int]: sql = insert(events).values( @@ -340,41 +332,41 @@ class OngekiStaticData(BaseData): name=event_name, ) - result = await self.execute(conflict) + 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 - async def get_event(self, version: int, event_id: int) -> Optional[List[Dict]]: + 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) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_events(self, version: int) -> Optional[List[Dict]]: + def get_events(self, version: int) -> Optional[List[Dict]]: sql = select(events).where(events.c.version == version) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_enabled_events(self, version: int) -> Optional[List[Dict]]: + def get_enabled_events(self, version: int) -> Optional[List[Dict]]: sql = select(events).where( and_(events.c.version == version, events.c.enabled == True) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_chart( + def put_chart( self, version: int, song_id: int, @@ -401,7 +393,7 @@ class OngekiStaticData(BaseData): level=level, ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning( f"Failed to insert chart! song_id: {song_id}, chart_id: {chart_id}" @@ -409,15 +401,15 @@ class OngekiStaticData(BaseData): return None return result.lastrowid - async def get_chart( + def get_chart( self, version: int, song_id: int, chart_id: int = None ) -> Optional[List[Dict]]: pass - async def get_music(self, version: int) -> Optional[List[Dict]]: + def get_music(self, version: int) -> Optional[List[Dict]]: pass - async def get_music_chart( + def get_music_chart( self, version: int, song_id: int, chart_id: int ) -> Optional[List[Row]]: sql = select(music).where( @@ -428,127 +420,93 @@ class OngekiStaticData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async 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 = await self.execute(sql) + 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() - async def put_reward( - self, version: int, rewardId: int, rewardname: str, itemKind: int, itemId: int - ) -> Optional[int]: + 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, - ) + version=version, + rewardId=rewardId, + rewardname=rewardname, + itemKind=itemKind, + itemId=itemId, + ) conflict = sql.on_duplicate_key_update( - rewardname=rewardname, - ) - result = await self.execute(conflict) + 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 - async def get_reward_list(self, version: int) -> Optional[List[Dict]]: + def get_reward_list(self, version: int) -> Optional[List[Dict]]: sql = select(rewards).where(rewards.c.version == version) - result = await self.execute(sql) + result = self.execute(sql) if result is None: self.logger.warning(f"Failed to load reward list") return None return result.fetchall() - async def get_present_list(self, version: int) -> Optional[List[Dict]]: + def get_present_list(self, version: int) -> Optional[List[Dict]]: sql = select(present).where(present.c.version == version) - result = await self.execute(sql) + result = self.execute(sql) if result is None: self.logger.warning(f"Failed to load present list") return None return result.fetchall() - async def get_tech_music(self, version: int) -> Optional[List[Dict]]: + def get_tech_music(self, version: int) -> Optional[List[Dict]]: sql = select(tech_music).where(tech_music.c.version == version) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_client_testmode_data( - self, region_id: int, client_testmode_data: Dict - ) -> Optional[List[Dict]]: + 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 - ) + conflict = sql.on_duplicate_key_update(regionId=region_id, **client_testmode_data) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: - ( - self.logger.warning( - f"region_id: {region_id} Failed to update ClientTestMode data" - ), - ) + self.logger.warning(f"region_id: {region_id} Failed to update ClientTestMode data"), return None return result.lastrowid - async 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 - ) + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: - ( - self.logger.warning( - f"machine_id: {machine_id} Failed to update ClientSetting data" - ), - ) + self.logger.warning(f"machine_id: {machine_id} Failed to update ClientSetting data"), return None return result.lastrowid - async 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}, - ] + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: self.logger.warning(f"Failed to insert default GP table!") return None return result.lastrowid - async 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 = await self.execute(sql) + 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 16fbbb6..adc8c0f 100644 --- a/titles/ongeki/summer.py +++ b/titles/ongeki/summer.py @@ -1,9 +1,9 @@ -from typing import Dict +from typing import Dict, Any from core.config import CoreConfig from titles.ongeki.base import OngekiBase -from titles.ongeki.config import OngekiConfig from titles.ongeki.const import OngekiConstants +from titles.ongeki.config import OngekiConfig class OngekiSummer(OngekiBase): @@ -11,8 +11,8 @@ class OngekiSummer(OngekiBase): super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_SUMMER - async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = await super().handle_get_game_setting_api_request(data) + 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" ret["gameSetting"]["onlineDataVersion"] = "1.10.00" return ret diff --git a/titles/ongeki/summerplus.py b/titles/ongeki/summerplus.py index abe4d08..8b2cd03 100644 --- a/titles/ongeki/summerplus.py +++ b/titles/ongeki/summerplus.py @@ -1,9 +1,9 @@ -from typing import Dict +from typing import Dict, Any from core.config import CoreConfig from titles.ongeki.base import OngekiBase -from titles.ongeki.config import OngekiConfig from titles.ongeki.const import OngekiConstants +from titles.ongeki.config import OngekiConfig class OngekiSummerPlus(OngekiBase): @@ -11,8 +11,8 @@ class OngekiSummerPlus(OngekiBase): super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_SUMMER_PLUS - async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = await super().handle_get_game_setting_api_request(data) + 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" ret["gameSetting"]["onlineDataVersion"] = "1.15.00" return ret diff --git a/titles/ongeki/templates/js/ongeki_scripts.js b/titles/ongeki/templates/js/ongeki_scripts.js deleted file mode 100644 index 6de309b..0000000 --- a/titles/ongeki/templates/js/ongeki_scripts.js +++ /dev/null @@ -1,24 +0,0 @@ -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/templates/ongeki_index.jinja b/titles/ongeki/templates/ongeki_index.jinja deleted file mode 100644 index a4c4004..0000000 --- a/titles/ongeki/templates/ongeki_index.jinja +++ /dev/null @@ -1,92 +0,0 @@ -{% extends "core/templates/index.jinja" %} -{% block content %} - -{% if sesh is defined and sesh["user_id"] > 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/pokken/__init__.py b/titles/pokken/__init__.py index 69040f6..94237c4 100644 --- a/titles/pokken/__init__.py +++ b/titles/pokken/__init__.py @@ -1,9 +1,10 @@ +from .index import PokkenServlet from .const import PokkenConstants from .database import PokkenData from .frontend import PokkenFrontend -from .index import PokkenServlet index = PokkenServlet database = PokkenData game_codes = [PokkenConstants.GAME_CODE] +current_schema_version = 1 frontend = PokkenFrontend diff --git a/titles/pokken/base.py b/titles/pokken/base.py index 96bdc17..9663e81 100644 --- a/titles/pokken/base.py +++ b/titles/pokken/base.py @@ -1,14 +1,14 @@ -import json -import logging -from datetime import datetime +from datetime import datetime, timedelta +import json, logging from typing import Any, Dict, List +import random +from core.data import Data from core import CoreConfig - from .config import PokkenConfig -from .const import PokkenConstants -from .database import PokkenData from .proto import jackal_pb2 +from .database import PokkenData +from .const import PokkenConstants class PokkenBase: @@ -20,21 +20,21 @@ class PokkenBase: self.data = PokkenData(core_cfg) self.SUPPORT_SET_NONE = 4294967295 - async def handle_noop(self, request: Any) -> bytes: + def handle_noop(self, request: Any) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = request.type return res.SerializeToString() - async def handle_ping(self, request: jackal_pb2.Request) -> 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() - async def handle_register_pcb(self, request: jackal_pb2.Request) -> 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 @@ -57,39 +57,39 @@ class PokkenBase: "port": self.game_cfg.server.stun_server_port, }, "AdmissionUrl": f"ws://{self.game_cfg.server.hostname}:{self.game_cfg.ports.admission}", - "locationId": 123, # FIXME: Get arcade's ID from the database + "locationId": 123, # FIXME: Get arcade's ID from the database "logfilename": "JackalMatchingLibrary.log", "biwalogfilename": "./biwa.log", } - regist_pcb.bnp_baseuri = f"{self.core_cfg.server.hostname}/bna" + regist_pcb.bnp_baseuri = f"{self.core_cfg.title.hostname}/bna" regist_pcb.biwa_setting = json.dumps(biwa_setting) res.register_pcb.CopyFrom(regist_pcb) return res.SerializeToString() - async def handle_save_ads(self, request: jackal_pb2.Request) -> 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() - async def handle_save_client_log(self, request: jackal_pb2.Request) -> 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() - async def handle_check_diagnosis(self, request: jackal_pb2.Request) -> 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() - async def handle_load_client_settings(self, request: jackal_pb2.Request) -> 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 @@ -112,7 +112,7 @@ class PokkenBase: return res.SerializeToString() - async def handle_load_ranking(self, request: jackal_pb2.Request) -> bytes: + def handle_load_ranking(self, request: jackal_pb2.Request) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.LOAD_RANKING @@ -126,17 +126,17 @@ class PokkenBase: res.load_ranking.CopyFrom(ranking) return res.SerializeToString() - async def handle_load_user(self, request: jackal_pb2.Request) -> bytes: + 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 = await self.data.card.get_user_id_from_card(access_code) + 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 = await self.data.user.create_user() - card_id = await self.data.card.create_card(user_id, access_code) + 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})" @@ -160,7 +160,7 @@ class PokkenBase: event_achievement_flag event_achievement_param """ - profile = await self.data.profile.get_profile(user_id) + profile = self.data.profile.get_profile(user_id) load_usr.commidserv_result = 1 load_usr.load_hash = 1 load_usr.cardlock_status = False @@ -169,7 +169,7 @@ class PokkenBase: load_usr.precedent_release_flag = 0xFFFFFFFF if profile is None: - profile_id = await self.data.profile.create_profile(user_id) + profile_id = self.data.profile.create_profile(user_id) profile_dict = {"id": profile_id, "user": user_id} pokemon_data = [] tutorial_progress = [] @@ -184,7 +184,7 @@ class PokkenBase: self.logger.info( f"Card-in user {user_id} (Trainer name {profile_dict.get('trainer_name', '')})" ) - pokemon_data = await self.data.profile.get_all_pokemon_data(user_id) + pokemon_data = self.data.profile.get_all_pokemon_data(user_id) tutorial_progress = [] rankmatch_progress = [] achievement_flag = [] @@ -266,49 +266,47 @@ class PokkenBase: 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) + 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() - async def handle_set_bnpassid_lock(self, data: jackal_pb2.Request) -> bytes: + 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() - async def handle_save_user(self, request: jackal_pb2.Request) -> bytes: + 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_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 @@ -318,7 +316,7 @@ class PokkenBase: 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 # ? + 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 @@ -326,28 +324,22 @@ class PokkenBase: battle = req.battle_data mon = req.pokemon_data - p = await self.data.profile.touch_profile(user_id) + p = self.data.profile.touch_profile(user_id) if p is None or not p: - await self.data.profile.create_profile(user_id) + 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 - await self.data.profile.set_profile_name( - user_id, - req.trainer_name_pending, - req.avatar_gender if req.avatar_gender else None, - ) + 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) - - await self.data.profile.update_profile_tutorial_flags(user_id, tut_flgs) + + self.data.profile.update_profile_tutorial_flags(user_id, tut_flgs) for ach_flg in req.achievement_flag: ach_flgs.append(ach_flg) - - await self.data.profile.update_profile_tutorial_flags(user_id, 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) @@ -355,64 +347,39 @@ class PokkenBase: for evt_param in req.event_achievement_param: evt_params.append(evt_param) - await self.data.profile.update_profile_event( - user_id, evt_state, evt_flgs, evt_params, req.last_play_event_id - ) - + 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: - await self.data.item.add_reward( - user_id, - reward.get_category_id, - reward.get_content_id, - reward.get_type_id, - ) - - await self.data.profile.add_profile_points( - user_id, get_rank_pts, get_money, get_score_pts, grade_max - ) - - await self.data.profile.update_support_team( - user_id, 1, req.support_set_1[0], req.support_set_1[1] - ) - await self.data.profile.update_support_team( - user_id, 2, req.support_set_2[0], req.support_set_2[1] - ) - await self.data.profile.update_support_team( - user_id, 3, req.support_set_3[0], req.support_set_3[1] - ) - - await self.data.profile.put_pokemon( - user_id, - mon.char_id, - mon.illustration_book_no, - mon.bp_point_atk, - mon.bp_point_res, - mon.bp_point_def, - mon.bp_point_sp, - ) - await self.data.profile.add_pokemon_xp( - user_id, mon.char_id, mon.get_pokemon_exp - ) + 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)): - await 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_pokemon_battle_result( + user_id, + mon.char_id, + PokkenConstants.BATTLE_TYPE(battle.play_mode[x]), + PokkenConstants.BATTLE_RESULT(battle.result[x]) ) - await self.data.profile.put_stats( + 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, + num_continues ) - await self.data.profile.put_extra( + self.data.profile.put_extra( user_id, extra_counter, evt_reward_get_flg, @@ -421,56 +388,67 @@ class PokkenBase: use_support_ct, beat_num, aid_skill, - last_evt, + last_evt ) + return res.SerializeToString() - async def handle_save_ingame_log(self, data: jackal_pb2.Request) -> bytes: + 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() - async def handle_save_charge(self, data: jackal_pb2.Request) -> bytes: + 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() - async def handle_matching_noop( + def handle_matching_noop( self, data: Dict = {}, client_ip: str = "127.0.0.1" ) -> Dict: return {} - async def handle_matching_start_matching( + def handle_matching_start_matching( self, data: Dict = {}, client_ip: str = "127.0.0.1" ) -> Dict: return {} - async def handle_matching_is_matching( + 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": [], + "sessionId":"12345678", + "A":{ + "pcb_id": data["data"]["must"]["pcb_id"], + "gip": client_ip + }, + "list":[] } } - async def handle_matching_stop_matching( + def handle_matching_stop_matching( self, data: Dict = {}, client_ip: str = "127.0.0.1" ) -> Dict: return {} - async def handle_admission_noop( - self, data: Dict, req_ip: str = "127.0.0.1" - ) -> Dict: + def handle_admission_noop(self, data: Dict, req_ip: str = "127.0.0.1") -> Dict: return {} - - async def handle_admission_joinsession( - self, data: Dict, req_ip: str = "127.0.0.1" - ) -> Dict: + + 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}} + return { + 'data': { + "id": 12345678 + } + } diff --git a/titles/pokken/config.py b/titles/pokken/config.py index 45c5956..d3741af 100644 --- a/titles/pokken/config.py +++ b/titles/pokken/config.py @@ -43,18 +43,14 @@ class PokkenServerConfig: 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", + self.__config, "pokken", "server", "stun_server_host", default="stunserver.stunprotocol.org" ) @property @@ -66,11 +62,10 @@ class PokkenServerConfig: self.__config, "pokken", "server", "stun_server_port", default=3478 ) - class PokkenPortsConfig: def __init__(self, parent_config: "PokkenConfig"): self.__config = parent_config - + @property def game(self) -> int: return CoreConfig.get_config_field( @@ -82,7 +77,7 @@ class PokkenPortsConfig: return CoreConfig.get_config_field( self.__config, "pokken", "ports", "admission", default=9001 ) - + class PokkenConfig(dict): def __init__(self) -> None: diff --git a/titles/pokken/const.py b/titles/pokken/const.py index 4983d50..9fa3b06 100644 --- a/titles/pokken/const.py +++ b/titles/pokken/const.py @@ -3,7 +3,6 @@ from enum import Enum class PokkenConstants: GAME_CODE = "SDAK" - GAME_CDS = ["PKF1"] CONFIG_NAME = "pokken.yaml" @@ -11,12 +10,6 @@ class PokkenConstants: VERSION_NAMES = "Pokken Tournament" - SERIAL_IDENT = [2747] - NETID_PREFIX = ["ABGN"] - SERIAL_REGIONS = [1] - SERIAL_ROLES = [3] - SERIAL_CAB_IDENTS = [19] - class BATTLE_TYPE(Enum): TUTORIAL = 1 AI = 2 diff --git a/titles/pokken/database.py b/titles/pokken/database.py index 303dc9f..272cfd8 100644 --- a/titles/pokken/database.py +++ b/titles/pokken/database.py @@ -1,5 +1,5 @@ -from core.config import CoreConfig from core.data import Data +from core.config import CoreConfig from .schema import * diff --git a/titles/pokken/frontend.py b/titles/pokken/frontend.py index 46f3297..af344dc 100644 --- a/titles/pokken/frontend.py +++ b/titles/pokken/frontend.py @@ -1,23 +1,17 @@ -from os import path -from typing import List - -import jinja2 import yaml -from core.config import CoreConfig -from core.frontend import FE_Base, UserSession -from starlette.requests import Request -from starlette.responses import RedirectResponse, Response -from starlette.routing import Route +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 -from .database import PokkenData class PokkenFrontend(FE_Base): - SN_PREFIX = PokkenConstants.SERIAL_IDENT - NETID_PREFIX = PokkenConstants.NETID_PREFIX - def __init__( self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str ) -> None: @@ -30,79 +24,16 @@ class PokkenFrontend(FE_Base): ) self.nav_name = "Pokken" - def get_routes(self) -> List[Route]: - return [ - Route("/", self.render_GET, methods=["GET"]), - Route("/update.name", self.change_name, methods=["POST"]), - ] - - async def render_GET(self, request: Request) -> Response: + def render_GET(self, request: Request) -> bytes: template = self.environment.get_template( - "titles/pokken/templates/pokken_index.jinja" - ) - pf = None - - usr_sesh = self.validate_session(request) - if not usr_sesh: - usr_sesh = UserSession() - - else: - profile = await self.data.profile.get_profile(usr_sesh.user_id) - if profile is not None and profile["trainer_name"]: - pf = profile._asdict() - - if "e" in request.query_params: - try: - err = int(request.query_params.get("e", 0)) - except Exception: - err = 0 - - else: - err = 0 - - if "s" in request.query_params: - try: - succ = int(request.query_params.get("s", 0)) - except Exception: - succ = 0 - - else: - succ = 0 - - return Response( - template.render( - title=f"{self.core_config.server.name} | {self.nav_name}", - game_list=self.environment.globals["game_list"], - sesh=vars(usr_sesh), - profile=pf, - error=err, - success=succ, - ), - media_type="text/html; charset=utf-8", + "titles/pokken/frontend/pokken_index.jinja" ) - async def change_name(self, request: Request) -> RedirectResponse: - usr_sesh = self.validate_session(request) - if not usr_sesh: - return RedirectResponse("/game/pokken/?e=9", 303) + sesh: Session = request.getSession() + usr_sesh = IUserSession(sesh) - frm = await request.form() - new_name = frm.get("new_name") - gender = frm.get("new_gender", 1) - - if len(new_name) > 14: - return RedirectResponse("/game/pokken/?e=8", 303) - - if not gender.isdigit(): - return RedirectResponse("/game/pokken/?e=4", 303) - - gender = int(gender) - - if gender != 1 and gender != 2: - return RedirectResponse( - "/game/pokken/?e=4", 303 - ) # no code for this yet, whatever - - await self.data.profile.set_profile_name(usr_sesh.user_id, new_name, gender) - - return RedirectResponse("/game/pokken/?s=1", 303) + 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/index.py b/titles/pokken/index.py index 26c5444..30b6617 100644 --- a/titles/pokken/index.py +++ b/titles/pokken/index.py @@ -1,25 +1,23 @@ -import ast -import logging +from typing import Tuple, List, Dict +from twisted.web.http import Request +from twisted.web import resource +from twisted.internet import reactor +import json, ast from datetime import datetime -from logging.handlers import TimedRotatingFileHandler -from os import path -from typing import Dict, List, Tuple - -import coloredlogs -import inflection import yaml +import logging, coloredlogs +from logging.handlers import TimedRotatingFileHandler +import inflection +from os import path +from google.protobuf.message import DecodeError + from core import CoreConfig, Utils from core.title import BaseServlet -from google.protobuf.message import DecodeError -from starlette.requests import Request -from starlette.responses import JSONResponse, Response -from starlette.routing import Route, WebSocketRoute -from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState - -from .base import PokkenBase from .config import PokkenConfig +from .base import PokkenBase from .const import PokkenConstants from .proto import jackal_pb2 +from .services import PokkenAdmissionFactory class PokkenServlet(BaseServlet): @@ -58,9 +56,7 @@ class PokkenServlet(BaseServlet): self.base = PokkenBase(core_cfg, self.game_cfg) @classmethod - def is_game_enabled( - cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str - ) -> bool: + def is_game_enabled(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> bool: game_cfg = PokkenConfig() if path.exists(f"{cfg_dir}/{PokkenConstants.CONFIG_NAME}"): @@ -70,82 +66,50 @@ class PokkenServlet(BaseServlet): if not game_cfg.server.enable: return False - + return True - - def get_routes(self) -> List[Route]: - return [ - Route("/pokken/", self.render_POST, methods=["POST"]), - Route("/pokken/matching", self.handle_matching, methods=["POST"]), - WebSocketRoute("/pokken/admission", self.handle_admission), - ] - - def get_allnet_info( - self, game_code: str, game_ver: int, keychip: str - ) -> Tuple[str, str]: + + def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: return ( - f"https://{self.game_cfg.server.hostname}:{Utils.get_title_port_ssl(self.core_cfg)}/pokken/", - f"{self.game_cfg.server.hostname}:{Utils.get_title_port_ssl(self.core_cfg)}/pokken/", + [], + [ + ("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]: - if not self.game_cfg.server.enable: - return (False, [], []) + game_cfg = PokkenConfig() - return (True, PokkenConstants.GAME_CDS, PokkenConstants.NETID_PREFIX) - - async def handle_admission(self, ws: WebSocket) -> None: - client_ip = Utils.get_ip_addr(ws) - await ws.accept() - while True: - try: - msg: Dict = await ws.receive_json() - except WebSocketDisconnect as e: - self.logger.debug(f"Client {client_ip} disconnected - {e}") - break - except Exception as e: - self.logger.error( - f"Could not load JSON from message from {client_ip} - {e}" - ) - if ws.client_state != WebSocketState.DISCONNECTED: - await ws.close() - break - - self.logger.debug( - f"Admission: Message from {client_ip}:{ws.client.port} - {msg}" + if path.exists(f"{cfg_dir}/{PokkenConstants.CONFIG_NAME}"): + game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{PokkenConstants.CONFIG_NAME}")) ) - api = msg.get("api", "noop") - handler = getattr(self.base, f"handle_admission_{api.lower()}") - resp = await handler(msg, client_ip) + if not game_cfg.server.enable: + return (False, "") - if resp is None: - resp = {} + return (True, "PKF1") - 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" + 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) + ) - self.logger.debug(f"Websocket response: {resp}") - try: - await ws.send_json(resp) - except WebSocketDisconnect as e: - self.logger.debug(f"Client {client_ip} disconnected - {e}") - break - except Exception as e: - self.logger.error(f"Could not send JSON message to {client_ip} - {e}") - break - - if ws.client_state != WebSocketState.DISCONNECTED: - await ws.close() - - async def render_POST(self, request: Request) -> bytes: - content = await request.body() + 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"" @@ -170,19 +134,19 @@ class PokkenServlet(BaseServlet): self.logger.info(f"{endpoint} request from {Utils.get_ip_addr(request)}") - ret = await handler(pokken_request) - return Response(ret) + ret = handler(pokken_request) + return ret - async def handle_matching(self, request: Request) -> bytes: + def handle_matching(self, request: Request, game_code: str, matchers: Dict) -> bytes: if not self.game_cfg.server.enable_matching: - return Response() - - content = await request.body() + 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 JSONResponse(self.base.handle_matching_noop()) + return json.dumps(self.base.handle_matching_noop()).encode() json_content = ast.literal_eval( content.decode() @@ -202,7 +166,7 @@ class PokkenServlet(BaseServlet): self.logger.warning( f"No handler found for message type {json_content['call']}" ) - return JSONResponse(self.base.handle_matching_noop()) + return json.dumps(self.base.handle_matching_noop()).encode() ret = handler(json_content, client_ip) @@ -217,4 +181,4 @@ class PokkenServlet(BaseServlet): self.logger.debug(f"Response {ret}") - return JSONResponse(ret) + return json.dumps(ret).encode() diff --git a/titles/pokken/proto/jackal_pb2.py b/titles/pokken/proto/jackal_pb2.py index 2c228f1..9a2e30e 100644 --- a/titles/pokken/proto/jackal_pb2.py +++ b/titles/pokken/proto/jackal_pb2.py @@ -2,11 +2,10 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # source: jackal.proto """Generated protocol buffer code.""" - +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 -from google.protobuf.internal import builder as _builder # @@protoc_insertion_point(imports) diff --git a/titles/pokken/schema/__init__.py b/titles/pokken/schema/__init__.py index e668c14..81b8132 100644 --- a/titles/pokken/schema/__init__.py +++ b/titles/pokken/schema/__init__.py @@ -1,5 +1,4 @@ -# ruff: noqa: F401 -from .item import PokkenItemData -from .match import PokkenMatchData 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 index 5a70a41..f68d6d9 100644 --- a/titles/pokken/schema/item.py +++ b/titles/pokken/schema/item.py @@ -1,10 +1,12 @@ -from typing import Optional +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 sqlalchemy import Column, Table, UniqueConstraint -from sqlalchemy.dialects.mysql import insert -from sqlalchemy.schema import ForeignKey -from sqlalchemy.types import Integer item = Table( "pokken_item", @@ -29,9 +31,7 @@ class PokkenItemData(BaseData): Items obtained as rewards """ - async def add_reward( - self, user_id: int, category: int, content: int, item_type: int - ) -> Optional[int]: + 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, @@ -43,10 +43,8 @@ class PokkenItemData(BaseData): content=content, ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: - self.logger.warning( - f"Failed to insert reward for user {user_id}: {category}-{content}-{item_type}" - ) + 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 index a19a8c3..c84ec63 100644 --- a/titles/pokken/schema/match.py +++ b/titles/pokken/schema/match.py @@ -1,10 +1,12 @@ -from typing import Dict, List, Optional +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 sqlalchemy import Column, Table -from sqlalchemy.engine import Row -from sqlalchemy.schema import ForeignKey -from sqlalchemy.types import JSON, TIMESTAMP, Integer # Pokken sends depressingly little match data... match_data = Table( @@ -37,14 +39,14 @@ class PokkenMatchData(BaseData): Match logs """ - async def save_match(self, user_id: int, match_data: Dict) -> Optional[int]: + def save_match(self, user_id: int, match_data: Dict) -> Optional[int]: pass - async def get_match(self, match_id: int) -> Optional[Row]: + def get_match(self, match_id: int) -> Optional[Row]: pass - async def get_matches_by_user(self, user_id: int) -> Optional[List[Row]]: + def get_matches_by_user(self, user_id: int) -> Optional[List[Row]]: pass - async def get_matches(self, limit: int = 20) -> Optional[List[Row]]: + 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 index 6799ee8..ab81d77 100644 --- a/titles/pokken/schema/profile.py +++ b/titles/pokken/schema/profile.py @@ -1,14 +1,13 @@ -from typing import List, Optional, Union +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 sqlalchemy import Column, Table, UniqueConstraint, and_ -from sqlalchemy.dialects.mysql import insert -from sqlalchemy.engine import Row -from sqlalchemy.schema import ForeignKey -from sqlalchemy.sql import select, update -from sqlalchemy.sql.functions import coalesce -from sqlalchemy.types import JSON, Boolean, Integer, String - 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 @@ -127,7 +126,7 @@ pokemon_data = Table( Column("win_vs_lan", Integer), Column("battle_num_vs_cpu", Integer), # 2 Column("win_cpu", Integer), - Column("battle_all_num_tutorial", Integer), # ??? + Column("battle_all_num_tutorial", Integer), # ??? Column("battle_num_tutorial", Integer), # 1? Column("bp_point_atk", Integer), Column("bp_point_res", Integer), @@ -139,44 +138,38 @@ pokemon_data = Table( class PokkenProfileData(BaseData): - async def touch_profile(self, user_id: int) -> Optional[int]: + def touch_profile(self, user_id: int) -> Optional[int]: sql = select([profile.c.id]).where(profile.c.user == user_id) - - result = await self.execute(sql) + + result = self.execute(sql) if result is None: return None - return result.fetchone()["id"] + return result.fetchone()['id'] - async def create_profile(self, user_id: int) -> Optional[int]: + 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 = await self.execute(conflict) + 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 - async def set_profile_name( - self, user_id: int, new_name: str, gender: Union[int, None] = None - ) -> None: - sql = ( - update(profile) - .where(profile.c.user == user_id) - .values( - trainer_name=new_name, - avatar_gender=gender if gender is not None else profile.c.avatar_gender, - ) + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: self.logger.error( f"Failed to update pokken profile name for user {user_id}!" ) - async def put_extra( - self, - user_id: int, + def put_extra( + self, + user_id: int, extra_counter: int, evt_reward_get_flg: int, total_play_days: int, @@ -184,107 +177,74 @@ class PokkenProfileData(BaseData): use_support_ct: int, beat_num: int, aid_skill: int, - last_evt: 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, - ) + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: self.logger.error(f"Failed to put extra data for user {user_id}") - async def update_profile_tutorial_flags( - self, user_id: int, tutorial_flags: List - ) -> None: - sql = ( - update(profile) - .where(profile.c.user == user_id) - .values( - tutorial_progress_flag=tutorial_flags, - ) + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: self.logger.error( f"Failed to update pokken profile tutorial flags for user {user_id}!" ) - async 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, - ) + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: self.logger.error( f"Failed to update pokken profile achievement flags for user {user_id}!" ) - async def update_profile_event( - self, - user_id: int, - event_state: List, - event_flags: List[int], - event_param: List[int], - last_evt: int = None, - ) -> None: - sql = ( - update(profile) - .where(profile.c.user == user_id) - .values( - 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, - ) + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: self.logger.error( f"Failed to update pokken profile event state for user {user_id}!" ) - async def add_profile_points( + 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, - ) + 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 ) - async def get_profile(self, user_id: int) -> Optional[Row]: + def get_profile(self, user_id: int) -> Optional[Row]: sql = profile.select(profile.c.user == user_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def put_pokemon( + def put_pokemon( self, user_id: int, pokemon_id: int, @@ -292,7 +252,7 @@ class PokkenProfileData(BaseData): atk: int, res: int, defe: int, - sp: int, + sp: int ) -> Optional[int]: sql = insert(pokemon_data).values( user=user_id, @@ -307,10 +267,10 @@ class PokkenProfileData(BaseData): 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, + 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( @@ -321,106 +281,66 @@ class PokkenProfileData(BaseData): bp_point_sp=pokemon_data.c.bp_point_sp + sp, ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: - self.logger.warning( - f"Failed to insert pokemon ID {pokemon_id} for user {user_id}" - ) + self.logger.warning(f"Failed to insert pokemon ID {pokemon_id} for user {user_id}") return None return result.lastrowid - async 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) + 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 = await self.execute(sql) + 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}" - ) + self.logger.warning(f"Failed to add {xp} XP to pokemon ID {pokemon_id} for user {user_id}") - async def get_pokemon_data(self, user_id: int, pokemon_id: int) -> Optional[Row]: - sql = pokemon_data.select( - and_(pokemon_data.c.user == user_id, pokemon_data.c.char_id == pokemon_id) - ) - result = await self.execute(sql) + 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() - async def get_all_pokemon_data(self, user_id: int) -> Optional[List[Row]]: + def get_all_pokemon_data(self, user_id: int) -> Optional[List[Row]]: sql = pokemon_data.select(pokemon_data.c.user == user_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def put_pokemon_battle_result( - self, - user_id: int, - pokemon_id: int, - match_type: PokkenConstants.BATTLE_TYPE, - match_result: PokkenConstants.BATTLE_RESULT, + 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), - ) + 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 = await self.execute(sql) + 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})" - ) + 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})") - async def put_stats( + def put_stats( self, user_id: int, exkos: int, @@ -433,53 +353,29 @@ class PokkenProfileData(BaseData): """ 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, - ) + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: self.logger.warning(f"Failed to update stats for user {user_id}") - async def update_support_team( - self, user_id: int, support_id: int, support1: int = None, support2: int = None - ) -> None: - sql = ( - update(profile) - .where(profile.c.user == user_id) - .values( - 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, - ) + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: - self.logger.warning( - f"Failed to update support team {support_id} for user {user_id}" - ) + 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 index ace0249..121ebc4 100644 --- a/titles/pokken/schema/static.py +++ b/titles/pokken/schema/static.py @@ -1,4 +1,12 @@ -from core.data.schema import BaseData +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): diff --git a/titles/pokken/templates/pokken_index.jinja b/titles/pokken/templates/pokken_index.jinja deleted file mode 100644 index 4752bc8..0000000 --- a/titles/pokken/templates/pokken_index.jinja +++ /dev/null @@ -1,48 +0,0 @@ -{% extends "core/templates/index.jinja" %} -{% block content %} -

Pokken

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

Profile for {{ profile.trainer_name }} 

-{% if error is defined %} -{% include "core/templates/widgets/err_banner.jinja" %} -{% endif %} -{% if success is defined and success == 1 %} -
-Update successful -
-{% endif %} - -{% elif sesh is defined and sesh is not none and sesh.user_id > 0 %} -No profile information found for this account. -{% else %} -Login to view profile information. -{% endif %} -{% endblock content %} \ No newline at end of file diff --git a/titles/sao/__init__.py b/titles/sao/__init__.py index 8d6947e..15a46f9 100644 --- a/titles/sao/__init__.py +++ b/titles/sao/__init__.py @@ -1,9 +1,10 @@ +from .index import SaoServlet from .const import SaoConstants from .database import SaoData -from .index import SaoServlet 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 index 5735107..09a9f88 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -1,21 +1,21 @@ import logging from csv import * -from os import path from random import choice from typing import Dict, List +from os import path +from core.data import Data from core import CoreConfig -from titles.sao.handlers.base import * - 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.data = SaoData(core_cfg) + self.core_data = Data(core_cfg) + self.game_data = SaoData(core_cfg) self.version = 0 self.logger = logging.getLogger("sao") @@ -23,497 +23,376 @@ class SaoBase: 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=",") + 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 - async def handle_noop(self, header: SaoRequestHeader, request: bytes) -> bytes: + def handle_noop(self, header: SaoRequestHeader, request: bytes) -> bytes: self.logger.info(f"Using Generic handler") resp_thing = SaoNoopResponse(header.cmd + 1) return resp_thing.make() - async def handle_c122(self, header: SaoRequestHeader, request: bytes) -> bytes: - # common/get_maintenance_info - resp = SaoGetMaintResponse(header.cmd + 1) + def handle_c122(self, header: SaoRequestHeader, request: bytes) -> bytes: + #common/get_maintenance_info + resp = SaoGetMaintResponse(header.cmd +1) return resp.make() - async def handle_c12a(self, header: SaoRequestHeader, request: bytes) -> bytes: - # common/give_free_ticket - req = SaoGiveFreeTicketRequest(header, request) - self.logger.info( - f"Give {req.give_num} free tickets (id {req.ticket_id}) to user {req.user_id}" - ) - resp = SaoGiveFreeTicketResponse(header.cmd + 1) + def handle_c12e(self, header: SaoRequestHeader, request: bytes) -> bytes: + #common/ac_cabinet_boot_notification + resp = SaoCommonAcCabinetBootNotificationResponse(header.cmd +1) return resp.make() - async def handle_c12e(self, header: SaoRequestHeader, request: bytes) -> bytes: - # common/ac_cabinet_boot_notification - resp = SaoCommonAcCabinetBootNotificationResponse(header.cmd + 1) + def handle_c100(self, header: SaoRequestHeader, request: bytes) -> bytes: + #common/get_app_versions + resp = SaoCommonGetAppVersionsRequest(header.cmd +1) return resp.make() - async def handle_c100(self, header: SaoRequestHeader, request: bytes) -> bytes: - # common/get_app_versions - resp = SaoCommonGetAppVersionsRequest(header.cmd + 1) + def handle_c102(self, header: SaoRequestHeader, request: bytes) -> bytes: + #common/master_data_version_check + resp = SaoMasterDataVersionCheckResponse(header.cmd +1) return resp.make() - async def handle_c102(self, header: SaoRequestHeader, request: bytes) -> bytes: - # common/master_data_version_check - resp = SaoMasterDataVersionCheckResponse(header.cmd + 1) + def handle_c10a(self, header: SaoRequestHeader, request: bytes) -> bytes: + #common/paying_play_start + resp = SaoCommonPayingPlayStartRequest(header.cmd +1) return resp.make() - async def handle_c10a(self, header: SaoRequestHeader, request: bytes) -> bytes: - # common/paying_play_start - resp = SaoCommonPayingPlayStartRequest(header.cmd + 1) + def handle_ca02(self, header: SaoRequestHeader, request: bytes) -> bytes: + #quest_multi_play_room/get_quest_scene_multi_play_photon_server + resp = SaoGetQuestSceneMultiPlayPhotonServerResponse(header.cmd +1) return resp.make() - async def handle_ca02(self, header: SaoRequestHeader, request: bytes) -> bytes: - # quest_multi_play_room/get_quest_scene_multi_play_photon_server - resp = SaoGetQuestSceneMultiPlayPhotonServerResponse(header.cmd + 1) - return resp.make() - - async def handle_c11e(self, header: SaoRequestHeader, request: bytes) -> bytes: - # common/get_auth_card_data + def handle_c11e(self, header: SaoRequestHeader, request: bytes) -> bytes: + #common/get_auth_card_data req = SaoGetAuthCardDataRequest(header, request) - # Check authentication - user_id = await self.data.card.get_user_id_from_card(req.access_code) + #Check authentication + user_id = self.core_data.card.get_user_id_from_card( req.access_code ) if not user_id: - user_id = await self.data.user.create_user() # works - card_id = await self.data.card.create_card(user_id, req.access_code) + 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 = await self.data.profile.create_profile(user_id) - await self.data.item.put_hero_log( - user_id, 101000010, 1, 0, 201000000, 0, 1002, 1003, 1014, 30001, 30310 - ) - await self.data.item.put_hero_log( - user_id, 102000010, 1, 0, 202000000, 0, 3001, 3002, 3004, 30007, 3011 - ) - await self.data.item.put_hero_log( - user_id, - 105000010, - 1, - 0, - 209000000, - 0, - 10005, - 10002, - 10004, - 30006, - 10003, - ) - await self.data.item.put_hero_log( - user_id, 101000110, 1, 0, 201000000, 101000110, 2002, 2001, 2014, 0, 0 - ) - await self.data.item.put_hero_party( - user_id, 0, 101000010, 102000010, 105000010 - ) - await self.data.item.put_equipment_data(user_id, 201000000, 1, 200, 0, 0, 0) - await self.data.item.put_equipment_data(user_id, 202000000, 1, 200, 0, 0, 0) - await self.data.item.put_equipment_data(user_id, 209000000, 1, 200, 0, 0, 0) - await self.data.item.put_equipment_data(user_id, 101000110, 1, 200, 0, 0, 0) - await self.data.item.put_player_quest(user_id, 1001, True, 300, 0, 0, 1) + 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 = await self.data.profile.get_profile(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 = await self.data.profile.create_profile(user_id) - await self.data.item.put_hero_log( - user_id, 101000010, 1, 0, 201000000, 0, 1002, 1003, 1014, 30001, 30310 - ) - await self.data.item.put_hero_log( - user_id, 102000010, 1, 0, 202000000, 0, 3001, 3002, 3004, 30007, 3011 - ) - await self.data.item.put_hero_log( - user_id, - 105000010, - 1, - 0, - 209000000, - 0, - 10005, - 10002, - 10004, - 30006, - 10003, - ) - await self.data.item.put_hero_log( - user_id, 101000110, 1, 0, 201000000, 101000110, 2002, 2001, 2014, 0, 0 - ) - await self.data.item.put_hero_party( - user_id, 0, 101000010, 102000010, 105000010 - ) - await self.data.item.put_equipment_data(user_id, 201000000, 1, 200, 0, 0, 0) - await self.data.item.put_equipment_data(user_id, 202000000, 1, 200, 0, 0, 0) - await self.data.item.put_equipment_data(user_id, 209000000, 1, 200, 0, 0, 0) - await self.data.item.put_equipment_data(user_id, 101000110, 1, 200, 0, 0, 0) - await self.data.item.put_player_quest(user_id, 1001, True, 300, 0, 0, 1) + 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 = await self.data.profile.get_profile(user_id) - resp = SaoGetAuthCardDataResponse(header.cmd + 1, profile_data) + profile_data = self.game_data.profile.get_profile(user_id) + + resp = SaoGetAuthCardDataResponse(header.cmd +1, profile_data) return resp.make() - async def handle_c40c(self, header: SaoRequestHeader, request: bytes) -> bytes: - # home/check_ac_login_bonus - resp = SaoHomeCheckAcLoginBonusResponse(header.cmd + 1) + def handle_c40c(self, header: SaoRequestHeader, request: bytes) -> bytes: + #home/check_ac_login_bonus + resp = SaoHomeCheckAcLoginBonusResponse(header.cmd +1) return resp.make() - async def handle_c104(self, header: SaoRequestHeader, request: bytes) -> bytes: - # common/login + def handle_c104(self, header: SaoRequestHeader, request: bytes) -> bytes: + #common/login req = SaoCommonLoginRequest(header, request) - user_id = await self.data.card.get_user_id_from_card(req.access_code) - profile_data = await self.data.profile.get_profile(user_id) + 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) + resp = SaoCommonLoginResponse(header.cmd +1, profile_data) return resp.make() - async def handle_c404(self, header: SaoRequestHeader, request: bytes) -> bytes: - # home/check_comeback_event - resp = SaoCheckComebackEventRequest(header.cmd + 1) + def handle_c404(self, header: SaoRequestHeader, request: bytes) -> bytes: + #home/check_comeback_event + resp = SaoCheckComebackEventRequest(header.cmd +1) return resp.make() - async def handle_c000(self, header: SaoRequestHeader, request: bytes) -> bytes: - # ticket/ticket - resp = SaoTicketResponse(header.cmd + 1) + def handle_c000(self, header: SaoRequestHeader, request: bytes) -> bytes: + #ticket/ticket + resp = SaoTicketResponse(header.cmd +1) return resp.make() - async def handle_c500(self, header: SaoRequestHeader, request: bytes) -> bytes: - # user_info/get_user_basic_data + def handle_c500(self, header: SaoRequestHeader, request: bytes) -> bytes: + #user_info/get_user_basic_data req = SaoGetUserBasicDataRequest(header, request) - profile_data = await self.data.profile.get_profile(req.user_id) + profile_data = self.game_data.profile.get_profile(req.user_id) - resp = SaoGetUserBasicDataResponse(header.cmd + 1, profile_data) + resp = SaoGetUserBasicDataResponse(header.cmd +1, profile_data) return resp.make() - - async def handle_c600(self, header: SaoRequestHeader, request: bytes) -> bytes: - # have_object/get_hero_log_user_data_list + + def handle_c600(self, header: SaoRequestHeader, request: bytes) -> bytes: + #have_object/get_hero_log_user_data_list req = SaoGetHeroLogUserDataListRequest(header, request) - hero_data = await self.data.item.get_hero_logs(req.user_id) - - resp = SaoGetHeroLogUserDataListResponse(header.cmd + 1, hero_data) + hero_data = self.game_data.item.get_hero_logs(req.user_id) + + resp = SaoGetHeroLogUserDataListResponse(header.cmd +1, hero_data) return resp.make() - - async def handle_c602(self, header: SaoRequestHeader, request: bytes) -> bytes: - # have_object/get_equipment_user_data_list + + 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) - equipment_data = await self.data.item.get_user_equipments(req.user_id) - - resp = SaoGetEquipmentUserDataListResponse(header.cmd + 1, equipment_data) + resp = SaoGetEquipmentUserDataListResponse(header.cmd +1, equipment_data) return resp.make() - - async def handle_c604(self, header: SaoRequestHeader, request: bytes) -> bytes: - # have_object/get_item_user_data_list + + def handle_c604(self, header: SaoRequestHeader, request: bytes) -> bytes: + #have_object/get_item_user_data_list req = SaoGetItemUserDataListRequest(header, request) - item_data = await self.data.item.get_user_items(req.user_id) + item_data = self.game_data.item.get_user_items(req.user_id) - resp = SaoGetItemUserDataListResponse(header.cmd + 1, item_data) + resp = SaoGetItemUserDataListResponse(header.cmd +1, item_data) return resp.make() - - async def handle_c606(self, header: SaoRequestHeader, request: bytes) -> bytes: - # have_object/get_support_log_user_data_list - supportIdsData = await self.data.static.get_support_log_ids(0, True) - - resp = SaoGetSupportLogUserDataListResponse(header.cmd + 1, supportIdsData) + + 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() - - async def handle_c800(self, header: SaoRequestHeader, request: bytes) -> bytes: - # custom/get_title_user_data_list - titleIdsData = await self.data.static.get_title_ids(0, True) - - resp = SaoGetTitleUserDataListResponse(header.cmd + 1, titleIdsData) + + 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() - - async def handle_c608(self, header: SaoRequestHeader, request: bytes) -> bytes: - # have_object/get_episode_append_data_list + + def handle_c608(self, header: SaoRequestHeader, request: bytes) -> bytes: + #have_object/get_episode_append_data_list req = SaoGetEpisodeAppendDataListRequest(header, request) - profile_data = await self.data.profile.get_profile(req.user_id) + profile_data = self.game_data.profile.get_profile(req.user_id) - resp = SaoGetEpisodeAppendDataListResponse(header.cmd + 1, profile_data) + resp = SaoGetEpisodeAppendDataListResponse(header.cmd +1, profile_data) return resp.make() - async def handle_c804(self, header: SaoRequestHeader, request: bytes) -> bytes: - # custom/get_party_data_list + def handle_c804(self, header: SaoRequestHeader, request: bytes) -> bytes: + #custom/get_party_data_list req = SaoGetPartyDataListRequest(header, request) - hero_party = await self.data.item.get_hero_party(req.user_id, 0) - hero1_data = await self.data.item.get_hero_log(req.user_id, hero_party[3]) - hero2_data = await self.data.item.get_hero_log(req.user_id, hero_party[4]) - hero3_data = await self.data.item.get_hero_log(req.user_id, hero_party[5]) + 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 - ) + resp = SaoGetPartyDataListResponse(header.cmd +1, hero1_data, hero2_data, hero3_data) return resp.make() - async def handle_c902( - self, header: SaoRequestHeader, request: bytes - ) -> ( - bytes - ): # for whatever reason, having all entries empty or filled changes nothing - # quest/get_quest_scene_prev_scan_profile_card - resp = SaoGetQuestScenePrevScanProfileCardResponse(header.cmd + 1) + def handle_c902(self, header: SaoRequestHeader, request: bytes) -> bytes: # for whatever reason, having all entries empty or filled changes nothing + #quest/get_quest_scene_prev_scan_profile_card + resp = SaoGetQuestScenePrevScanProfileCardResponse(header.cmd +1) return resp.make() - async def handle_c124(self, header: SaoRequestHeader, request: bytes) -> bytes: - # common/get_resource_path_info - resp = SaoGetResourcePathInfoResponse(header.cmd + 1) + def handle_c124(self, header: SaoRequestHeader, request: bytes) -> bytes: + #common/get_resource_path_info + resp = SaoGetResourcePathInfoResponse(header.cmd +1) return resp.make() - async def handle_c900(self, header: SaoRequestHeader, request: bytes) -> bytes: - # quest/get_quest_scene_user_data_list // QuestScene.csv + def handle_c900(self, header: SaoRequestHeader, request: bytes) -> bytes: + #quest/get_quest_scene_user_data_list // QuestScene.csv req = SaoGetQuestSceneUserDataListRequest(header, request) - quest_data = await self.data.item.get_quest_logs(req.user_id) + quest_data = self.game_data.item.get_quest_logs(req.user_id) - resp = SaoGetQuestSceneUserDataListResponse(header.cmd + 1, quest_data) + resp = SaoGetQuestSceneUserDataListResponse(header.cmd +1, quest_data) return resp.make() - async def handle_c400(self, header: SaoRequestHeader, request: bytes) -> bytes: - # home/check_yui_medal_get_condition - resp = SaoCheckYuiMedalGetConditionResponse(header.cmd + 1) + def handle_c400(self, header: SaoRequestHeader, request: bytes) -> bytes: + #home/check_yui_medal_get_condition + resp = SaoCheckYuiMedalGetConditionResponse(header.cmd +1) return resp.make() - async def handle_c402(self, header: SaoRequestHeader, request: bytes) -> bytes: - # home/get_yui_medal_bonus_user_data - resp = SaoGetYuiMedalBonusUserDataResponse(header.cmd + 1) + def handle_c402(self, header: SaoRequestHeader, request: bytes) -> bytes: + #home/get_yui_medal_bonus_user_data + resp = SaoGetYuiMedalBonusUserDataResponse(header.cmd +1) return resp.make() - async def handle_c40a(self, header: SaoRequestHeader, request: bytes) -> bytes: - # home/check_profile_card_used_reward - resp = SaoCheckProfileCardUsedRewardResponse(header.cmd + 1) + def handle_c40a(self, header: SaoRequestHeader, request: bytes) -> bytes: + #home/check_profile_card_used_reward + resp = SaoCheckProfileCardUsedRewardResponse(header.cmd +1) return resp.make() - async def handle_c814(self, header: SaoRequestHeader, request: bytes) -> bytes: - # custom/synthesize_enhancement_hero_log + def handle_c814(self, header: SaoRequestHeader, request: bytes) -> bytes: + #custom/synthesize_enhancement_hero_log req = SaoSynthesizeEnhancementHeroLogRequest(header, request) - synthesize_hero_log_data = await self.data.item.get_hero_log( - req.user_id, req.origin_user_hero_log_id - ) + 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 = await self.data.static.get_item_id(x.user_common_reward_id) - heroList = await self.data.static.get_hero_id(x.user_common_reward_id) - equipmentList = await self.data.static.get_equipment_id( - x.user_common_reward_id - ) + 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"]) - await self.data.item.remove_item(req.user_id, x.user_common_reward_id) + self.game_data.item.remove_item(req.user_id, x.user_common_reward_id) if equipmentList: - equipment_data = await self.data.item.get_user_equipment( - req.user_id, x.user_common_reward_id - ) + 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}!" - ) + self.logger.error(f"Failed to find equipment {x.user_common_reward_id} for user {req.user_id}!") continue - - hero_exp = int(equipment_data["enhancement_exp"]) + int( - synthesize_hero_log_data["log_exp"] - ) - await self.data.item.remove_equipment( - req.user_id, x.user_common_reward_id - ) + + 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 = await self.data.item.get_hero_log( - req.user_id, x.user_common_reward_id - ) + 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}!" - ) + 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"] - ) - await self.data.item.remove_hero_log( - req.user_id, x.user_common_reward_id - ) - + + 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.logger.warn(f"Hero {x.user_common_reward_id} (type {x.common_reward_type}) not found!") - await self.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"], + 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 = await self.data.profile.get_profile(req.user_id) + profile = self.game_data.profile.get_profile(req.user_id) new_col = int(profile["own_col"]) - 100 # Update profile - - await self.data.profile.put_profile( + + self.game_data.profile.put_profile( req.user_id, - profile["user_type"], - profile["nick_name"], + 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"], + 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 = await self.data.item.get_hero_log( - req.user_id, req.origin_user_hero_log_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 - ) + resp = SaoSynthesizeEnhancementHeroLogResponse(header.cmd +1, synthesize_hero_log_data) return resp.make() - async def handle_c816(self, header: SaoRequestHeader, request: bytes) -> bytes: - # custom/synthesize_enhancement_equipment + def handle_c816(self, header: SaoRequestHeader, request: bytes) -> bytes: + #custom/synthesize_enhancement_equipment req_data = SaoSynthesizeEnhancementEquipmentRequest(header, request) - synthesize_equipment_data = await self.data.item.get_user_equipment( - req_data.user_id, req_data.origin_user_equipment_id - ) + 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 = await self.data.static.get_item_id(x.user_common_reward_id) - heroList = await self.data.static.get_hero_id(x.user_common_reward_id) - equipmentList = await self.data.static.get_equipment_id( - x.user_common_reward_id - ) + 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"]) - await self.data.item.remove_item( - req_data.user_id, x.user_common_reward_id - ) + self.game_data.item.remove_item(req_data.user_id, x.user_common_reward_id) if equipmentList: - equipment_data = await self.data.item.get_user_equipment( - req_data.user_id, x.user_common_reward_id - ) + 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}!" - ) + 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"] - ) - await self.data.item.remove_equipment( - req_data.user_id, x.user_common_reward_id - ) + 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 = await self.data.item.get_hero_log( - req_data.user_id, x.user_common_reward_id - ) + 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}!" - ) + self.logger.error(f"Failed to find hero {x.user_common_reward_id} for user {req_data.user_id}!") continue - equipment_exp = int(hero_data["log_exp"]) + int( - synthesize_equipment_data["enhancement_exp"] - ) - await self.data.item.remove_hero_log( - req_data.user_id, x.user_common_reward_id - ) + 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!" - ) + 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) - await self.data.item.put_equipment_data( - req_data.user_id, - int(req_data.origin_user_equipment_id), - synthesize_equipment_data["enhancement_value"], - equipment_exp, - 0, - 0, - 0, - ) - - profile = await self.data.profile.get_profile(req_data.user_id) + profile = self.game_data.profile.get_profile(req_data.user_id) new_col = int(profile["own_col"]) - 100 # Update profile - - await self.data.profile.put_profile( + + self.game_data.profile.put_profile( req_data.user_id, - profile["user_type"], - profile["nick_name"], + 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"], - ) + profile["own_vp"], + profile["own_yui_medal"], + profile["setting_title_id"] + ) - # Load the item again to push to the response handler - synthesize_equipment_data = await self.data.item.get_user_equipment( - req_data.user_id, req_data.origin_user_equipment_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 - ) + resp = SaoSynthesizeEnhancementEquipmentResponse(header.cmd +1, synthesize_equipment_data) return resp.make() - async def handle_c806(self, header: SaoRequestHeader, request: bytes) -> bytes: - # custom/change_party + 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 = await self.data.item.get_hero_log( - req_data.user_id, party_team.user_hero_log_id - ) + 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 @@ -521,7 +400,7 @@ class SaoBase: hero_level = hero_data["log_level"] hero_exp = hero_data["log_exp"] - await self.data.item.put_hero_log( + self.game_data.item.put_hero_log( req_data.user_id, party_team.user_hero_log_id, hero_level, @@ -532,74 +411,52 @@ class SaoBase: party_team.skill_slot2_skill_id, party_team.skill_slot3_skill_id, party_team.skill_slot4_skill_id, - party_team.skill_slot5_skill_id, + party_team.skill_slot5_skill_id ) party_hero_list.append(party_team.user_hero_log_id) - await self.data.item.put_hero_party( - req_data.user_id, - req_data.party_data_list[0].party_team_data_list[0].user_party_team_id, - party_hero_list[0], - party_hero_list[1], - party_hero_list[2], - ) + 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) + resp = SaoNoopResponse(header.cmd +1) return resp.make() - async def handle_c904(self, header: SaoRequestHeader, request: bytes) -> bytes: - # quest/episode_play_start + 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 = await self.data.profile.get_profile(user_id) + profile_data = self.game_data.profile.get_profile(user_id) - await self.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, - ) + 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) + resp = SaoEpisodePlayStartResponse(header.cmd +1, profile_data) return resp.make() - async def handle_c908( - self, header: SaoRequestHeader, request: bytes - ) -> bytes: # Level calculation missing for the profile and heroes - # quest/episode_play_end + 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 - ) + 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 - ) + 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 = await self.data.profile.get_profile(user_id) + 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 - ) + 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 @@ -615,89 +472,76 @@ class SaoBase: episode_id = int(episode_id) + int(stage_id) # Match episode_id with the questSceneId saved in the DB through sortNo - questId = await self.data.static.get_quests_id(episode_id) + questId = self.game_data.static.get_quests_id(episode_id) episode_id = questId[2] - await self.data.item.put_player_quest( - user_id, - episode_id, - quest_clear_flag, - clear_time, - combo_num, - total_damage, - concurrent_destroying_num, - ) + 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 - 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=",") + 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 + 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[i + 1][1]): + 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 < int(data[e + 1][1]): + + for e in range(0,len(data)): + if log_exp>=int(data[e][1]) and log_exp bytes: - # quest/trial_tower_play_start + def handle_c914(self, header: SaoRequestHeader, request: bytes) -> bytes: + #quest/trial_tower_play_start req_data = SaoTrialTowerPlayStartRequest(header, request) user_id = req_data.user_id floor_id = req_data.trial_tower_id - profile_data = await self.data.profile.get_profile(user_id) + profile_data = self.game_data.profile.get_profile(user_id) - await self.data.item.create_session( - user_id, - int(req_data.play_start_request_data[0].user_party_id), - req_data.trial_tower_id, - req_data.play_mode, - req_data.play_start_request_data[0].quest_drop_boost_apply_flag, - ) + 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) + resp = SaoEpisodePlayStartResponse(header.cmd +1, profile_data) return resp.make() - async def handle_c918(self, header: SaoRequestHeader, request: bytes) -> bytes: - # quest/trial_tower_play_end + 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 - ) + 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 - ) + 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: + if trial_tower_id == 9: next_tower_id = 10001 - elif trial_tower_id == 10: + elif trial_tower_id == 10: trial_tower_id = 10001 next_tower_id = 3011 - elif trial_tower_id == 19: + 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: + 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: + 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: + elif trial_tower_id == 49: next_tower_id = 10005 elif trial_tower_id == 50: trial_tower_id = 10005 @@ -872,99 +663,81 @@ class SaoBase: trial_tower_id = trial_tower_id + 3000 next_tower_id = trial_tower_id + 1 - await self.data.item.put_player_quest( - user_id, - trial_tower_id, - quest_clear_flag, - clear_time, - combo_num, - total_damage, - concurrent_destroying_num, - ) + 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 = await self.data.item.get_quest_log(user_id, next_tower_id) + checkQuest = self.game_data.item.get_quest_log(user_id, next_tower_id) if not checkQuest: if next_tower_id != 3101: - await self.data.item.put_player_quest( - user_id, next_tower_id, 0, 0, 0, 0, 0 - ) + self.game_data.item.put_player_quest(user_id, next_tower_id, 0, 0, 0, 0, 0) - # Update the profile - profile = await self.data.profile.get_profile(user_id) - - 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 - ) + # 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=",") + 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 + 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[i + 1][1]): + 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 < int(data[e + 1][1]): + + for e in range(0,len(data)): + if log_exp>=int(data[e][1]) and log_exp bytes: - # quest/episode_play_end_unanalyzed_log_fixed + def handle_c90a(self, header: SaoRequestHeader, request: bytes) -> bytes: + #quest/episode_play_end_unanalyzed_log_fixed req = SaoEpisodePlayEndUnanalyzedLogFixedRequest(header, request) - end_session_data = await self.data.item.get_end_session(req.user_id) + end_session_data = self.game_data.item.get_end_session(req.user_id) - resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse( - header.cmd + 1, end_session_data[4] - ) + resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(header.cmd +1, end_session_data[4]) return resp.make() - async def handle_c91a( - self, header: SaoRequestHeader, request: bytes - ) -> bytes: # handler is identical to the episode - # quest/trial_tower_play_end_unanalyzed_log_fixed + 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 = await self.data.item.get_end_session(req.user_id) + end_session_data = self.game_data.item.get_end_session(req.user_id) - resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse( - header.cmd + 1, end_session_data[4] - ) + resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(header.cmd +1, end_session_data[4]) return resp.make() - async def handle_cd00(self, header: SaoRequestHeader, request: bytes) -> bytes: - # defrag_match/get_defrag_match_basic_data - resp = SaoGetDefragMatchBasicDataResponse(header.cmd + 1) + def handle_cd00(self, header: SaoRequestHeader, request: bytes) -> bytes: + #defrag_match/get_defrag_match_basic_data + resp = SaoGetDefragMatchBasicDataResponse(header.cmd +1) return resp.make() - async def handle_cd02(self, header: SaoRequestHeader, request: bytes) -> bytes: - # defrag_match/get_defrag_match_ranking_user_data - resp = SaoGetDefragMatchRankingUserDataResponse(header.cmd + 1) + 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() - async def handle_cd04(self, header: SaoRequestHeader, request: bytes) -> bytes: - # defrag_match/get_defrag_match_league_point_ranking_list - resp = SaoGetDefragMatchLeaguePointRankingListResponse(header.cmd + 1) + 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() - async def handle_cd06(self, header: SaoRequestHeader, request: bytes) -> bytes: - # defrag_match/get_defrag_match_league_score_ranking_list - resp = SaoGetDefragMatchLeagueScoreRankingListResponse(header.cmd + 1) + 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() - async def handle_d404(self, header: SaoRequestHeader, request: bytes) -> bytes: - # other/bnid_serial_code_check - resp = SaoBnidSerialCodeCheckResponse(header.cmd + 1) + def handle_d404(self, header: SaoRequestHeader, request: bytes) -> bytes: + #other/bnid_serial_code_check + resp = SaoBnidSerialCodeCheckResponse(header.cmd +1) return resp.make() - async def handle_c306(self, header: SaoRequestHeader, request: bytes) -> bytes: - # card/scan_qr_quest_profile_card - resp = SaoScanQrQuestProfileCardResponse(header.cmd + 1) + def handle_c306(self, header: SaoRequestHeader, request: bytes) -> bytes: + #card/scan_qr_quest_profile_card + resp = SaoScanQrQuestProfileCardResponse(header.cmd +1) return resp.make() - - async def handle_c700(self, header: SaoRequestHeader, request: bytes) -> bytes: + + def handle_c700(self, header: SaoRequestHeader, request: bytes) -> bytes: # shop/get_shop_resource_sales_data_list # TODO: Get user shop data req = GetShopResourceSalesDataListRequest(header, request) resp = GetShopResourceSalesDataListResponse(header.cmd + 1) return resp.make() - - async def handle_d100(self, header: SaoRequestHeader, request: bytes) -> bytes: + + def handle_d100(self, header: SaoRequestHeader, request: bytes) -> bytes: # shop/get_yui_medal_shop_user_data_list # TODO: Get user shop data req = GetYuiMedalShopUserDataListRequest(header, request) resp = GetYuiMedalShopUserDataListResponse(header.cmd + 1) return resp.make() - - async def handle_cf0e(self, header: SaoRequestHeader, request: bytes) -> bytes: + + 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() - - async def handle_d5da(self, header: SaoRequestHeader, request: bytes) -> bytes: + + 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 - + 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() - - async def handle_d5dc(self, header: SaoRequestHeader, request: bytes) -> bytes: + + 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"]) - + 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() - - async def handle_d5fc(self, header: SaoRequestHeader, request: bytes) -> bytes: + + 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 - + 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() - - async def handle_d5fe(self, header: SaoRequestHeader, request: bytes) -> bytes: + + def handle_d5fe(self, header: SaoRequestHeader, request: bytes) -> bytes: # master_data/get_m_gasha_medal_shop_items return SaoNoopResponse(header.cmd + 1).make() - - async def handle_d604(self, header: SaoRequestHeader, request: bytes) -> bytes: + + 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 - + 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") + #self.logger.debug(f"Load {len(resp.data_list)} Res Earn Campaign Shops") return SaoNoopResponse(header.cmd + 1).make() - - async def handle_d606(self, header: SaoRequestHeader, request: bytes) -> bytes: + + def handle_d606(self, header: SaoRequestHeader, request: bytes) -> bytes: # master_data_2/get_m_res_earn_campaign_shop_items - return SaoNoopResponse(header.cmd + 1).make() - - async def handle_c108(self, header: SaoRequestHeader, request: bytes) -> bytes: - # common/logout_ticket_unpurchased - req = SaoLogoutTicketUnpurchasedRequest(header, request) - return SaoLogoutTicketUnpurchasedResponse(header.cmd + 1).make() - - async def handle_cb02(self, header: SaoRequestHeader, request: bytes) -> bytes: - # quest_ranking/get_quest_hierarchy_progress_degrees_ranking_list - req = SaoGetQuestHierarchyProgressDegreesRankingListRequest(header, request) - return SaoGetQuestHierarchyProgressDegreesRankingListResponse( - header.cmd + 1 - ).make() - - async def handle_cb04(self, header: SaoRequestHeader, request: bytes) -> bytes: - # quest_ranking/get_quest_popular_hero_log_ranking_list - req = SaoGetQuestPopularHeroLogRankingListRequest(header, request) - return SaoGetQuestPopularHeroLogRankingListResponse(header.cmd + 1).make() + return SaoNoopResponse(header.cmd + 1).make() \ No newline at end of file diff --git a/titles/sao/config.py b/titles/sao/config.py index 17f5e13..dfae3c0 100644 --- a/titles/sao/config.py +++ b/titles/sao/config.py @@ -35,40 +35,38 @@ class SaoServerConfig: 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( diff --git a/titles/sao/const.py b/titles/sao/const.py index 86c0e38..8bdea0f 100644 --- a/titles/sao/const.py +++ b/titles/sao/const.py @@ -1,21 +1,14 @@ +from enum import Enum + + class SaoConstants: GAME_CODE = "SDEW" - GAME_CDS = ["SAO1"] CONFIG_NAME = "sao.yaml" VER_SAO = 0 - VERSION_NAMES = "Sword Art Online Arcade" - - SERIAL_IDENT_SATALITE = 4 - SERIAL_IDENT_TERMINAL = 5 - - SERIAL_IDENT = [2825] - NETID_PREFIX = ["ABLN"] - SERIAL_REGIONS = [1] - SERIAL_ROLES = [3] - SERIAL_CAB_IDENTS = [SERIAL_IDENT_SATALITE, SERIAL_IDENT_TERMINAL] + VERSION_NAMES = ("Sword Art Online Arcade") @classmethod def game_ver_to_string(cls, ver: int): diff --git a/titles/sao/data/Equipment.csv b/titles/sao/data/Equipment.csv deleted file mode 100644 index a7fd263..0000000 --- a/titles/sao/data/Equipment.csv +++ /dev/null @@ -1,117 +0,0 @@ -EquipmentId,EquipmentType,WeaponTypeId,Name,Rarity,Prefab,Power,StrengthIncrement,SkillCondition,Property1PropertyId,Property1Value1,Property1Value2,Property2PropertyId,Property2Value1,Property2Value2,Property3PropertyId,Property3Value1,Property3Value2,Property4PropertyId,Property4Value1,Property4Value2,SalePrice,CompositionExp,AwakeningExp,FlavorText,CollectionDisplayStartDate,CollectionEmptyFrameDisplayFlag,DateVersionId, -101000000,0,1,"ディバイネーション",1,"Prefabs/Weapons/w00_0023_00",80,8,0,1,0,0,1,0,0,99999,0,0,99999,0,0,100,100,100,"多くの冒険者にとって基本とな
る装備。片手剣としては比較的軽
く、扱いやすい。","2019/01/01",True,"1", -101000001,0,1,"ユナイティウォークス",2,"Prefabs/Weapons/w00_0024_00",96,10,0,1,0,0,1,0,0,2,0,0,99999,0,0,200,200,100,"竜の姿を象った片手剣。稀代の名
鍛冶師が生み出した業物で、一突
きで竜の心臓を貫くといわれる。","2019/01/01",True,"1", -101000002,0,1,"ブラックプレート",3,"Prefabs/Weapons/w00_0013_00",112,11,0,204300,33,0,204800,5,0,2,0,0,99999,0,0,400,400,100,"まるで板のような外観の巨大な黒
い片手剣。厚く重いが、ゆえに破
壊力は大きい。","2019/01/01",True,"1", -101000003,0,1,"マクアフィテル",4,"Prefabs/Weapons/w00_0014_00",134,13,0,1,0,0,1,0,0,2,0,0,2,0,0,800,800,100,"黒い刀身の片手剣。刃が非常に鋭
いため、扱いには細心の注意が必
要。","2019/01/01",True,"1", -101000004,0,1,"エリュシデータ",5,"Prefabs/Weapons/w00_0011_00",144,14,0,1,0,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"漆黒の刀身の片手剣。その刀身は
重く、魔剣並みの威力を持つ。","2019/01/01",True,"1", -101000005,0,1,"スケイル・ブレード",2,"Prefabs/Weapons/w00_0027_00",106,11,0,1,0,0,2,0,0,2,0,0,99999,0,0,200,200,100,"硬い鱗片を繋ぎ合わせて作った片
手剣。敵を斬りつけた後に、ノコ
ギリ状の部分で挽き切るようにし
て使う。","2019/01/01",True,"1", -101000006,0,1,"カゲミツG4",3,"Prefabs/Weapons/w00_0021_00",106,11,13,204400,0,0,204900,0,0,2,0,0,99999,0,0,400,400,100,"白兵戦用光剣。近接時の戦闘力は
群を抜いているが、銃の世界にお
いてこれのみで生き抜くことは窮
状を極めるであろう。","2019/01/01",True,"1", -101000007,0,1,"スウィープセイバー",4,"Prefabs/Weapons/w00_0007_00",128,13,0,202500,25,0,1,0,0,2,0,0,2,0,0,800,800,100,"特徴的な刀身の片手剣。“一掃す
るもの”の異名を持つ。","2019/01/01",True,"1", -101000008,0,1,"シャルルマーニュ",4,"Prefabs/Weapons/w00_0020_00",128,13,0,1,0,0,1,0,0,2,0,0,2,0,0,800,800,100,"古き王の愛剣。その柄には王の聖
片が埋められているという。","2019/01/01",True,"1", -101000009,0,1,"ダークリパルサー",5,"Prefabs/Weapons/w00_0012_00",140,14,0,204000,5,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"ドラゴンの体内で精製されるとい
う特殊な金属から鍛えられた片手
剣。","2019/01/01",True,"1", -101000010,0,1,"エクスキャリバー",5,"Prefabs/Weapons/w00_0010_01",158,16,0,2,0,0,2,0,0,2,0,0,2,0,0,1600,1600,100,"その名を知らぬものはない伝説の
聖剣。剣に選ばれた王は全ての富
を得るといわれる。","2019/01/01",True,"1", -101000011,0,1,"青薔薇の剣",5,"Prefabs/Weapons/w00_0055_00",148,14,0,209900,30,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"最果ての山脈の洞窟で、白竜に
守られていたという神器。美しい
青薔薇を内に宿した《永久氷塊》
が変じたもの。","2019/01/01",True,"1", -101000012,0,1,"金木犀の剣",5,"Prefabs/Weapons/w00_0059_00",151,15,0,1,0,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"神が世界で最も初めに植えた
という金木犀を変じた神器。
宿す性質は《永劫不朽》。","2019/01/01",True,"1", -101000013,0,1,"夜空の剣",5,"Prefabs/Weapons/w00_0060_00",150,14,0,204200,5,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"ルーリッドの辺境に生えた巨木
《ギガスシダー》の頂点の最も
硬い部分を削り出して作った剣。
膨大なリソースが集結した神器。","2019/01/01",True,"1", -101000014,0,1,"オブシディアナ",5,"Prefabs/Weapons/w00_0014_00",147,15,0,1,0,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"刀身全てが黒曜石で作られた
世界で最も切れ味の鋭い剣。
その分脆いため、取り扱いには
高い技術を要する。","2019/01/01",True,"1", -101000015,0,1,"エターナル・ツリー",5,"Prefabs/Weapons/w00_0061_00",144,14,0,207100,0,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"翡翠で出来たツリーをかたどった
剣。聖夜の奇跡を永久に約束する
類まれな力を宿す。","2019/01/01",True,"1", -101000016,0,1,"ソード・オブ・ホグニ",5,"Prefabs/Weapons/w00_0062_00",148,14,0,207300,50,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"詩に出る伝説の片手剣。
生き血を浴びるまで、鞘には収ま
らないといわれる。","2020/06/09 7:00:00",True,"1", -101000017,0,1,"ヴァルトレーニス",5,"Prefabs/Weapons/w00_0007_00",144,14,0,202500,50,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"風を纏ったような若草色の剣。
妖精の加護を受けており、
暴風の如き鋭い切れ味を誇る。","2020/09/22 7:00:00",True,"1", -101000018,0,1,"ヴァーデュラス・アニマ",5,"Prefabs/Weapons/w00_0063_00",151,15,0,1,0,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"生きとし生けるものの魂を司る
地神の剣。そのひと振りで悪を滅
ぼし、人々に勇気を与えるとされ
る。","2020/07/14 7:00:00",True,"1", -101000019,0,1,"マサムネG4",5,"Prefabs/Weapons/w00_0021_00",143,14,13,204400,0,0,204900,0,0,2,0,0,2,0,0,1600,1600,100,"従来の光剣の出力を更に高めた
改良型。接触さえ出来れば、
その熱量で硬い鋼鉄も容易く切断
出来る。","2020/11/03 7:00:00",True,"1", -101000020,0,1,"冥界王の剣",5,"Prefabs/Weapons/w00_0064_00",1400,140,0,1,0,0,1,0,0,99999,0,0,99999,0,0,1600,1600,100,"冥界王ハーデスの所持していた剣がアイテム化したもの。","2030/01/01",True,"1", -102000000,0,3,"エペ・ラピエル",1,"Prefabs/Weapons/w02_0001_01",70,7,0,1,0,0,1,0,0,99999,0,0,99999,0,0,100,100,100,"もっとも原始的な形状を持つとい
われる刺突剣。","2019/01/01",True,"1", -102000001,0,3,"ミームング",2,"Prefabs/Weapons/w02_0009_00",84,8,0,1,0,0,1,0,0,2,0,0,99999,0,0,200,200,100,"刀身が細く軽い剣。扱いやすいた
め、幅広い層に愛用されている。","2019/01/01",True,"1", -102000002,0,3,"エペ・ド・マルブル",3,"Prefabs/Weapons/w02_0052_00",98,10,0,1,0,0,1,0,0,2,0,0,99999,0,0,400,400,100,"決闘用の細剣。装飾部分に特殊な
素材が使われており、使う者に
繁栄をもたらす力があるといわれ
る。","2019/01/01",True,"1", -102000003,0,3,"フラワリング・エペ",4,"Prefabs/Weapons/w02_0051_00",112,11,0,1,0,0,1,0,0,2,0,0,2,0,0,800,800,100,"鍔の部分に花の装飾が施されてい
るのが特徴的な細剣。その美しさ
とは裏腹に殺傷力は非常に高い。","2019/01/01",True,"1", -102000004,0,3,"アロンダイト",5,"Prefabs/Weapons/w02_0005_01",126,13,0,1,0,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"伝説の騎士が用いた剣。聖剣と双
璧を成す存在といわれる。","2019/01/01",True,"1", -102000005,0,3,"フロッティ",4,"Prefabs/Weapons/w02_0012_00",112,11,0,204000,5,0,1,0,0,2,0,0,2,0,0,800,800,100,"数々の英雄が手にとった名剣。古
い言葉で“突き刺すもの”の意味
を持つ。","2019/01/01",True,"1", -102000006,0,3,"ランベントライト",5,"Prefabs/Weapons/w02_0007_00",126,13,0,202400,25,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"技術の粋を集め鍛え上げられた薄
く眩い細剣。用いる様が揺れる光
と共に踊るように見えることから
この名が与えられた。","2019/01/01",True,"1", -102000007,0,3,"世界樹の枝",5,"Prefabs/Weapons/w09_0011_00",76,8,0,200500,100,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"世界樹から手折られた一枝。
末端の枝ながら生命の力に溢れて
おり、土に植えればたちまち根を
張り大樹を成す。","2019/01/01",True,"1", -102000008,0,3,"スノウマン・ステッキ",5,"Prefabs/Weapons/w02_0056_00",126,13,0,204300,50,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"雪だるまを模したキュートな
ステッキ。可愛らしい見た目とは
裏腹に、非常に鋭利で繊細。","2019/01/01",True,"1", -102000009,0,3,"ラディアント・ライト",5,"Prefabs/Weapons/w02_0057_00",126,13,0,1,0,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"大地をも操る神の御業を補助する
とされる、創世神の細剣。その刀
身は燦然とした輝きを放つ。","2020/07/14 7:00:00",True,"1", -103000000,0,4,"クロスダガー",1,"Prefabs/Weapons/w03_0001_01",40,4,0,1,0,0,1,0,0,99999,0,0,99999,0,0,100,100,100,"柄の部分に十字を見立てた装飾が
施された短剣。信心深い者が護身
用に携帯することが多い。","2019/01/01",True,"1", -103000001,0,4,"スティング・ダガー",2,"Prefabs/Weapons/w03_0005_00",48,5,0,1,0,0,1,0,0,2,0,0,99999,0,0,200,200,100,"鋭く硬い刃を持つ短剣。鎧の隙間
を突くことに長けている。","2019/01/01",True,"1", -103000002,0,4,"イントルーダー",3,"Prefabs/Weapons/w03_0004_01",56,6,0,1,0,0,1,0,0,2,0,0,99999,0,0,400,400,100,"闇に紛れる者が愛用する短剣。
これに傷つけられ、奪われる命は
後を絶たない。","2019/01/01",True,"1", -103000003,0,4,"ナーゲルリング",4,"Prefabs/Weapons/w03_0008_01",64,6,0,1,0,0,1,0,0,2,0,0,2,0,0,800,800,100,"大振りの刀身を持つ短剣。
古くは巨人の首を落とすために使
われた。","2019/01/01",True,"1", -103000004,0,4,"スクレープ",5,"Prefabs/Weapons/w03_0002_00",72,7,0,204100,0,0,204800,5,0,2,0,0,2,0,0,1600,1600,100,"錆びた刀身の鮮やかな赤が印象的
な短剣。血を吸うほど暗い赤に染
まるという。","2019/01/01",True,"1", -103000005,0,4,"ソード・ブレイカー",3,"Prefabs/Weapons/w03_0006_00",54,5,0,204500,25,0,1,0,0,2,0,0,99999,0,0,400,400,100,"相手の武器を無力化する為に造ら
れた短剣。得物を失った相手は次
の瞬間、全身を切り刻まれている
だろう。","2019/01/01",True,"1", -103000006,0,4,"イーボン・ダガー",4,"Prefabs/Weapons/w03_0001_00",67,7,0,204000,5,0,1,0,0,2,0,0,2,0,0,800,800,100,"漆黒の短剣。軽く頑強な素材で作
られているため、武具としての信
頼度も非常に高い。","2019/01/01",True,"1", -103000007,0,4,"ジャルディーノ",5,"Prefabs/Weapons/w03_0009_00",76,8,0,1,0,0,1,0,0,1,0,0,1,0,0,1600,1600,100,"神の庭の守人の短剣。園を侵す
穢れた者たちを退けるために用い
る。","2019/01/01",True,"1", -105000000,0,6,"ウチガタナ",1,"Prefabs/Weapons/w05_0002_00",140,14,0,1,0,0,1,0,0,99999,0,0,99999,0,0,100,100,100,"鋼から鍛え上げられた一般的な直
刀。","2019/01/01",True,"1", -105000001,0,6,"コテツ",2,"Prefabs/Weapons/w05_0001_00",168,17,0,1,0,0,1,0,0,2,0,0,99999,0,0,200,200,100,"その名を知らぬものはない名刀。
同名の刀が数多く存在する。","2019/01/01",True,"1", -105000002,0,6,"スイリュウケン",3,"Prefabs/Weapons/w05_0002_01",196,20,0,1,0,0,1,0,0,2,0,0,99999,0,0,400,400,100,"反りのない、直刀と呼ばれる刀。
かつては竜の加護を受けたものが
用いたという。","2019/01/01",True,"1", -105000003,0,6,"ドウタヌキ",4,"Prefabs/Weapons/w05_0001_01",224,22,0,204300,33,0,204800,5,0,2,0,0,2,0,0,800,800,100,"深奥に住まう一族が鍛えた名刀。
この一族が鍛えたものは、全てこ
の名で呼ばれる。","2019/01/01",True,"1", -105000004,0,6,"ムラマサ",5,"Prefabs/Weapons/w05_0005_00",277,28,0,202700,50,0,204700,1,0,2,0,0,2,0,0,1600,1600,100,"かつて名将の一族を次々と死に追
いやったとされる妖刀。その刃の
輝きに魅せられる者は後を絶たな
い。","2019/01/01",True,"1", -105000005,0,6,"ヒザマル",4,"Prefabs/Weapons/w05_0006_00",235,24,0,1,0,0,1,0,0,2,0,0,2,0,0,800,800,100,"咎人を罰するための刀。首を落と
した時、膝まで切れたという。","2019/01/01",True,"1", -105000006,0,6,"フツノミタマ",5,"Prefabs/Weapons/w05_0008_00",252,25,0,202600,33,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"東国を治めた神が用いたとされる
刀剣。あらゆる邪気と辛苦を払い
軍勢を勝利に導く。","2019/01/01",True,"1", -105000007,0,6,"アメノハバキリ",5,"Prefabs/Weapons/w05_0055_00",280,28,0,1,0,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"八つの頭と尾を持つ伝説の大蛇の
討伐に用いられたとされる、由緒
ある刀。","2019/01/01",True,"1", -107000000,0,8,"ティアーズ・クロー",1,"Prefabs/Weapons/w07_0002_00",60,6,0,1,0,0,1,0,0,99999,0,0,99999,0,0,100,100,100,"近接格闘戦に特化した鉤爪。
軽くて堅い特殊な金属で出来て
いる。","2019/01/01",True,"1", -107000001,0,8,"ドラゴンズ・ネイル",2,"Prefabs/Weapons/w07_0002_01",72,7,0,1,0,0,1,0,0,2,0,0,99999,0,0,200,200,100,"鉄よりも硬く鋭い、
竜の爪を削りだしたクロー。","2019/01/01",True,"1", -107000002,0,8,"ブラッディ・タロン",3,"Prefabs/Weapons/w07_0002_02",84,8,0,1,0,0,1,0,0,2,0,0,99999,0,0,400,400,100,"かつて激しい戦いの果てに、使用
者の血で染まってしまったという
逸話を持つ鉤爪。","2019/01/01",True,"1", -107000003,0,8,"ダーツラム",4,"Prefabs/Weapons/w07_0051_00",96,10,0,209900,25,0,1,0,0,2,0,0,2,0,0,800,800,100,"斬撃を得意としたクロー亜種。
力強く振るえば打撃も可能なため
拳闘士の武器として愛用された。","2019/01/01",True,"1", -107000004,0,8,"ウィズィー・ガジェット",5,"Prefabs/Weapons/w07_0055_00",108,11,0,205600,0,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"最先端技術を駆使して作成された
鉤爪。いかに効率よく敵を屠るか
を追及しつくされている。","2019/01/01",True,"1", -107000005,0,8,"ヴィルトカッツェ",4,"Prefabs/Weapons/w07_0008_00",101,10,0,1,0,0,1,0,0,2,0,0,2,0,0,800,800,100,"軽さを重視したつくりの鉄爪。
軽く扱いやすさに長けているので、
身軽さを求めるものに好まれる。","2019/01/01",True,"1", -107000006,0,8,"パオペエ",5,"Prefabs/Weapons/w07_0007_00",113,11,0,205500,30,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"構造、製法が不明な異国の武器。
愛らしい見た目に反し、強烈な威
力を秘めている。","2019/01/01",True,"1", -107000007,0,8,"キティ・パウ",5,"Prefabs/Weapons/w07_0007_01",108,11,0,207700,0,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"可愛らしい子猫の手。外見は
柔らかそうだが、毛皮の中には
獰猛な鋭い爪が隠されている。","2019/01/01",True,"1", -108000000,0,9,"バトル・アクス",1,"Prefabs/Weapons/w08_0001_00",180,18,0,1,0,0,1,0,0,99999,0,0,99999,0,0,100,100,100,"典型的な戦斧。斧は日常から戦ま
で、用途は多岐に渡る。","2019/01/01",True,"1", -108000001,0,9,"ボイド・アクス",2,"Prefabs/Weapons/w08_0002_00",216,22,0,1,0,0,1,0,0,2,0,0,99999,0,0,200,200,100,"刃の一部が欠けたような形の斧。
敵の首を真芯で捉えやすいよう、
この形状になった。","2019/01/01",True,"1", -108000002,0,9,"ゴアスパウト",3,"Prefabs/Weapons/w08_0001_01",252,25,0,1,0,0,1,0,0,2,0,0,99999,0,0,400,400,100,"無骨な戦斧。この斧が振るわれる
と、見事な血しぶきがあがること
から名づけられた。","2019/01/01",True,"1", -108000003,0,9,"エリミネイター",4,"Prefabs/Weapons/w08_0002_01",288,29,0,202500,25,0,1,0,0,2,0,0,2,0,0,800,800,100,"古来より処刑者が用いた斧。当た
れば相手を確実に死に至らしめる
ことができる。","2019/01/01",True,"1", -108000004,0,9,"ヴェンデッタ",5,"Prefabs/Weapons/w08_0005_00",330,33,0,1,0,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"別名“復讐者の斧”。かつて激し
い怒りをもった集団がこれを用い
て領主への復讐を成した。","2019/01/01",True,"1", -108000005,0,9,"ヘッド・リムーバー",4,"Prefabs/Weapons/w08_0003_00",288,29,0,202400,25,0,1,0,0,2,0,0,2,0,0,800,800,100,"農作業用だったものを大きくした
もの。刈り取るのは草本ではなく
敵の首である。","2019/01/01",True,"1", -108000006,0,9,"ナズ",5,"Prefabs/Weapons/w08_0006_00",324,32,0,205000,100,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"生涯を戦いの中ですごし、神話に
なった、ある男の斧。","2019/01/01",True,"1", -108000007,0,9,"ケラヴノス",5,"Prefabs/Weapons/w08_0007_00",340,34,0,1,0,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"神の放つ雷から形作られたとされ
る荘厳な両手斧。その名に恥じず
重量感のある両刃で繰り出される
攻撃は敵を余さず薙ぎ倒す。","2019/01/01",True,"1", -109000000,0,10,"プレーン・メイス",1,"Prefabs/Weapons/w09_0001_01",60,6,0,1,0,0,1,0,0,99999,0,0,99999,0,0,100,100,100,"敵の殴打に特化した武器。
特別な訓練を行わずとも扱え、
当てさえすれば大きな効果を得ら
れるため、需要が高い。","2019/01/01",True,"1", -109000001,0,10,"バルバロイ・クラブ",2,"Prefabs/Weapons/w09_0002_00",72,7,0,1,0,0,1,0,0,2,0,0,99999,0,0,200,200,100,"形状が単純なため量産に向いてお
り、数多く出回っている片手棍。","2019/01/01",True,"1", -109000002,0,10,"アーデント・スマッシャー",3,"Prefabs/Weapons/w09_0001_00",84,8,0,1,0,0,1,0,0,2,0,0,99999,0,0,400,400,100,"かつて剣闘士が用いていたもの。
重い一撃による決着は観客を沸か
せた。","2019/01/01",True,"1", -109000003,0,10,"ジェム・アンド・スパイク",4,"Prefabs/Weapons/w09_0004_00",96,10,0,204200,5,0,1,0,0,2,0,0,2,0,0,800,800,100,"形状もさることながら、宝石を埋
め込んでいることも特徴的な片手
棍。","2019/01/01",True,"1", -109000004,0,10,"ジャイアント・タスク",5,"Prefabs/Weapons/w09_0005_00",108,11,0,1,0,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"巨大な牙獣の牙から作り出された
ハンマー。非常に重く頑丈。","2019/01/01",True,"1", -109000005,0,10,"グリダ・レプリカント",4,"Prefabs/Weapons/w09_0010_00",98,10,0,204600,25,0,1,0,0,2,0,0,2,0,0,800,800,100,"神の武器から複製されたもの。
異なるのは素材のみで、形状は同
一。","2019/01/01",True,"1", -109000006,0,10,"グリダヴォル",5,"Prefabs/Weapons/w09_0010_01",110,11,0,204600,25,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"神が用いた武器。伝承によって、
杖であるとも言われているが、詳
細は不明。","2019/01/01",True,"1", -109000007,0,10,"ミョルニル",5,"Prefabs/Weapons/w09_0009_00",111,11,0,203700,10,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"“粉砕するもの”の名を持つ黄金
の金槌。威力は凄まじく、巨人た
ちはその存在と、近く訪れるであ
ろう己の死に震える。","2019/01/01",True,"1", -109000008,0,10,"ジングル・ヘッド",5,"Prefabs/Weapons/w09_0056_00",108,11,5,204100,0,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"軽やかな音の鳴る、大きなベルは
その音に反して重く容赦ない。
聖夜を乱す不届き者へ強烈な鉄槌
を下すのにもってこい。","2019/01/01",True,"1", -111000000,0,11,"ガーディアンズ・ボウ",1,"Prefabs/Weapons/w11_0006_00",80,8,0,1,0,0,1,0,0,99999,0,0,99999,0,0,100,100,100,"広く使われる守衛用の弓。しなや
かさと丈夫さを併せ持っており、
扱いやすい。","2019/01/01",True,"1", -111000001,0,11,"ラファーガ",2,"Prefabs/Weapons/w11_0002_00",96,10,0,1,0,0,1,0,0,2,0,0,99999,0,0,200,200,100,"戦闘用に殺傷力を高めた弓。ある
程度弓の扱いに熟達した者が用い
る。","2019/01/01",True,"1", -111000002,0,11,"リベリオン・ボウ",3,"Prefabs/Weapons/w11_0003_00",112,11,0,1,0,0,1,0,0,2,0,0,99999,0,0,400,400,100,"別名“逆賊の弓”。かつて革命を
成した一団がこの弓を用いた。","2019/01/01",True,"1", -111000003,0,11,"ユーダリル",4,"Prefabs/Weapons/w11_0004_00",128,13,0,1,0,0,1,0,0,2,0,0,2,0,0,800,800,100,"弓の神が生まれた地に育つ大樹よ
り作られた弓。美しい形状が目を
引く。","2019/01/01",True,"1", -111000004,0,11,"フェイルノート",5,"Prefabs/Weapons/w11_0005_00",144,14,0,1,0,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"別名“無駄なしの弓”。かつての
持ち主は、ただの一度も的を外し
たことがなかったという。","2019/01/01",True,"1", -111000005,0,11,"エウロス",4,"Prefabs/Weapons/w11_0000_01",128,13,0,1,0,0,1,0,0,2,0,0,2,0,0,800,800,100,"風の神の名を戴いた弓。この弓で
射た相手には、一陣の風と共に不
吉がもたらされるという。","2019/01/01",True,"1", -111000006,0,11,"アッキヌフォート",5,"Prefabs/Weapons/w11_0010_00",144,14,0,1,0,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"伝説の聖騎士が用いた豪弓。決し
て的を外さないと伝えられるが、
この弓を指すのか所持者の技術な
のかは定かではない。","2019/01/01",True,"1", -111000007,0,11,"アネモイ",5,"Prefabs/Weapons/w11_0000_01",144,14,0,210100,30,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"神話における風の神の総称。
その攻撃は四方八方より吹き寄せ
る疾風の如く、敵を追い詰めると
言われている。","2020/09/22 7:00:00",True,"1", -111000008,0,11,"アニヒレート・レイ",5,"Prefabs/Weapons/w11_0056_00",144,14,0,1,0,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"強大な殲滅力を有する、太陽神の
弓。降り注ぐ光矢が敵を殲滅する
様は、神罰がくだっているかのよ
う。","2020/07/14 7:00:00",True,"1", -115000000,0,14,"DragonFall",1,"Prefabs/Weapons/w15_0055_00",180,18,30,1,0,0,1,0,0,99999,0,0,99999,0,0,100,100,100,"軽量で扱いやすい
セミオートタイプのライフル。","2019/01/01",True,"1", -115000001,0,14,"MEBRカスタム",2,"Prefabs/Weapons/w15_0054_00",216,22,31,1,0,0,1,0,0,2,0,0,99999,0,0,200,200,100,"様々なカスタムが可能で
汎用性の高い人気の狙撃銃。","2019/01/01",True,"1", -115000002,0,14,"サンフロワ",3,"Prefabs/Weapons/w15_0056_00",252,25,32,1,0,0,1,0,0,2,0,0,99999,0,0,400,400,100,"高い精度を誇る狙撃銃。
攻撃力・命中精度などの
バランスの良い優秀なモデル。","2019/01/01",True,"1", -115000003,0,14,"DragonBreaker",4,"Prefabs/Weapons/w15_0055_01",288,29,33,1,0,0,1,0,0,2,0,0,2,0,0,800,800,100,"DragonFallの改良型。
より扱いやすくなっており、
広く愛用されている。","2019/01/01",True,"1", -115000004,0,14,"MEBRマニアック",5,"Prefabs/Weapons/w15_0054_01",324,32,34,1,0,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"MEBRカスタムに、よりマニアッ
クなカスタマイズを施した銃。
一部のプレイヤーの間で高値で
取引されているという。","2019/01/01",True,"1", -115000005,0,14,"AMRティアマト",5,"Prefabs/Weapons/w15_0051_00",340,34,35,1,0,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"ボルトアクション方式の大型対物
ライフル。非常に高い威力を誇る
強力な銃だが、その分とても重く
取り回しが難しい。","2019/01/01",True,"1", -112000000,1,12,"サークル・バックラー",1,"Prefabs/Weapons/w12_0001_00",50,5,0,305000,100,0,1,0,0,99999,0,0,99999,0,0,100,100,100,"標準的な、
扱いやすいサイズの盾。","2019/01/01",True,"1", -112000001,1,12,"メタルスミス・バックラー",2,"Prefabs/Weapons/w12_0007_00",60,6,0,305000,100,0,1,0,0,2,0,0,99999,0,0,200,200,100,"多少重いが防御に秀でた盾。
仕上がりの美しさに腕が問われる
ため、鍛冶師が己の技術の誇示に
用いることが多い。","2019/01/01",True,"1", -112000002,1,12,"サブシスタンス",3,"Prefabs/Weapons/w12_0001_01",70,7,0,305000,100,0,1,0,0,2,0,0,99999,0,0,400,400,100,"簡素な作りだが、必要な要素を吟
味された優秀な盾。","2019/01/01",True,"1", -112000003,1,12,"テラー・レジスタ",4,"Prefabs/Weapons/w12_0003_00",80,8,0,305000,100,0,305200,0,0,2,0,0,2,0,0,800,800,100,"使用者への無駄な精神負荷を取り
除くため、徹底した衝撃吸収を施
された盾。","2019/01/01",True,"1", -112000004,1,12,"スヴェル",5,"Prefabs/Weapons/w12_0004_00",90,9,0,305000,100,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"燃え盛る太陽の炎すら遮るといわ
れる伝説の盾。","2019/01/01",True,"1", -112000005,1,12,"スケイル・シールド",2,"Prefabs/Weapons/w12_0010_00",66,7,0,305000,100,0,2,0,0,2,0,0,99999,0,0,200,200,100,"硬い鱗片を繋ぎ合わせて作った
盾。軽いが丈夫で、鉄に劣らぬ防
御力を誇る。","2019/01/01",True,"1", -112000006,1,12,"サバイバー・シールド",4,"Prefabs/Weapons/w12_0002_00",76,8,0,305000,100,0,305300,5,0,2,0,0,2,0,0,800,800,100,"かつて四方を軍勢に囲まれた状態
から生還した、屈強な兵たちが愛
用していたという盾。","2019/01/01",True,"1", -112000007,1,12,"シールド・オブ・アイギス",5,"Prefabs/Weapons/w12_0006_01",94,9,0,305000,100,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"あらゆる邪悪を退けるといわれる
盾。鍛冶の神の手によって世に生
まれた。","2019/01/01",True,"1", -112000008,1,12,"マジカル・リース",5,"Prefabs/Weapons/w12_0055_00",94,9,0,305000,100,0,305400,0,0,2,0,0,2,0,0,1600,1600,100,"ヒイラギによる魔除け効果のある
リース型の盾。聖なる夜を祝福
する力が宿っている。","2019/01/01",True,"1", -112000009,1,12,"冥界王の盾",5,"Prefabs/Weapons/w12_0056_00",126,13,0,305010,100,0,311700,0,0,2,0,0,2,0,0,1600,1600,100,"冥界王ハーデスの所持していた盾
がアイテム化したもの。一説には
この盾が冥界王の本体とも言われ
る。","2021/07/27 7:00:00",True,"1", -120000000,1,13,"プレーン・リング",1,"Prefabs/Weapons/w12_0001_00",25,3,0,1,0,0,1,0,0,99999,0,0,99999,0,0,100,100,100,"簡易な素材でできた、一般的な指
輪。指輪には、身につけた者を守
る特殊な力が秘められている。","2019/01/01",True,"1", -120000001,1,13,"シルバー・リング",2,"Prefabs/Weapons/w12_0001_00",30,3,0,1,0,0,1,0,0,2,0,0,99999,0,0,200,200,100,"銀を素材とした指輪。その輝きは
月の光とも呼ばれ、神聖な力が宿
るという。","2019/01/01",True,"1", -120000002,1,13,"ゴールド・リング",3,"Prefabs/Weapons/w12_0001_00",35,4,0,1,0,0,1,0,0,2,0,0,99999,0,0,400,400,100,"純金と宝石でできた指輪。身につ
けたものに活力をもたらし、物事
を好転させる力がある。","2019/01/01",True,"1", -120000003,1,13,"ネックレス",4,"Prefabs/Weapons/w12_0001_00",40,4,0,1,0,0,1,0,0,2,0,0,2,0,0,800,800,100,"華美な装飾が施された首飾り。
作成した職人の魂が込められてお
り、特殊な力を発揮する。","2019/01/01",True,"1", -120000004,1,13,"ゴールド・ネックレス",5,"Prefabs/Weapons/w12_0001_00",45,5,0,1,0,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"純金のみを使用した非常に豪奢な
首飾り。人々を魅了する力を持つ
為、時の権力者達はこれを巡り幾
度も争ったという。","2019/01/01",True,"1", -120000005,1,13,"アミュレット",4,"Prefabs/Weapons/w12_0001_00",42,4,0,1,0,0,1,0,0,2,0,0,2,0,0,800,800,100,"退魔の力が込められた首飾り。
様々な困難から身を守ると言われ
ている。","2019/01/01",True,"1", -120000006,1,13,"タリスマン",5,"Prefabs/Weapons/w12_0001_00",47,5,0,1,0,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"特別な宝石で作られたお守り。
偉大な天の加護を秘めており、持
ち主に大いなる力を授ける。","2019/01/01",True,"1", -120000007,1,13,"アルゴの攻略本",5,"Prefabs/Weapons/w12_0001_00",35,4,28,1,0,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"アルゴが独自に調査した
Unknown内の情報を纏めた本。
フラグメントの位置が
記載されている。","2021/12/14 7:00:00",False,"1", -120000008,1,13,"樹枝六花のアンクレット",5,"Prefabs/Weapons/w12_0001_00",59,6,0,311200,0,0,1,0,0,2,0,0,2,0,0,1600,1600,100,"雪の結晶で作られた足飾り。
結晶自ら輝いており、ある夜に
願い事を三回唱えると叶うが、
輝きは失われてしまうという噂。","2021/12/14 7:00:00",True,"1", -201000000,0,1,"デフォルト片手剣",1,"Prefabs/Weapons/w00_0023_00",0,0,0,1,0,0,1,0,0,1,0,0,1,0,0,0,0,0,,"2030/01/01",False,"1", -202000000,0,3,"デフォルト細剣",1,"Prefabs/Weapons/w02_0001_01",0,0,0,1,0,0,1,0,0,1,0,0,1,0,0,0,0,0,,"2030/01/01",False,"1", -203000000,0,4,"デフォルト短剣",1,"Prefabs/Weapons/w03_0001_01",0,0,0,1,0,0,1,0,0,1,0,0,1,0,0,0,0,0,,"2030/01/01",False,"1", -205000000,0,6,"デフォルト刀",1,"Prefabs/Weapons/w05_0002_00",0,0,0,1,0,0,1,0,0,1,0,0,1,0,0,0,0,0,,"2030/01/01",False,"1", -207000000,0,8,"デフォルトクロー",1,"Prefabs/Weapons/w07_0002_00",0,0,0,1,0,0,1,0,0,1,0,0,1,0,0,0,0,0,,"2030/01/01",False,"1", -208000000,0,9,"デフォルト両手斧",1,"Prefabs/Weapons/w08_0001_00",0,0,0,1,0,0,1,0,0,1,0,0,1,0,0,0,0,0,,"2030/01/01",False,"1", -209000000,0,10,"デフォルト片手棍",1,"Prefabs/Weapons/w09_0001_00",0,0,0,1,0,0,1,0,0,1,0,0,1,0,0,0,0,0,,"2030/01/01",False,"1", -211000000,0,11,"デフォルト弓",1,"Prefabs/Weapons/w11_0006_00",0,0,0,1,0,0,1,0,0,1,0,0,1,0,0,0,0,0,,"2030/01/01",False,"1", -215000000,0,14,"デフォルト狙撃銃",1,"Prefabs/Weapons/w15_0055_00",0,0,30,1,0,0,1,0,0,1,0,0,1,0,0,0,0,0,,"2030/01/01",False,"1", -201000001,0,1,"意志力キリト左手デフォルト装備",1,"Prefabs/Weapons/w02_0007_00",126,13,8,1,0,0,1,0,0,1,0,0,1,0,0,0,0,0,,"2030/01/01",False,"1", diff --git a/titles/sao/data/HeroLog.csv b/titles/sao/data/HeroLog.csv deleted file mode 100644 index 6fbacac..0000000 --- a/titles/sao/data/HeroLog.csv +++ /dev/null @@ -1,485 +0,0 @@ -HeroLogId,CharaId,Name,Nickname,Rarity,WeaponTypeId,HeroLogRoleId,CostumeTypeId,UnitId,DefaultEquipmentId1,DefaultEquipmentId2,SkillTableSubId,HpMin,HpMax,StrMin,StrMax,VitMin,VitMax,IntMin,IntMax,Property1PropertyId,Property1Value1,Property1Value2,Property2PropertyId,Property2Value1,Property2Value2,Property3PropertyId,Property3Value1,Property3Value2,Property4PropertyId,Property4Value1,Property4Value2,FlavorText,SalePrice,CompositionExp,AwakeningExp,Slot4UnlockLevel,Slot5UnlockLevel,CutinImage,CutinImageAwake,CutinUpperSideText,CharaCommentImage,CharaCommentImageAwake,QuestStartIntroduce,QuestStartIntroduceAwake,QuestCharaIcon,QuestCharaIconAwake,QuestCharaIconLoss,QuestCharaIconAwakeLoss,CollectionDisplayStartDate,CollectionEmptyFrameDisplayFlag,DateVersionId, -101000010,1,"キリト","黒き妖精",1,1,4,0,101000010,201000000,-1,101000010,1000,100800,120,12096,50,5040,100,10080,1,0,0,1,0,0,99999,0,0,99999,0,0,"ALO初ログイン時の記録。SAOの
データを変換したため、姿は異な
るが各能力値はSAOから引き継が
れている。",100,1000,100,10,20,"hlog_101000010_kir_1_01_oha","hlog_101000010_kir_1_01_oha",False,"Chara_comment/101000010","Chara_comment/101000010","101000010_cha","101000010_cha","Chara_icon/101000010","Chara_icon/101000010","Chara_icon_loss/101000010","Chara_icon_loss/101000010","2019/01/01",True,"1", -101000020,1,"キリト","新生の黒き剣士",2,6,4,0,101000020,205000000,-1,101000020,1200,120960,144,14514,60,6050,120,12100,1,0,0,1,0,0,2,0,0,99999,0,0,"新生ALOにおける平時の記録。
逆立った髪はユイの要望により
下ろされた。",200,2000,100,10,20,"hlog_101000020_kir_2_01_kat","hlog_101000020_kir_2_01_kat",False,"Chara_comment/101000020","Chara_comment/101000020","101000020_cha","101000020_cha","Chara_icon/101000020","Chara_icon/101000020","Chara_icon_loss/101000020","Chara_icon_loss/101000020","2019/01/01",True,"1", -101000030,1,"キリト","空舞う双牙",2,2,4,0,101000030,201000000,201000000,101000030,1200,120960,144,14514,60,6050,120,12100,1,0,0,1,0,0,2,0,0,99999,0,0,"新生ALOにおける平時の記録。
逆立った髪はユイの要望により
下ろされた。",200,2000,100,10,20,"hlog_101000030_kir_2_01_nit","hlog_101000030_kir_2_01_nit",False,"Chara_comment/101000030","Chara_comment/101000030","101000030_cha","101000030_cha","Chara_icon/101000030","Chara_icon/101000030","Chara_icon_loss/101000030","Chara_icon_loss/101000030","2019/01/01",True,"1", -101000040,1,"キリト","神技の黒影",3,2,4,0,101000040,201000000,201000000,101000040,1400,141120,168,16932,70,7055,140,14110,1,0,0,1,0,0,2,0,0,99999,0,0,"SAOにおいて攻略組として最前線
に立っていた頃の記録。過酷な環
境下において彼はソロプレイヤー
であることを選び続けた。",400,6000,100,10,20,"hlog_101000040_kir_3_01_dag","hlog_101000040_kir_3_01_dag",False,"Chara_comment/101000040","Chara_comment/101000040","101000040_cha","101000040_cha","Chara_icon/101000040","Chara_icon/101000040","Chara_icon_loss/101000040","Chara_icon_loss/101000040","2019/01/01",True,"1", -101000050,1,"キリト","銃世界の黒き剣士",3,1,1,0,101000050,201000000,-1,101000050,1120,112900,204,20322,55,5645,140,14110,1,0,0,1,0,0,2,0,0,99999,0,0,"女性に見える容姿だが歴とした男
性素体。彼は銃の世界にあっても
従来の戦闘スタイルを選択した。",400,6000,100,10,20,"hlog_101000050_kir_3_01_oha","hlog_101000050_kir_3_01_oha",False,"Chara_comment/101000050","Chara_comment/101000050","101000050_cha","101000050_cha","Chara_icon/101000050","Chara_icon/101000050","Chara_icon_loss/101000050","Chara_icon_loss/101000050","2019/01/01",True,"1", -101000060,1,"キリト","漆黒の旋風",3,1,4,0,101000060,201000000,-1,101000060,1400,141120,168,16932,70,7055,140,14110,1,0,0,1,0,0,2,0,0,99999,0,0,"ALO初ログイン時の記録。SAOの
データを変換したため、姿は異な
るが各能力値はSAOから引き継が
れている。",400,6000,100,10,20,"hlog_101000060_kir_3_01_oha","hlog_101000060_kir_3_01_oha",False,"Chara_comment/101000060","Chara_comment/101000060","101000060_cha","101000060_cha","Chara_icon/101000060","Chara_icon/101000060","Chara_icon_loss/101000060","Chara_icon_loss/101000060","2019/01/01",True,"1", -101000070,1,"キリト","神速の刃",3,4,1,0,101000070,203000000,-1,101000070,1120,112900,204,20322,55,5645,140,14110,1,0,0,1,0,0,2,0,0,99999,0,0,"新生ALOにおける平時の記録。
逆立った髪はユイの要望により
下ろされた。",400,6000,100,10,20,"hlog_101000070_kir_3_01_nit","hlog_101000070_kir_3_01_nit",False,"Chara_comment/101000070","Chara_comment/101000070","101000070_cha","101000070_cha","Chara_icon/101000070","Chara_icon/101000070","Chara_icon_loss/101000070","Chara_icon_loss/101000070","2019/01/01",True,"1", -101000080,1,"キリト","黒の剣士",3,1,1,0,101000080,201000000,-1,101000080,1120,112900,204,20322,55,5645,140,14110,1,0,0,1,0,0,2,0,0,99999,0,0,"SAOにおいて攻略組として最前線
に立っていた頃の記録。過酷な環
境下において彼はソロプレイヤー
であることを選び続けた。",400,6000,100,10,20,"hlog_101000080_kir_3_01_oha","hlog_101000080_kir_3_01_oha",False,"Chara_comment/101000080","Chara_comment/101000080","101000080_cha","101000080_cha","Chara_icon/101000080","Chara_icon/101000080","Chara_icon_loss/101000080","Chara_icon_loss/101000080","2019/01/01",True,"1", -101000090,1,"キリト","光剣一閃",4,1,1,0,101000090,201000000,-1,101000090,1280,129020,228,23226,65,6450,160,16130,1,0,0,1,0,0,2,0,0,2,0,0,"一本の光剣で銃弾を斬り払い闘う
姿は第3回BoBにて広く知られ、
多くのプレイヤーに衝撃を与えた。",800,10000,100,10,20,"hlog_101000090_kir_4_01_oha","hlog_101000090_kir_4_01_oha",False,"Chara_comment/101000090","Chara_comment/101000090","101000090_cha","101000090_cha","Chara_icon/101000090","Chara_icon/101000090","Chara_icon_loss/101000090","Chara_icon_loss/101000090","2019/01/01",True,"1", -101000100,1,"キリト","二剣瞬撃",4,2,4,0,101000100,201000000,201000000,101000100,1600,161280,192,19356,80,8065,160,16130,1,0,0,1,0,0,2,0,0,2,0,0,"二刀流スキルが存在しないALOに
おいて二刀流を実現させたキリト
は、それぞれの剣でソードスキル
を発動させるまでに至った。",800,10000,100,10,20,"hlog_101000100_kir_4_01_nit","hlog_101000100_kir_4_01_nit",False,"Chara_comment/101000100","Chara_comment/101000100","101000100_cha","101000100_cha","Chara_icon/101000100","Chara_icon/101000100","Chara_icon_loss/101000100","Chara_icon_loss/101000100","2019/01/01",True,"1", -101000110,1,"キリト","団結と鼓舞",4,2,4,0,101000110,201000000,201000000,101000110,1600,161280,192,19356,80,8065,160,16130,1,0,0,1,0,0,2,0,0,2,0,0,"Unknownにて記録された一枚。こ
のログの存在は、過去はもとより
今この瞬間もUnknownに我々が記
録されていることを意味する。",800,10000,100,10,20,"hlog_101000110_kir_4_01_oha","hlog_101000110_kir_4_01_oha",False,"Chara_comment/101000110","Chara_comment/101000110","101000110_cha","101000110_cha","Chara_icon/101000110","Chara_icon/101000110","Chara_icon_loss/101000110","Chara_icon_loss/101000110","2019/01/01",True,"1", -101000120,1,"キリト","心頼の誓い",5,1,4,0,101000120,201000000,-1,101000120,1800,181440,216,21774,90,9070,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"数多の闘いを経て【心頼】できる
仲間を得た黒の剣士。その表情に
かつての影は見えない。",1600,20000,100,10,20,"hlog_101000120_kir_5_01_oha","hlog_101000121_kir_5_01_oha",False,"Chara_comment/101000120","Chara_comment/101000121","101000120_cha","101000121_cha","Chara_icon/101000120","Chara_icon/101000121","Chara_icon_loss/101000120","Chara_icon_loss/101000121","2019/01/01",True,"1", -101000130,1,"キリト","神域の双剣使い",5,2,1,0,101000130,201000000,201000000,101000130,1440,145150,258,26130,70,7260,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"ユニークスキル《二刀流》。SAO
で当スキルを保持することは、
SAO内最高の反応速度を持つプレ
イヤーであることを表している。",1600,20000,100,10,20,"hlog_101000130_kir_5_01_nit","hlog_101000131_kir_5_01_nit",False,"Chara_comment/101000130","Chara_comment/101000131","101000130_cha","101000131_cha","Chara_icon/101000130","Chara_icon/101000131","Chara_icon_loss/101000130","Chara_icon_loss/101000131","2019/01/01",True,"1", -101000140,1,"キリト","静穏の笑み",4,2,4,0,101000140,201000000,201000000,101000140,1600,161280,192,19356,80,8065,160,16130,1,0,0,1,0,0,2,0,0,2,0,0,"ALOにて記録された一枚。激しい
闘いを終えた後、心を許した仲間
だけに見せる、黒の剣士の束の間
の表情。",800,10000,100,10,20,"hlog_101000140_kir_4_01_nit","hlog_101000140_kir_4_01_nit",False,"Chara_comment/101000140","Chara_comment/101000140","101000140_cha","101000140_cha","Chara_icon/101000140","Chara_icon/101000140","Chara_icon_loss/101000140","Chara_icon_loss/101000140","2019/01/01",True,"1", -102000010,2,"アスナ","煌輝の光影",1,3,3,0,102000010,202000000,-1,102000010,810,81650,96,9678,40,4080,140,14520,1,0,0,1,0,0,99999,0,0,99999,0,0,"SAOにおいての記録。その容姿と
卓越した剣の腕から、彼女の名は
ギルド内のみならず多くのプレイ
ヤーに知られている。",100,1000,100,10,20,"hlog_102000010_asu_1_01_rap","hlog_102000010_asu_1_01_rap",False,"Chara_comment/102000010","Chara_comment/102000010","102000010_cha","102000010_cha","Chara_icon/102000010","Chara_icon/102000010","Chara_icon_loss/102000010","Chara_icon_loss/102000010","2019/01/01",True,"1", -102000020,2,"アスナ","ひだまりの笑顔",2,10,4,0,102000020,209000000,-1,102000020,1080,108860,144,14514,55,5445,140,14520,1,0,0,1,0,0,2,0,0,99999,0,0,"SAOにおいての記録。その容姿と
卓越した剣の腕から、彼女の名は
ギルド内のみならず多くのプレイ
ヤーに知られている。",200,2000,100,10,20,"hlog_102000020_asu_2_01_mac","hlog_102000020_asu_2_01_mac",False,"Chara_comment/102000020","Chara_comment/102000020","102000020_cha","102000020_cha","Chara_icon/102000020","Chara_icon/102000020","Chara_icon_loss/102000020","Chara_icon_loss/102000020","2019/01/01",True,"1", -102000030,2,"アスナ","流水の如き剣",2,3,1,0,102000030,202000000,-1,102000030,860,87090,174,17418,45,4355,140,14520,1,0,0,1,0,0,2,0,0,99999,0,0,"新生ALOにおける平時の記録。SA
Oでの過酷な日々を共に乗り越え
てきた仲間は彼女にとってかけが
えのないものとなった。",200,2000,100,10,20,"hlog_102000030_asu_2_01_rap","hlog_102000030_asu_2_01_rap",False,"Chara_comment/102000030","Chara_comment/102000030","102000030_cha","102000030_cha","Chara_icon/102000030","Chara_icon/102000030","Chara_icon_loss/102000030","Chara_icon_loss/102000030","2019/01/01",True,"1", -102000040,2,"アスナ","煌きの蒼き妖精",2,3,3,0,102000040,202000000,-1,102000040,970,97980,114,11610,50,4900,170,17420,1,0,0,1,0,0,2,0,0,99999,0,0,"新生ALOにおける平時の記録。SA
Oでの過酷な日々を共に乗り越え
てきた仲間は彼女にとってかけが
えのないものとなった。",200,2000,100,10,20,"hlog_102000040_asu_2_01_rap","hlog_102000040_asu_2_01_rap",False,"Chara_comment/102000040","Chara_comment/102000040","102000040_cha","102000040_cha","Chara_icon/102000040","Chara_icon/102000040","Chara_icon_loss/102000040","Chara_icon_loss/102000040","2019/01/01",True,"1", -102000050,2,"アスナ","水閃の煌星",3,11,3,0,102000050,211000000,-1,102000050,1130,114310,132,13548,55,5715,200,20320,1,0,0,1,0,0,2,0,0,99999,0,0,"新生ALOにおける平時の記録。SA
Oでの過酷な日々を共に乗り越え
てきた仲間は彼女にとってかけが
えのないものとなった。",400,6000,100,10,20,"hlog_102000050_asu_3_01_bow","hlog_102000050_asu_3_01_bow",False,"Chara_comment/102000050","Chara_comment/102000050","102000050_cha","102000050_cha","Chara_icon/102000050","Chara_icon/102000050","Chara_icon_loss/102000050","Chara_icon_loss/102000050","2019/01/01",True,"1", -102000060,2,"アスナ","流光の剣戟",3,3,3,0,102000060,202000000,-1,102000060,1130,114310,132,13548,55,5715,200,20320,1,0,0,1,0,0,2,0,0,99999,0,0,"新生ALOにおける平時の記録。SA
Oでの過酷な日々を共に乗り越え
てきた仲間は彼女にとってかけが
えのないものとなった。",400,6000,100,10,20,"hlog_102000060_asu_3_01_rap","hlog_102000060_asu_3_01_rap",False,"Chara_comment/102000060","Chara_comment/102000060","102000060_cha","102000060_cha","Chara_icon/102000060","Chara_icon/102000060","Chara_icon_loss/102000060","Chara_icon_loss/102000060","2019/01/01",True,"1", -102000070,2,"アスナ","烈煌の剣閃",3,3,1,0,102000070,202000000,-1,102000070,1010,101610,204,20322,50,5080,170,16930,1,0,0,1,0,0,2,0,0,99999,0,0,"SAOにおいての記録。その容姿と
卓越した剣の腕から、彼女の名は
ギルド内のみならず多くのプレイ
ヤーに知られている。",400,6000,100,10,20,"hlog_102000070_asu_3_01_rap","hlog_102000070_asu_3_01_rap",False,"Chara_comment/102000070","Chara_comment/102000070","102000070_cha","102000070_cha","Chara_icon/102000070","Chara_icon/102000070","Chara_icon_loss/102000070","Chara_icon_loss/102000070","2019/01/01",True,"1", -102000080,2,"アスナ","一条の閃光",3,3,4,0,102000080,202000000,-1,102000080,1260,127010,168,16932,65,6350,170,16930,1,0,0,1,0,0,2,0,0,99999,0,0,"SAOにおいての記録。その容姿と
卓越した剣の腕から、彼女の名は
ギルド内のみならず多くのプレイ
ヤーに知られている。",400,6000,100,10,20,"hlog_102000080_asu_3_01_rap","hlog_102000080_asu_3_01_rap",False,"Chara_comment/102000080","Chara_comment/102000080","102000080_cha","102000080_cha","Chara_icon/102000080","Chara_icon/102000080","Chara_icon_loss/102000080","Chara_icon_loss/102000080","2019/01/01",True,"1", -102000090,2,"アスナ","慈愛の閃き",4,1,4,0,102000090,201000000,-1,102000090,1440,145150,192,19356,70,7260,190,19350,1,0,0,1,0,0,2,0,0,2,0,0,"強さと優しさを併せ持つ彼女の存
在感は、ある意味ではギルドマス
ター以上であったと推測される。",800,10000,100,10,20,"hlog_102000090_asu_4_01_oha","hlog_102000090_asu_4_01_oha",False,"Chara_comment/102000090","Chara_comment/102000090","102000090_cha","102000090_cha","Chara_icon/102000090","Chara_icon/102000090","Chara_icon_loss/102000090","Chara_icon_loss/102000090","2019/01/01",True,"1", -102000100,2,"アスナ","麗しき笑み",4,3,4,0,102000100,202000000,-1,102000100,1440,145150,192,19356,70,7260,190,19350,1,0,0,1,0,0,2,0,0,2,0,0,"ALOで過ごす平穏な日々と仲間た
ちは、彼女に本来の明るさを取り
戻させた。",800,10000,100,10,20,"hlog_102000100_asu_4_01_rap","hlog_102000100_asu_4_01_rap",False,"Chara_comment/102000100","Chara_comment/102000100","102000100_cha","102000100_cha","Chara_icon/102000100","Chara_icon/102000100","Chara_icon_loss/102000100","Chara_icon_loss/102000100","2019/01/01",True,"1", -102000110,2,"アスナ","まどろみの隣人",4,3,1,0,102000110,202000000,-1,102000110,1150,116120,228,23226,60,5805,190,19350,1,0,0,1,0,0,2,0,0,2,0,0,"Unknownにて記録された一枚。
仲間と過ごす穏やかな時間。仮想
と現実の違いは、彼女にとっては
意味をなさない。",800,10000,100,10,20,"hlog_102000110_asu_4_01_rap","hlog_102000110_asu_4_01_rap",False,"Chara_comment/102000110","Chara_comment/102000110","102000110_cha","102000110_cha","Chara_icon/102000110","Chara_icon/102000110","Chara_icon_loss/102000110","Chara_icon_loss/102000110","2019/01/01",True,"1", -102000120,2,"アスナ","流麗なる剣士",5,3,3,0,102000120,202000000,-1,102000120,1460,146970,174,17418,75,7350,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"ウンディーネは回復役を得意とす
る種族だが、最前線にて時折見せ
る鋭く激しい剣技はSAOのそれを
想起させる。",1600,20000,100,10,20,"hlog_102000120_asu_5_01_rap","hlog_102000121_asu_5_01_rap",False,"Chara_comment/102000120","Chara_comment/102000121","102000120_cha","102000121_cha","Chara_icon/102000120","Chara_icon/102000121","Chara_icon_loss/102000120","Chara_icon_loss/102000121","2019/01/01",True,"1", -102000130,2,"アスナ","閃耀の刺突",5,3,1,0,102000130,202000000,-1,102000130,1300,130640,258,26130,65,6530,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"SAOにて比類なきスピードと精確
な技を併せ持つ彼女はやがて
《閃光》の二つ名で呼ばれること
となった。",1600,20000,100,10,20,"hlog_102000130_asu_5_01_rap","hlog_102000131_asu_5_01_rap",False,"Chara_comment/102000130","Chara_comment/102000131","102000130_cha","102000131_cha","Chara_icon/102000130","Chara_icon/102000131","Chara_icon_loss/102000130","Chara_icon_loss/102000131","2019/01/01",True,"1", -102000140,2,"アスナ","情愛のまなざし",4,3,4,0,102000140,202000000,-1,102000140,1440,145150,192,19356,70,7260,190,19350,1,0,0,1,0,0,2,0,0,2,0,0,"ALOにて記録された一枚。愛情に
満ちたまなざし。たとえ仮想世界
にあっても、その感情は真実のも
のなのだろう。",800,10000,100,10,20,"hlog_102000140_asu_4_01_rap","hlog_102000140_asu_4_01_rap",False,"Chara_comment/102000140","Chara_comment/102000140","102000140_cha","102000140_cha","Chara_icon/102000140","Chara_icon/102000140","Chara_icon_loss/102000140","Chara_icon_loss/102000140","2019/01/01",True,"1", -103000010,3,"リーファ","翡翠の剣士",1,1,4,0,103000010,201000000,-1,103000010,1000,100800,120,12096,40,4030,120,12100,1,0,0,1,0,0,99999,0,0,99999,0,0,"ALO古参である彼女は、ALO最大
の特色である《フライト・システ
ム》を活用した戦闘において、卓
越した技能をもつ。",100,1000,100,10,20,"hlog_103000010_lea_1_01_oha","hlog_103000010_lea_1_01_oha",False,"Chara_comment/103000010","Chara_comment/103000010","103000010_cha","103000010_cha","Chara_icon/103000010","Chara_icon/103000010","Chara_icon_loss/103000010","Chara_icon_loss/103000010","2019/01/01",True,"1", -103000020,3,"リーファ","翠緑の剣影",2,10,4,0,103000020,209000000,-1,103000020,1200,120960,144,14514,50,4840,140,14520,1,0,0,1,0,0,2,0,0,99999,0,0,"ALOアバターを流用した特殊なア
クセスを敢行したため、彼女はSA
Oにあっても現実に即したアバタ
ーを持たなかった。",200,2000,100,10,20,"hlog_103000020_lea_2_01_mac","hlog_103000020_lea_2_01_mac",False,"Chara_comment/103000020","Chara_comment/103000020","103000020_cha","103000020_cha","Chara_icon/103000020","Chara_icon/103000020","Chara_icon_loss/103000020","Chara_icon_loss/103000020","2019/01/01",True,"1", -103000030,3,"リーファ","疾風の妖精",2,1,4,0,103000030,201000000,-1,103000030,1200,120960,144,14514,50,4840,140,14520,1,0,0,1,0,0,2,0,0,99999,0,0,"ALO古参である彼女は、ALO最大
の特色である《フライト・システ
ム》を活用した戦闘において、卓
越した技能をもつ。",200,2000,100,10,20,"hlog_103000030_lea_2_01_oha","hlog_103000030_lea_2_01_oha",False,"Chara_comment/103000030","Chara_comment/103000030","103000030_cha","103000030_cha","Chara_icon/103000030","Chara_icon/103000030","Chara_icon_loss/103000030","Chara_icon_loss/103000030","2019/01/01",True,"1", -103000040,3,"リーファ","颯の刃",2,1,1,0,103000040,201000000,-1,103000040,960,96770,174,17418,40,3870,140,14520,1,0,0,1,0,0,2,0,0,99999,0,0,"ALOアバターを流用した特殊なア
クセスを敢行したため、彼女はSA
Oにあっても現実に即したアバタ
ーを持たなかった。",200,2000,100,10,20,"hlog_103000040_lea_2_01_oha","hlog_103000040_lea_2_01_oha",False,"Chara_comment/103000040","Chara_comment/103000040","103000040_cha","103000040_cha","Chara_icon/103000040","Chara_icon/103000040","Chara_icon_loss/103000040","Chara_icon_loss/103000040","2019/01/01",True,"1", -103000050,3,"リーファ","春花の風剣",3,3,4,0,103000050,202000000,-1,103000050,1400,141120,168,16932,55,5645,170,16930,1,0,0,1,0,0,2,0,0,99999,0,0,"ALOアバターを流用した特殊なア
クセスを敢行したため、彼女はSA
Oにあっても現実に即したアバタ
ーを持たなかった。",400,6000,100,10,20,"hlog_103000050_lea_3_01_rap","hlog_103000050_lea_3_01_rap",False,"Chara_comment/103000050","Chara_comment/103000050","103000050_cha","103000050_cha","Chara_icon/103000050","Chara_icon/103000050","Chara_icon_loss/103000050","Chara_icon_loss/103000050","2019/01/01",True,"1", -103000060,3,"リーファ","迅速の嵐舞",3,1,4,0,103000060,201000000,-1,103000060,1400,141120,168,16932,55,5645,170,16930,1,0,0,1,0,0,2,0,0,99999,0,0,"ALOアバターを流用した特殊なア
クセスを敢行したため、彼女はSA
Oにあっても現実に即したアバタ
ーを持たなかった。",400,6000,100,10,20,"hlog_103000060_lea_3_01_oha","hlog_103000060_lea_3_01_oha",False,"Chara_comment/103000060","Chara_comment/103000060","103000060_cha","103000060_cha","Chara_icon/103000060","Chara_icon/103000060","Chara_icon_loss/103000060","Chara_icon_loss/103000060","2019/01/01",True,"1", -103000070,3,"リーファ","旋閃の疾刃",3,1,3,0,103000070,201000000,-1,103000070,1260,127010,132,13548,50,5080,200,20320,1,0,0,1,0,0,2,0,0,99999,0,0,"ALO古参である彼女は、ALO最大
の特色である《フライト・システ
ム》を活用した戦闘において、卓
越した技能をもつ。",400,6000,100,10,20,"hlog_103000070_lea_3_01_oha","hlog_103000070_lea_3_01_oha",False,"Chara_comment/103000070","Chara_comment/103000070","103000070_cha","103000070_cha","Chara_icon/103000070","Chara_icon/103000070","Chara_icon_loss/103000070","Chara_icon_loss/103000070","2019/01/01",True,"1", -103000080,3,"リーファ","薫風の瞳",3,1,4,0,103000080,201000000,-1,103000080,1400,141120,168,16932,55,5645,170,16930,1,0,0,1,0,0,2,0,0,99999,0,0,"ALO古参である彼女は、ALO最大
の特色である《フライト・システ
ム》を活用した戦闘において、卓
越した技能をもつ。",400,6000,100,10,20,"hlog_103000080_lea_3_01_oha","hlog_103000080_lea_3_01_oha",False,"Chara_comment/103000080","Chara_comment/103000080","103000080_cha","103000080_cha","Chara_icon/103000080","Chara_icon/103000080","Chara_icon_loss/103000080","Chara_icon_loss/103000080","2019/01/01",True,"1", -103000090,3,"リーファ","親密な微笑",4,6,3,0,103000090,205000000,-1,103000090,1440,145150,156,15480,60,5805,230,23220,1,0,0,1,0,0,2,0,0,2,0,0,"快活で愛想がよい彼女は自然と人
の心を緩ませる魅力を纏ってい
る。これはその記録の中の一枚。",800,10000,100,10,20,"hlog_103000090_lea_4_01_kat","hlog_103000090_lea_4_01_kat",True,"Chara_comment/103000090","Chara_comment/103000090","103000090_cha","103000090_cha","Chara_icon/103000090","Chara_icon/103000090","Chara_icon_loss/103000090","Chara_icon_loss/103000090","2019/01/01",True,"1", -103000100,3,"リーファ","清爽なる風",4,1,1,0,103000100,201000000,-1,103000100,1280,129020,228,23226,50,5160,190,19350,1,0,0,1,0,0,2,0,0,2,0,0,"剣道で培った腕と反応速度から
《シルフ五傑》の一人に数えられ
る程の技量を誇る。",800,10000,100,10,20,"hlog_103000100_lea_4_01_oha","hlog_103000100_lea_4_01_oha",False,"Chara_comment/103000100","Chara_comment/103000100","103000100_cha","103000100_cha","Chara_icon/103000100","Chara_icon/103000100","Chara_icon_loss/103000100","Chara_icon_loss/103000100","2019/01/01",True,"1", -103000110,3,"リーファ","爽風の笑み",5,1,3,0,103000110,201000000,-1,103000110,1620,163300,174,17418,65,6530,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"屈託の無い表情に反し、飛行速度
への飽くなき追及の姿勢は、周囲
をして彼女をスピードホリックと
呼ばせしめた。",1600,20000,100,10,20,"hlog_103000110_lea_5_01_oha","hlog_103000111_lea_5_01_oha",False,"Chara_comment/103000110","Chara_comment/103000111","103000110_cha","103000111_cha","Chara_icon/103000110","Chara_icon/103000111","Chara_icon_loss/103000110","Chara_icon_loss/103000111","2019/01/01",True,"1", -103000120,3,"リーファ","疾駆する剣閃",5,1,4,0,103000120,201000000,-1,103000120,1800,181440,216,21774,70,7260,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"SAOは魔法が存在しない剣の世界
だが、剣道を学ぶ彼女にとってそ
れはさほどの問題ではなかった。",1600,20000,100,10,20,"hlog_103000120_lea_5_01_oha","hlog_103000121_lea_5_01_oha",False,"Chara_comment/103000120","Chara_comment/103000121","103000120_cha","103000121_cha","Chara_icon/103000120","Chara_icon/103000121","Chara_icon_loss/103000120","Chara_icon_loss/103000121","2019/01/01",True,"1", -103000130,3,"リーファ","親愛の絆",4,1,4,0,103000130,201000000,-1,103000130,1600,161280,192,19356,65,6450,190,19350,1,0,0,1,0,0,2,0,0,2,0,0,"ALOにて記録された一枚。
特別の信頼を寄せたものだけに見
せる笑顔。",800,10000,100,10,20,"hlog_103000130_lea_4_01_oha","hlog_103000130_lea_4_01_oha",False,"Chara_comment/103000130","Chara_comment/103000130","103000130_cha","103000130_cha","Chara_icon/103000130","Chara_icon/103000130","Chara_icon_loss/103000130","Chara_icon_loss/103000130","2019/01/01",True,"1", -104000010,4,"シノン","必撃の妖精",1,11,4,0,104000010,211000000,-1,104000010,800,80640,132,13308,40,4030,110,11090,1,0,0,1,0,0,99999,0,0,99999,0,0,"弓の戦士シノン。扱う者が少数で
ある弓を用いること、またその腕
前においても、彼女は非凡な存在
感を放つ。",100,1000,100,10,20,"hlog_104000010_sin_1_01_bow","hlog_104000010_sin_1_01_bow",False,"Chara_comment/104000010","Chara_comment/104000010","104000010_cha","104000010_cha","Chara_icon/104000010","Chara_icon/104000010","Chara_icon_loss/104000010","Chara_icon_loss/104000010","2019/01/01",True,"1", -104000020,4,"シノン","信頼の微笑",2,3,1,0,104000020,202000000,-1,104000020,770,77410,192,19158,40,3870,130,13310,1,0,0,1,0,0,2,0,0,99999,0,0,"SAOでは珍しい弓使い。75層以
下ではアクセス履歴が無いという
こともあり、謎の多いアバターで
ある。",200,2000,100,10,20,"hlog_104000020_sin_2_01_rap","hlog_104000020_sin_2_01_rap",False,"Chara_comment/104000020","Chara_comment/104000020","104000020_cha","104000020_cha","Chara_icon/104000020","Chara_icon/104000020","Chara_icon_loss/104000020","Chara_icon_loss/104000020","2019/01/01",True,"1", -104000030,4,"シノン","飛躍の弓撃",2,11,4,0,104000030,211000000,-1,104000030,960,96770,156,15966,50,4840,130,13310,1,0,0,1,0,0,2,0,0,99999,0,0,"SAOでは珍しい弓使い。75層以
下ではアクセス履歴が無いという
こともあり、謎の多いアバターで
ある。",200,2000,100,10,20,"hlog_104000030_sin_2_01_bow","hlog_104000030_sin_2_01_bow",False,"Chara_comment/104000030","Chara_comment/104000030","104000030_cha","104000030_cha","Chara_icon/104000030","Chara_icon/104000030","Chara_icon_loss/104000030","Chara_icon_loss/104000030","2019/01/01",True,"1", -104000040,4,"シノン","氷麗の華弓",2,11,1,0,104000040,211000000,-1,104000040,770,77410,192,19158,40,3870,130,13310,1,0,0,1,0,0,2,0,0,99999,0,0,"弓の戦士シノン。扱う者が少数で
ある弓を用いること、またその腕
前においても、彼女は非凡な存在
感を放つ。",200,2000,100,10,20,"hlog_104000040_sin_2_01_bow","hlog_104000040_sin_2_01_bow",False,"Chara_comment/104000040","Chara_comment/104000040","104000040_cha","104000040_cha","Chara_icon/104000040","Chara_icon/104000040","Chara_icon_loss/104000040","Chara_icon_loss/104000040","2019/01/01",True,"1", -104000050,4,"シノン","呼応する絆",3,10,1,0,104000050,209000000,-1,104000050,900,90320,222,22356,45,4515,150,15520,1,0,0,1,0,0,2,0,0,99999,0,0,"SAOでは珍しい弓使い。75層以
下ではアクセス履歴が無いという
こともあり、謎の多いアバターで
ある。",400,6000,100,10,20,"hlog_104000050_sin_3_01_mac","hlog_104000050_sin_3_01_mac",False,"Chara_comment/104000050","Chara_comment/104000050","104000050_cha","104000050_cha","Chara_icon/104000050","Chara_icon/104000050","Chara_icon_loss/104000050","Chara_icon_loss/104000050","2019/01/01",True,"1", -104000060,4,"シノン","必中の撃弓",3,11,4,0,104000060,211000000,-1,104000060,1120,112900,186,18630,55,5645,150,15520,1,0,0,1,0,0,2,0,0,99999,0,0,"SAOでは珍しい弓使い。75層以
下ではアクセス履歴が無いという
こともあり、謎の多いアバターで
ある。",400,6000,100,10,20,"hlog_104000060_sin_3_01_bow","hlog_104000060_sin_3_01_bow",False,"Chara_comment/104000060","Chara_comment/104000060","104000060_cha","104000060_cha","Chara_icon/104000060","Chara_icon/104000060","Chara_icon_loss/104000060","Chara_icon_loss/104000060","2019/01/01",True,"1", -104000070,4,"シノン","凜閃の華弓",3,11,3,0,104000070,211000000,-1,104000070,1010,101610,150,14904,50,5080,180,18630,1,0,0,1,0,0,2,0,0,99999,0,0,"弓の戦士シノン。扱う者が少数で
ある弓を用いること、またその腕
前においても、彼女は非凡な存在
感を放つ。",400,6000,100,10,20,"hlog_104000070_sin_3_01_bow","hlog_104000070_sin_3_01_bow",False,"Chara_comment/104000070","Chara_comment/104000070","104000070_cha","104000070_cha","Chara_icon/104000070","Chara_icon/104000070","Chara_icon_loss/104000070","Chara_icon_loss/104000070","2019/01/01",True,"1", -104000080,4,"シノン","対敵を射貫く者",3,11,1,0,104000080,211000000,-1,104000080,900,90320,222,22356,45,4515,150,15520,1,0,0,1,0,0,2,0,0,99999,0,0,"弓の戦士シノン。扱う者が少数で
ある弓を用いること、またその腕
前においても、彼女は非凡な存在
感を放つ。",400,6000,100,10,20,"hlog_104000080_sin_3_01_bow","hlog_104000080_sin_3_01_bow",False,"Chara_comment/104000080","Chara_comment/104000080","104000080_cha","104000080_cha","Chara_icon/104000080","Chara_icon/104000080","Chara_icon_loss/104000080","Chara_icon_loss/104000080","2019/01/01",True,"1", -104000090,4,"シノン","凍志のまなざし",4,4,4,0,104000090,203000000,-1,104000090,1280,129020,210,21288,65,6450,180,17740,1,0,0,1,0,0,2,0,0,2,0,0,"彼女の技術は弓の扱いに留まらな
い。その真価は研ぎ澄まされた集
中力にあると言えよう。",800,10000,100,10,20,"hlog_104000090_sin_4_01_dag","hlog_104000090_sin_4_01_dag",False,"Chara_comment/104000090","Chara_comment/104000090","104000090_cha","104000090_cha","Chara_icon/104000090","Chara_icon/104000090","Chara_icon_loss/104000090","Chara_icon_loss/104000090","2019/01/01",True,"1", -104000100,4,"シノン","必滅の一矢",4,11,1,0,104000100,211000000,-1,104000100,1020,103220,252,25548,50,5160,180,17740,1,0,0,1,0,0,2,0,0,2,0,0,"尋常の射程からの正確な射撃は
彼女の技量のほんの一端にすぎな
い。超長距離からの狙撃こそ、真
価が発揮される。",800,10000,100,10,20,"hlog_104000100_sin_4_01_bow","hlog_104000100_sin_4_01_bow",False,"Chara_comment/104000100","Chara_comment/104000100","104000100_cha","104000100_cha","Chara_icon/104000100","Chara_icon/104000100","Chara_icon_loss/104000100","Chara_icon_loss/104000100","2019/01/01",True,"1", -104000110,4,"シノン","戦士の安息",4,11,4,0,104000110,211000000,-1,104000110,1280,129020,210,21288,65,6450,180,17740,1,0,0,1,0,0,2,0,0,2,0,0,"SAOにおける記録のひとつ。彼女
のSAOにおける記録はその活躍に
反して非常に限定的で、75層以下
での記録が確認されていない。",800,10000,100,10,20,"hlog_104000110_sin_4_01_bow","hlog_104000110_sin_4_01_bow",False,"Chara_comment/104000110","Chara_comment/104000110","104000110_cha","104000110_cha","Chara_icon/104000110","Chara_icon/104000110","Chara_icon_loss/104000110","Chara_icon_loss/104000110","2019/01/01",True,"1", -104000120,4,"シノン","怜悧なる狙撃手",5,11,1,0,104000120,211000000,-1,104000120,1150,116120,288,28740,60,5805,200,19960,1,0,0,1,0,0,2,0,0,2,0,0,"遥か遠方より冷静に標的を見定め
射貫く。こと弓の扱いにおいて、
彼女に比肩するプレイヤーは未だ
確認されていない。",1600,20000,100,10,20,"hlog_104000120_sin_5_01_bow","hlog_104000121_sin_5_01_bow",False,"Chara_comment/104000120","Chara_comment/104000121","104000120_cha","104000121_cha","Chara_icon/104000120","Chara_icon/104000121","Chara_icon_loss/104000120","Chara_icon_loss/104000121","2019/01/01",True,"1", -104000130,4,"シノン","傍らの休息",4,11,4,0,104000130,211000000,-1,104000130,1280,129020,210,21288,65,6450,180,17740,1,0,0,1,0,0,2,0,0,2,0,0,"ALOにて記録された一枚。たとえ
ゲームでも闘いは精神を研ぎ澄ま
せるもの。休息は重要な要素であ
り、彼女も例外ではない。",800,10000,100,10,20,"hlog_104000130_sin_4_01_bow","hlog_104000130_sin_4_01_bow",False,"Chara_comment/104000130","Chara_comment/104000130","104000130_cha","104000130_cha","Chara_icon/104000130","Chara_icon/104000130","Chara_icon_loss/104000130","Chara_icon_loss/104000130","2019/01/01",True,"1", -105000010,5,"リズベット","技巧の鍛鎚",1,10,4,0,105000010,209000000,-1,105000010,1000,100800,108,10884,60,6050,100,10080,1,0,0,1,0,0,99999,0,0,99999,0,0,"SAOにおけるリズベットの鍛冶師
としての評価は極めて高い。勇名
を馳せたプレイヤーが多く訪れて
いたことからもそれが窺える。",100,1000,100,10,20,"hlog_105000010_lis_1_01_mac","hlog_105000010_lis_1_01_mac",False,"Chara_comment/105000010","Chara_comment/105000010","105000010_cha","105000010_cha","Chara_icon/105000010","Chara_icon/105000010","Chara_icon_loss/105000010","Chara_icon_loss/105000010","2019/01/01",True,"1", -105000020,5,"リズベット","喜色満面",2,4,2,0,105000020,203000000,-1,105000020,1260,127010,102,10452,85,8345,100,9680,1,0,0,1,0,0,2,0,0,99999,0,0,"SAOにおけるリズベットの鍛冶師
としての評価は極めて高い。勇名
を馳せたプレイヤーが多く訪れて
いたことからもそれが窺える。",200,2000,100,10,20,"hlog_105000020_lis_2_01_dag","hlog_105000020_lis_2_01_dag",False,"Chara_comment/105000020","Chara_comment/105000020","105000020_cha","105000020_cha","Chara_icon/105000020","Chara_icon/105000020","Chara_icon_loss/105000020","Chara_icon_loss/105000020","2019/01/01",True,"1", -105000030,5,"リズベット","鍛花の砕鎚",2,10,2,0,105000030,209000000,-1,105000030,1260,127010,102,10452,85,8345,100,9680,1,0,0,1,0,0,2,0,0,99999,0,0,"SAOと同じくALOにおいても彼女
は鍛冶職を営むが、仲間と共に戦
線に立つことも多くみられる。",200,2000,100,10,20,"hlog_105000030_lis_2_01_mac","hlog_105000030_lis_2_01_mac",False,"Chara_comment/105000030","Chara_comment/105000030","105000030_cha","105000030_cha","Chara_icon/105000030","Chara_icon/105000030","Chara_icon_loss/105000030","Chara_icon_loss/105000030","2019/01/01",True,"1", -105000040,5,"リズベット","飛翼の技工士",2,10,3,0,105000040,209000000,-1,105000040,1080,108860,102,10452,65,6530,140,14520,1,0,0,1,0,0,2,0,0,99999,0,0,"SAOと同じくALOにおいても彼女
は鍛冶職を営むが、仲間と共に戦
線に立つことも多くみられる。",200,2000,100,10,20,"hlog_105000040_lis_2_01_mac","hlog_105000040_lis_2_01_mac",False,"Chara_comment/105000040","Chara_comment/105000040","105000040_cha","105000040_cha","Chara_icon/105000040","Chara_icon/105000040","Chara_icon_loss/105000040","Chara_icon_loss/105000040","2019/01/01",True,"1", -105000050,5,"リズベット","熱情の覇気",3,3,2,0,105000050,202000000,-1,105000050,1470,148180,120,12192,95,9735,110,11290,1,0,0,1,0,0,2,0,0,99999,0,0,"SAOにおけるリズベットの鍛冶師
としての評価は極めて高い。勇名
を馳せたプレイヤーが多く訪れて
いたことからもそれが窺える。",400,6000,100,10,20,"hlog_105000050_lis_3_01_rap","hlog_105000050_lis_3_01_rap",False,"Chara_comment/105000050","Chara_comment/105000050","105000050_cha","105000050_cha","Chara_icon/105000050","Chara_icon/105000050","Chara_icon_loss/105000050","Chara_icon_loss/105000050","2019/01/01",True,"1", -105000060,5,"リズベット","闘輝の錬鎚",3,10,2,0,105000060,209000000,-1,105000060,1470,148180,120,12192,95,9735,110,11290,1,0,0,1,0,0,2,0,0,99999,0,0,"SAOと同じくALOにおいても彼女
は鍛冶職を営むが、仲間と共に戦
線に立つことも多くみられる。",400,6000,100,10,20,"hlog_105000060_lis_3_01_mac","hlog_105000060_lis_3_01_mac",False,"Chara_comment/105000060","Chara_comment/105000060","105000060_cha","105000060_cha","Chara_icon/105000060","Chara_icon/105000060","Chara_icon_loss/105000060","Chara_icon_loss/105000060","2019/01/01",True,"1", -105000070,5,"リズベット","華麗な奉仕",3,10,3,0,105000070,209000000,-1,105000070,1260,127010,120,12192,75,7620,170,16930,1,0,0,1,0,0,2,0,0,99999,0,0,"SAOと同じくALOにおいても彼女
は鍛冶職を営むが、仲間と共に戦
線に立つことも多くみられる。",400,6000,100,10,20,"hlog_105000070_lis_3_01_mac","hlog_105000070_lis_3_01_mac",False,"Chara_comment/105000070","Chara_comment/105000070","105000070_cha","105000070_cha","Chara_icon/105000070","Chara_icon/105000070","Chara_icon_loss/105000070","Chara_icon_loss/105000070","2019/01/01",True,"1", -105000080,5,"リズベット","征野の刀工",3,10,1,0,105000080,209000000,-1,105000080,1120,112900,180,18288,65,6775,140,14110,1,0,0,1,0,0,2,0,0,99999,0,0,"SAOにおけるリズベットの鍛冶師
としての評価は極めて高い。勇名
を馳せたプレイヤーが多く訪れて
いたことからもそれが窺える。",400,6000,100,10,20,"hlog_105000080_lis_3_01_mac","hlog_105000080_lis_3_01_mac",False,"Chara_comment/105000080","Chara_comment/105000080","105000080_cha","105000080_cha","Chara_icon/105000080","Chara_icon/105000080","Chara_icon_loss/105000080","Chara_icon_loss/105000080","2019/01/01",True,"1", -105000090,5,"リズベット","生彩の鍛冶師",4,9,4,0,105000090,208000000,-1,105000090,1600,161280,174,17418,95,9675,160,16130,1,0,0,1,0,0,2,0,0,2,0,0,"鍛冶で鍛えられたパラメータは武
具の扱いにも影響を及ぼすため、
彼女は細腕とは裏腹に重量のある
武具も扱える。",800,10000,100,10,20,"hlog_105000090_lis_4_01_axe","hlog_105000090_lis_4_01_axe",False,"Chara_comment/105000090","Chara_comment/105000090","105000090_cha","105000090_cha","Chara_icon/105000090","Chara_icon/105000090","Chara_icon_loss/105000090","Chara_icon_loss/105000090","2019/01/01",True,"1", -105000100,5,"リズベット","鎚戟の闘志",4,10,2,0,105000100,209000000,-1,105000100,1680,169340,138,13932,110,11130,130,12900,1,0,0,1,0,0,2,0,0,2,0,0,"SAO事件の当時、鍛冶師に専念し
ていた彼女が武具に身を包み自ら
戦線に立つことは稀だった。",800,10000,100,10,20,"hlog_105000100_lis_4_01_mac","hlog_105000100_lis_4_01_mac",False,"Chara_comment/105000100","Chara_comment/105000100","105000100_cha","105000100_cha","Chara_icon/105000100","Chara_icon/105000100","Chara_icon_loss/105000100","Chara_icon_loss/105000100","2019/01/01",True,"1", -105000110,5,"リズベット","穏やかな休日",4,10,1,0,105000110,209000000,-1,105000110,1280,129020,210,20904,75,7740,160,16130,1,0,0,1,0,0,2,0,0,2,0,0,"鍛冶師として名高い彼女の元には
様々なプレイヤーが訪れる。これ
は中でも親交の深い相手と過ごし
ていた時の記録である。",800,10000,100,10,20,"hlog_105000110_lis_4_01_mac","hlog_105000110_lis_4_01_mac",False,"Chara_comment/105000110","Chara_comment/105000110","105000110_cha","105000110_cha","Chara_icon/105000110","Chara_icon/105000110","Chara_icon_loss/105000110","Chara_icon_loss/105000110","2019/01/01",True,"1", -105000120,5,"リズベット","反撃の烽火",5,10,3,0,105000120,209000000,-1,105000120,1620,163300,156,15678,95,9800,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"希少素材獲得のため戦線に立つ姿
がALOでは多く確認される。それ
は彼女が心強い仲間を得たことに
も起因するのだろう。",1600,20000,100,10,20,"hlog_105000120_lis_5_01_mac","hlog_105000121_lis_5_01_mac",False,"Chara_comment/105000120","Chara_comment/105000121","105000120_cha","105000121_cha","Chara_icon/105000120","Chara_icon/105000121","Chara_icon_loss/105000120","Chara_icon_loss/105000121","2019/01/01",True,"1", -105000130,5,"リズベット","絆つなぐ笑顔",4,10,4,0,105000130,209000000,-1,105000130,1600,161280,174,17418,95,9675,160,16130,1,0,0,1,0,0,2,0,0,2,0,0,"ALOにて記録された一枚。SAO
時代からの仲間たちを交えたひと
時。彼女の日常は間違いなくここ
にも存在する。",800,10000,100,10,20,"hlog_105000130_lis_4_01_mac","hlog_105000130_lis_4_01_mac",False,"Chara_comment/105000130","Chara_comment/105000130","105000130_cha","105000130_cha","Chara_icon/105000130","Chara_icon/105000130","Chara_icon_loss/105000130","Chara_icon_loss/105000130","2019/01/01",True,"1", -106000010,6,"シリカ","決意の刃",1,4,4,0,106000010,203000000,-1,106000010,800,80640,108,10884,40,4030,120,12100,1,0,0,1,0,0,99999,0,0,99999,0,0,"SAOの中では年少に分類されるシ
リカ。幼い彼女が戦線に立ち、心
身無事に事件を生き延びたのは極
めて稀なケースである。",100,1000,100,10,20,"hlog_106000010_sil_1_01_dag","hlog_106000010_sil_1_01_dag",False,"Chara_comment/106000010","Chara_comment/106000010","106000010_cha","106000010_cha","Chara_icon/106000010","Chara_icon/106000010","Chara_icon_loss/106000010","Chara_icon_loss/106000010","2019/01/01",True,"1", -106000020,6,"シリカ","愛慕の竜星",2,9,3,0,106000020,208000000,-1,106000020,860,87090,102,10452,45,4355,170,17420,1,0,0,1,0,0,2,0,0,99999,0,0,"SAOの中では年少に分類されるシ
リカ。幼い彼女が戦線に立ち、心
身無事に事件を生き延びたのは極
めて稀なケースである。",200,2000,100,10,20,"hlog_106000020_sil_2_01_axe","hlog_106000020_sil_2_01_axe",False,"Chara_comment/106000020","Chara_comment/106000020","106000020_cha","106000020_cha","Chara_icon/106000020","Chara_icon/106000020","Chara_icon_loss/106000020","Chara_icon_loss/106000020","2019/01/01",True,"1", -106000030,6,"シリカ","竜伴の妖精",2,4,2,0,106000030,203000000,-1,106000030,1060,106440,102,10452,55,5320,120,11610,1,0,0,1,0,0,2,0,0,99999,0,0,"新生ALOにおける平時の記録。か
つての陰りは無く、傍らの使い魔
は変わらず彼女と共にある。",200,2000,100,10,20,"hlog_106000030_sil_2_01_dag","hlog_106000030_sil_2_01_dag",False,"Chara_comment/106000030","Chara_comment/106000030","106000030_cha","106000030_cha","Chara_icon/106000030","Chara_icon/106000030","Chara_icon_loss/106000030","Chara_icon_loss/106000030","2019/01/01",True,"1", -106000040,6,"シリカ","竜翔の戦姫",2,4,3,0,106000040,203000000,-1,106000040,860,87090,102,10452,45,4355,170,17420,1,0,0,1,0,0,2,0,0,99999,0,0,"SAOの中では年少に分類されるシ
リカ。幼い彼女が戦線に立ち、心
身無事に事件を生き延びたのは極
めて稀なケースである。",200,2000,100,10,20,"hlog_106000040_sil_2_01_dag","hlog_106000040_sil_2_01_dag",False,"Chara_comment/106000040","Chara_comment/106000040","106000040_cha","106000040_cha","Chara_icon/106000040","Chara_icon/106000040","Chara_icon_loss/106000040","Chara_icon_loss/106000040","2019/01/01",True,"1", -106000050,6,"シリカ","解放の笑顔",3,10,3,0,106000050,209000000,-1,106000050,1010,101610,120,12192,50,5080,200,20320,1,0,0,1,0,0,2,0,0,99999,0,0,"新生ALOにおける平時の記録。か
つての陰りは無く、傍らの使い魔
は変わらず彼女と共にある。",400,6000,100,10,20,"hlog_106000050_sil_3_01_mac","hlog_106000050_sil_3_01_mac",False,"Chara_comment/106000050","Chara_comment/106000050","106000050_cha","106000050_cha","Chara_icon/106000050","Chara_icon/106000050","Chara_icon_loss/106000050","Chara_icon_loss/106000050","2019/01/01",True,"1", -106000060,6,"シリカ","竜花の剣爪",3,4,3,0,106000060,203000000,-1,106000060,1010,101610,120,12192,50,5080,200,20320,1,0,0,1,0,0,2,0,0,99999,0,0,"新生ALOにおける平時の記録。か
つての陰りは無く、傍らの使い魔
は変わらず彼女と共にある。",400,6000,100,10,20,"hlog_106000060_sil_3_01_dag","hlog_106000060_sil_3_01_dag",False,"Chara_comment/106000060","Chara_comment/106000060","106000060_cha","106000060_cha","Chara_icon/106000060","Chara_icon/106000060","Chara_icon_loss/106000060","Chara_icon_loss/106000060","2019/01/01",True,"1", -106000070,6,"シリカ","竜影の閃爪",3,4,1,0,106000070,203000000,-1,106000070,900,90320,180,18288,45,4515,170,16930,1,0,0,1,0,0,2,0,0,99999,0,0,"SAOの中では年少に分類されるシ
リカ。幼い彼女が戦線に立ち、心
身無事に事件を生き延びたのは極
めて稀なケースである。",400,6000,100,10,20,"hlog_106000070_sil_3_01_dag","hlog_106000070_sil_3_01_dag",False,"Chara_comment/106000070","Chara_comment/106000070","106000070_cha","106000070_cha","Chara_icon/106000070","Chara_icon/106000070","Chara_icon_loss/106000070","Chara_icon_loss/106000070","2019/01/01",True,"1", -106000080,6,"シリカ","可憐なる竜使い",3,4,4,0,106000080,203000000,-1,106000080,1120,112900,150,15240,55,5645,170,16930,1,0,0,1,0,0,2,0,0,99999,0,0,"SAOの中では年少に分類されるシ
リカ。幼い彼女が戦線に立ち、心
身無事に事件を生き延びたのは極
めて稀なケースである。",400,6000,100,10,20,"hlog_106000080_sil_3_01_dag","hlog_106000080_sil_3_01_dag",False,"Chara_comment/106000080","Chara_comment/106000080","106000080_cha","106000080_cha","Chara_icon/106000080","Chara_icon/106000080","Chara_icon_loss/106000080","Chara_icon_loss/106000080","2019/01/01",True,"1", -106000090,6,"シリカ","慈愛の竜姫",4,3,3,0,106000090,202000000,-1,106000090,1150,116120,138,13932,60,5805,230,23220,1,0,0,1,0,0,2,0,0,2,0,0,"ヒーリング、バフ、索敵。彼女と
使い魔による多彩なサポートは、
仲間にとっても貴重な存在となっ
ている。",800,10000,100,10,20,"hlog_106000090_sil_4_01_rap","hlog_106000090_sil_4_01_rap",False,"Chara_comment/106000090","Chara_comment/106000090","106000090_cha","106000090_cha","Chara_icon/106000090","Chara_icon/106000090","Chara_icon_loss/106000090","Chara_icon_loss/106000090","2019/01/01",True,"1", -106000100,6,"シリカ","戯れの笑顔",4,4,4,0,106000100,203000000,-1,106000100,1280,129020,174,17418,65,6450,190,19350,1,0,0,1,0,0,2,0,0,2,0,0,"本来使い魔は設定に基づいた行動
のみを行うが、彼女の使い魔にそ
れは当てはまらず、現実の生物の
ような振舞いが度々確認される。",800,10000,100,10,20,"hlog_106000100_sil_4_01_dag","hlog_106000100_sil_4_01_dag",False,"Chara_comment/106000100","Chara_comment/106000100","106000100_cha","106000100_cha","Chara_icon/106000100","Chara_icon/106000100","Chara_icon_loss/106000100","Chara_icon_loss/106000100","2019/01/01",True,"1", -106000110,6,"シリカ","歓談と休息",4,4,1,0,106000110,203000000,-1,106000110,1020,103220,210,20904,50,5160,190,19350,1,0,0,1,0,0,2,0,0,2,0,0,"闘いが明け緊張状態から解放され
れば、彼女はごく一般的な年相応
の少女である。これはその一面を
記す記録。",800,10000,100,10,20,"hlog_106000110_sil_4_01_dag","hlog_106000110_sil_4_01_dag",False,"Chara_comment/106000110","Chara_comment/106000110","106000110_cha","106000110_cha","Chara_icon/106000110","Chara_icon/106000110","Chara_icon_loss/106000110","Chara_icon_loss/106000110","2019/01/01",True,"1", -106000120,6,"シリカ","爪刃連撃",5,4,3,0,106000120,203000000,-1,106000120,1300,130640,156,15678,65,6530,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"他に類を見ない使い魔との密な連
携。これが年少でありながらも他
プレイヤーと一線を画す大きな要
因であると推測される。",1600,20000,100,10,20,"hlog_106000120_sil_5_01_dag","hlog_106000121_sil_5_01_dag",False,"Chara_comment/106000120","Chara_comment/106000121","106000120_cha","106000121_cha","Chara_icon/106000120","Chara_icon/106000121","Chara_icon_loss/106000120","Chara_icon_loss/106000121","2019/01/01",True,"1", -106000130,6,"シリカ","友愛と安寧",4,4,4,0,106000130,203000000,-1,106000130,1280,129020,174,17418,65,6450,190,19350,1,0,0,1,0,0,2,0,0,2,0,0,"ALOにて記録された一枚。苦楽を
共にしてきた使い魔ピナは彼女に
とって現実の仲間同様に、大きな
支えとなっている。",800,10000,100,10,20,"hlog_106000130_sil_4_01_dag","hlog_106000130_sil_4_01_dag",False,"Chara_comment/106000130","Chara_comment/106000130","106000130_cha","106000130_cha","Chara_icon/106000130","Chara_icon/106000130","Chara_icon_loss/106000130","Chara_icon_loss/106000130","2019/01/01",True,"1", -107000010,7,"クライン","義侠の徒",1,6,4,0,107000010,205000000,-1,107000010,1100,110880,132,13308,50,5040,80,8060,1,0,0,1,0,0,99999,0,0,99999,0,0,"自称侍の青年。軽く見えるが人情
に厚く責任感は強い。ギルド風林
火山のリーダーを務める。",100,1000,100,10,20,"hlog_107000010_kle_1_01_kat","hlog_107000010_kle_1_01_kat",False,"Chara_comment/107000010","Chara_comment/107000010","107000010_cha","107000010_cha","Chara_icon/107000010","Chara_icon/107000010","Chara_icon_loss/107000010","Chara_icon_loss/107000010","2019/01/01",True,"1", -107000020,7,"クライン","信義の剣豪",2,1,2,0,107000020,201000000,-1,107000020,1450,146360,126,12774,65,6655,80,7740,1,0,0,1,0,0,2,0,0,99999,0,0,"自称侍の青年。軽く見えるが人情
に厚く責任感は強い。ギルド風林
火山のリーダーを務める。",200,2000,100,10,20,"hlog_107000020_kle_2_01_oha","hlog_107000020_kle_2_01_oha",False,"Chara_comment/107000020","Chara_comment/107000020","107000020_cha","107000020_cha","Chara_icon/107000020","Chara_icon/107000020","Chara_icon_loss/107000020","Chara_icon_loss/107000020","2019/01/01",True,"1", -107000030,7,"クライン","新天地を馳せる者",2,6,1,0,107000030,205000000,-1,107000030,1060,106440,192,19158,50,4840,100,9680,1,0,0,1,0,0,2,0,0,99999,0,0,"新生ALOで火妖精族となったが、
北欧神話の世界であっても、彼は
己のスタイルを貫いている。",200,2000,100,10,20,"hlog_107000030_kle_2_01_kat","hlog_107000030_kle_2_01_kat",False,"Chara_comment/107000030","Chara_comment/107000030","107000030_cha","107000030_cha","Chara_icon/107000030","Chara_icon/107000030","Chara_icon_loss/107000030","Chara_icon_loss/107000030","2019/01/01",True,"1", -107000040,7,"クライン","義心の武将",2,6,4,0,107000040,205000000,-1,107000040,1320,133060,156,15966,60,6050,100,9680,1,0,0,1,0,0,2,0,0,99999,0,0,"自称侍の青年。軽く見えるが人情
に厚く責任感は強い。ギルド風林
火山のリーダーを務める。",200,2000,100,10,20,"hlog_107000040_kle_2_01_kat","hlog_107000040_kle_2_01_kat",False,"Chara_comment/107000040","Chara_comment/107000040","107000040_cha","107000040_cha","Chara_icon/107000040","Chara_icon/107000040","Chara_icon_loss/107000040","Chara_icon_loss/107000040","2019/01/01",True,"1", -107000050,7,"クライン","義勇の武士",3,9,1,0,107000050,208000000,-1,107000050,1230,124190,222,22356,55,5645,110,11290,1,0,0,1,0,0,2,0,0,99999,0,0,"自称侍の青年。軽く見えるが人情
に厚く責任感は強い。ギルド風林
火山のリーダーを務める。",400,6000,100,10,20,"hlog_107000050_kle_3_01_axe","hlog_107000050_kle_3_01_axe",False,"Chara_comment/107000050","Chara_comment/107000050","107000050_cha","107000050_cha","Chara_icon/107000050","Chara_icon/107000050","Chara_icon_loss/107000050","Chara_icon_loss/107000050","2019/01/01",True,"1", -107000060,7,"クライン","断撃の斬光",3,6,2,0,107000060,205000000,-1,107000060,1690,170760,150,14904,75,7760,90,9030,1,0,0,1,0,0,2,0,0,99999,0,0,"自称侍の青年。軽く見えるが人情
に厚く責任感は強い。ギルド風林
火山のリーダーを務める。",400,6000,100,10,20,"hlog_107000060_kle_3_01_kat","hlog_107000060_kle_3_01_kat",False,"Chara_comment/107000060","Chara_comment/107000060","107000060_cha","107000060_cha","Chara_icon/107000060","Chara_icon/107000060","Chara_icon_loss/107000060","Chara_icon_loss/107000060","2019/01/01",True,"1", -107000070,7,"クライン","炎燃ゆる侍妖精",3,6,4,0,107000070,205000000,-1,107000070,1540,155230,186,18630,70,7055,110,11290,1,0,0,1,0,0,2,0,0,99999,0,0,"新生ALOで火妖精族となったが、
北欧神話の世界であっても、彼は
己のスタイルを貫いている。",400,6000,100,10,20,"hlog_107000070_kle_3_01_kat","hlog_107000070_kle_3_01_kat",False,"Chara_comment/107000070","Chara_comment/107000070","107000070_cha","107000070_cha","Chara_icon/107000070","Chara_icon/107000070","Chara_icon_loss/107000070","Chara_icon_loss/107000070","2019/01/01",True,"1", -107000080,7,"クライン","真朱の武人",3,6,1,0,107000080,205000000,-1,107000080,1230,124190,222,22356,55,5645,110,11290,1,0,0,1,0,0,2,0,0,99999,0,0,"自称侍の青年。軽く見えるが人情
に厚く責任感は強い。ギルド風林
火山のリーダーを務める。",400,6000,100,10,20,"hlog_107000080_kle_3_01_kat","hlog_107000080_kle_3_01_kat",False,"Chara_comment/107000080","Chara_comment/107000080","107000080_cha","107000080_cha","Chara_icon/107000080","Chara_icon/107000080","Chara_icon_loss/107000080","Chara_icon_loss/107000080","2019/01/01",True,"1", -107000090,7,"クライン","歴戦の義将",4,11,4,0,107000090,211000000,-1,107000090,1760,177410,210,21288,80,8065,130,12900,1,0,0,1,0,0,2,0,0,2,0,0,"SAOでは攻略組に参画しながらも
最後までギルドメンバーを守り抜
いたことから、その実力と寄せら
れる信頼の高さが窺われる。",800,10000,100,10,20,"hlog_107000090_kle_4_01_bow","hlog_107000090_kle_4_01_bow",False,"Chara_comment/107000090","Chara_comment/107000090","107000090_cha","107000090_cha","Chara_icon/107000090","Chara_icon/107000090","Chara_icon_loss/107000090","Chara_icon_loss/107000090","2019/01/01",True,"1", -107000100,7,"クライン","剣豪推参",4,6,1,0,107000100,205000000,-1,107000100,1410,141930,252,25548,65,6450,130,12900,1,0,0,1,0,0,2,0,0,2,0,0,"己が信じる刀で生きる。この姿勢
はデスゲームにあっても変わるこ
とはなかった。",800,10000,100,10,20,"hlog_107000100_kle_4_01_kat","hlog_107000100_kle_4_01_kat",False,"Chara_comment/107000100","Chara_comment/107000100","107000100_cha","107000100_cha","Chara_icon/107000100","Chara_icon/107000100","Chara_icon_loss/107000100","Chara_icon_loss/107000100","2019/01/01",True,"1", -107000110,7,"クライン","制空の構え",5,6,1,0,107000110,205000000,-1,107000110,1580,159670,288,28740,70,7260,140,14520,1,0,0,1,0,0,2,0,0,2,0,0,"魔法が存在するALOにあっても、
彼は己の侍の道を通すため、自ら
魔法の使用を禁じている。",1600,20000,100,10,20,"hlog_107000110_kle_5_01_kat","hlog_107000111_kle_5_01_kat",False,"Chara_comment/107000110","Chara_comment/107000111","107000110_cha","107000111_cha","Chara_icon/107000110","Chara_icon/107000111","Chara_icon_loss/107000110","Chara_icon_loss/107000111","2019/01/01",True,"1", -107000120,7,"クライン","破顔の戦友",4,6,4,0,107000120,205000000,-1,107000120,1760,177410,210,21288,80,8065,130,12900,1,0,0,1,0,0,2,0,0,2,0,0,"ALOにて記録された一枚。気さく
な性格でありながら義理堅く、分
け隔てなく接するからこそ、彼の
周囲に人が絶えないのだろう。",800,10000,100,10,20,"hlog_107000120_kle_4_01_kat","hlog_107000120_kle_4_01_kat",False,"Chara_comment/107000120","Chara_comment/107000120","107000120_cha","107000120_cha","Chara_icon/107000120","Chara_icon/107000120","Chara_icon_loss/107000120","Chara_icon_loss/107000120","2019/01/01",True,"1", -108000010,8,"エギル","豪斧の戦士",1,9,2,0,108000010,208000000,-1,108000010,1380,139100,108,10644,60,5820,70,7260,1,0,0,1,0,0,99999,0,0,99999,0,0,"商人としての営業の傍ら、攻略組
の一員として前線に立つなど、幅
広く活動していたことが記録され
ている。",100,1000,100,10,20,"hlog_108000010_agi_1_01_axe","hlog_108000010_agi_1_01_axe",False,"Chara_comment/108000010","Chara_comment/108000010","108000010_cha","108000010_cha","Chara_icon/108000010","Chara_icon/108000010","Chara_icon_loss/108000010","Chara_icon_loss/108000010","2019/01/01",True,"1", -108000020,8,"エギル","不動の観察眼",2,11,2,0,108000020,211000000,-1,108000020,1660,166920,126,12774,70,6985,90,8710,1,0,0,1,0,0,2,0,0,99999,0,0,"商人としての営業の傍ら、攻略組
の一員として前線に立つなど、幅
広く活動していたことが記録され
ている。",200,2000,100,10,20,"hlog_108000020_agi_2_01_bow","hlog_108000020_agi_2_01_bow",False,"Chara_comment/108000020","Chara_comment/108000020","108000020_cha","108000020_cha","Chara_icon/108000020","Chara_icon/108000020","Chara_icon_loss/108000020","Chara_icon_loss/108000020","2019/01/01",True,"1", -108000030,8,"エギル","撃滅の断斧",2,9,2,0,108000030,208000000,-1,108000030,1660,166920,126,12774,70,6985,90,8710,1,0,0,1,0,0,2,0,0,99999,0,0,"商人としての営業の傍ら、攻略組
の一員として前線に立つなど、幅
広く活動していたことが記録され
ている。",200,2000,100,10,20,"hlog_108000030_agi_2_01_axe","hlog_108000030_agi_2_01_axe",False,"Chara_comment/108000030","Chara_comment/108000030","108000030_cha","108000030_cha","Chara_icon/108000030","Chara_icon/108000030","Chara_icon_loss/108000030","Chara_icon_loss/108000030","2019/01/01",True,"1", -108000040,8,"エギル","仇裂く戦斧",2,9,4,0,108000040,208000000,-1,108000040,1440,145150,156,15966,65,6655,110,10890,1,0,0,1,0,0,2,0,0,99999,0,0,"商人としての営業の傍ら、攻略組
の一員として前線に立つなど、幅
広く活動していたことが記録され
ている。",200,2000,100,10,20,"hlog_108000040_agi_2_01_axe","hlog_108000040_agi_2_01_axe",False,"Chara_comment/108000040","Chara_comment/108000040","108000040_cha","108000040_cha","Chara_icon/108000040","Chara_icon/108000040","Chara_icon_loss/108000040","Chara_icon_loss/108000040","2019/01/01",True,"1", -108000050,8,"エギル","鎧袖一触",3,6,2,0,108000050,205000000,-1,108000050,1930,194750,150,14904,80,8150,100,10160,1,0,0,1,0,0,2,0,0,99999,0,0,"ALOに場所を移してもなお、彼の
豪腕と商人としての信頼は変わら
ず認知されている。",400,6000,100,10,20,"hlog_108000050_agi_3_01_kat","hlog_108000050_agi_3_01_kat",False,"Chara_comment/108000050","Chara_comment/108000050","108000050_cha","108000050_cha","Chara_icon/108000050","Chara_icon/108000050","Chara_icon_loss/108000050","Chara_icon_loss/108000050","2019/01/01",True,"1", -108000060,8,"エギル","轟閃の砕斧",3,9,4,0,108000060,208000000,-1,108000060,1680,169340,186,18630,75,7760,130,12700,1,0,0,1,0,0,2,0,0,99999,0,0,"商人としての営業の傍ら、攻略組
の一員として前線に立つなど、幅
広く活動していたことが記録され
ている。",400,6000,100,10,20,"hlog_108000060_agi_3_01_axe","hlog_108000060_agi_3_01_axe",False,"Chara_comment/108000060","Chara_comment/108000060","108000060_cha","108000060_cha","Chara_icon/108000060","Chara_icon/108000060","Chara_icon_loss/108000060","Chara_icon_loss/108000060","2019/01/01",True,"1", -108000070,8,"エギル","闘斧の豪商",3,9,1,0,108000070,208000000,-1,108000070,1340,135480,222,22356,60,6210,130,12700,1,0,0,1,0,0,2,0,0,99999,0,0,"商人としての営業の傍ら、攻略組
の一員として前線に立つなど、幅
広く活動していたことが記録され
ている。",400,6000,100,10,20,"hlog_108000070_agi_3_01_axe","hlog_108000070_agi_3_01_axe",False,"Chara_comment/108000070","Chara_comment/108000070","108000070_cha","108000070_cha","Chara_icon/108000070","Chara_icon/108000070","Chara_icon_loss/108000070","Chara_icon_loss/108000070","2019/01/01",True,"1", -108000080,8,"エギル","剛腕の威風",3,9,2,0,108000080,208000000,-1,108000080,1930,194750,150,14904,80,8150,100,10160,1,0,0,1,0,0,2,0,0,99999,0,0,"商人としての営業の傍ら、攻略組
の一員として前線に立つなど、幅
広く活動していたことが記録され
ている。",400,6000,100,10,20,"hlog_108000080_agi_3_01_axe","hlog_108000080_agi_3_01_axe",False,"Chara_comment/108000080","Chara_comment/108000080","108000080_cha","108000080_cha","Chara_icon/108000080","Chara_icon/108000080","Chara_icon_loss/108000080","Chara_icon_loss/108000080","2019/01/01",True,"1", -108000090,8,"エギル","破砕の潰鎚",4,10,2,0,108000090,209000000,-1,108000090,2210,222570,168,17034,90,9315,120,11610,1,0,0,1,0,0,2,0,0,2,0,0,"闘いに赴く時、エギルは率先して
仲間を守る位置に立つ。それはSA
Oから変わらない、彼が考える彼
自身の役割である。",800,10000,100,10,20,"hlog_108000090_agi_4_01_mac","hlog_108000090_agi_4_01_mac",False,"Chara_comment/108000090","Chara_comment/108000090","108000090_cha","108000090_cha","Chara_icon/108000090","Chara_icon/108000090","Chara_icon_loss/108000090","Chara_icon_loss/108000090","2019/01/01",True,"1", -108000100,8,"エギル","泰然の風柄",4,9,1,0,108000100,208000000,-1,108000100,1540,154830,252,25548,70,7095,140,14520,1,0,0,1,0,0,2,0,0,2,0,0,"キリト達一行にとって、エギルの
存在は大きい。それは彼が仲間の
中でもっとも泰然とし、落ち着き
をもって事に接するからだ。",800,10000,100,10,20,"hlog_108000100_agi_4_01_axe","hlog_108000100_agi_4_01_axe",False,"Chara_comment/108000100","Chara_comment/108000100","108000100_cha","108000100_cha","Chara_icon/108000100","Chara_icon/108000100","Chara_icon_loss/108000100","Chara_icon_loss/108000100","2019/01/01",True,"1", -108000110,8,"エギル","不動覇山",5,9,2,0,108000110,208000000,-1,108000110,2480,250390,192,19158,105,10480,130,13060,1,0,0,1,0,0,2,0,0,2,0,0,"普段は商人として活動するエギル
だが、闘いの場ではその見た目に
違わぬ豪腕を振るい、敵を圧倒す
る。",1600,20000,100,10,20,"hlog_108000110_agi_5_01_axe","hlog_108000111_agi_5_01_axe",False,"Chara_comment/108000110","Chara_comment/108000111","108000110_cha","108000111_cha","Chara_icon/108000110","Chara_icon/108000111","Chara_icon_loss/108000110","Chara_icon_loss/108000111","2019/01/01",True,"1", -108000120,8,"エギル","寛ぎのもてなし",4,9,4,0,108000120,208000000,-1,108000120,1920,193540,210,21288,90,8870,140,14520,1,0,0,1,0,0,2,0,0,2,0,0,"ALOにて記録された一枚。彼の淹
れるコーヒーは仲間内でも評判が
高く、安息の時間をもたらしてく
れる。",800,10000,100,10,20,"hlog_108000120_agi_4_01_axe","hlog_108000120_agi_4_01_axe",False,"Chara_comment/108000120","Chara_comment/108000120","108000120_cha","108000120_cha","Chara_icon/108000120","Chara_icon/108000120","Chara_icon_loss/108000120","Chara_icon_loss/108000120","2019/01/01",True,"1", -109000010,9,"ユウキ","剣聖の風格",1,1,1,0,109000010,201000000,-1,109000010,800,80640,156,15966,35,3630,100,10080,1,0,0,1,0,0,99999,0,0,99999,0,0,"新生ALOに突如現れた闇妖精族の
剣士。目で追うことすら敵わぬ剣
の武を指し、人は彼女を《絶剣》
と呼んだ。",100,1000,100,10,20,"hlog_109000010_yuu_1_01_oha","hlog_109000010_yuu_1_01_oha",False,"Chara_comment/109000010","Chara_comment/109000010","109000010_cha","109000010_cha","Chara_icon/109000010","Chara_icon/109000010","Chara_icon_loss/109000010","Chara_icon_loss/109000010","2019/01/01",True,"1", -109000020,9,"ユウキ","真純の紫星",2,4,4,0,109000020,203000000,-1,109000020,1200,120960,156,15966,55,5445,120,12100,1,0,0,1,0,0,2,0,0,99999,0,0,"新生ALOに突如現れた闇妖精族の
剣士。目で追うことすら敵わぬ剣
の武を指し、人は彼女を《絶剣》
と呼んだ。",200,2000,100,10,20,"hlog_109000020_yuu_2_01_dag","hlog_109000020_yuu_2_01_dag",False,"Chara_comment/109000020","Chara_comment/109000020","109000020_cha","109000020_cha","Chara_icon/109000020","Chara_icon/109000020","Chara_icon_loss/109000020","Chara_icon_loss/109000020","2019/01/01",True,"1", -109000030,9,"ユウキ","天雷の迅撃",2,1,1,0,109000030,201000000,-1,109000030,960,96770,192,19158,45,4355,120,12100,1,0,0,1,0,0,2,0,0,99999,0,0,"新生ALOに突如現れた闇妖精族の
剣士。目で追うことすら敵わぬ剣
の武を指し、人は彼女を《絶剣》
と呼んだ。",200,2000,100,10,20,"hlog_109000030_yuu_2_01_oha","hlog_109000030_yuu_2_01_oha",False,"Chara_comment/109000030","Chara_comment/109000030","109000030_cha","109000030_cha","Chara_icon/109000030","Chara_icon/109000030","Chara_icon_loss/109000030","Chara_icon_loss/109000030","2019/01/01",True,"1", -109000040,9,"ユウキ","神速の剣技",2,1,4,0,109000040,201000000,-1,109000040,1200,120960,156,15966,55,5445,120,12100,1,0,0,1,0,0,2,0,0,99999,0,0,"新生ALOに突如現れた闇妖精族の
剣士。目で追うことすら敵わぬ剣
の武を指し、人は彼女を《絶剣》
と呼んだ。",200,2000,100,10,20,"hlog_109000040_yuu_2_01_oha","hlog_109000040_yuu_2_01_oha",False,"Chara_comment/109000040","Chara_comment/109000040","109000040_cha","109000040_cha","Chara_icon/109000040","Chara_icon/109000040","Chara_icon_loss/109000040","Chara_icon_loss/109000040","2019/01/01",True,"1", -109000050,9,"ユウキ","純真な閃風",3,6,1,0,109000050,205000000,-1,109000050,1120,112900,222,22356,50,5080,140,14110,1,0,0,1,0,0,2,0,0,99999,0,0,"新生ALOに突如現れた闇妖精族の
剣士。目で追うことすら敵わぬ剣
の武を指し、人は彼女を《絶剣》
と呼んだ。",400,6000,100,10,20,"hlog_109000050_yuu_3_01_kat","hlog_109000050_yuu_3_01_kat",False,"Chara_comment/109000050","Chara_comment/109000050","109000050_cha","109000050_cha","Chara_icon/109000050","Chara_icon/109000050","Chara_icon_loss/109000050","Chara_icon_loss/109000050","2019/01/01",True,"1", -109000060,9,"ユウキ","意思貫く剣先",3,1,2,0,109000060,201000000,-1,109000060,1540,155230,150,14904,70,6985,110,11290,1,0,0,1,0,0,2,0,0,99999,0,0,"新生ALOに突如現れた闇妖精族の
剣士。目で追うことすら敵わぬ剣
の武を指し、人は彼女を《絶剣》
と呼んだ。",400,6000,100,10,20,"hlog_109000060_yuu_3_01_oha","hlog_109000060_yuu_3_01_oha",False,"Chara_comment/109000060","Chara_comment/109000060","109000060_cha","109000060_cha","Chara_icon/109000060","Chara_icon/109000060","Chara_icon_loss/109000060","Chara_icon_loss/109000060","2019/01/01",True,"1", -109000070,9,"ユウキ","空裂く黒刃",3,1,3,0,109000070,201000000,-1,109000070,1260,127010,150,14904,55,5715,170,16930,1,0,0,1,0,0,2,0,0,99999,0,0,"新生ALOに突如現れた闇妖精族の
剣士。目で追うことすら敵わぬ剣
の武を指し、人は彼女を《絶剣》
と呼んだ。",400,6000,100,10,20,"hlog_109000070_yuu_3_01_oha","hlog_109000070_yuu_3_01_oha",False,"Chara_comment/109000070","Chara_comment/109000070","109000070_cha","109000070_cha","Chara_icon/109000070","Chara_icon/109000070","Chara_icon_loss/109000070","Chara_icon_loss/109000070","2019/01/01",True,"1", -109000080,9,"ユウキ","絶剣",3,1,4,0,109000080,201000000,-1,109000080,1400,141120,186,18630,65,6350,140,14110,1,0,0,1,0,0,2,0,0,99999,0,0,"新生ALOに突如現れた闇妖精族の
剣士。目で追うことすら敵わぬ剣
の武を指し、人は彼女を《絶剣》
と呼んだ。",400,6000,100,10,20,"hlog_109000080_yuu_3_01_oha","hlog_109000080_yuu_3_01_oha",False,"Chara_comment/109000080","Chara_comment/109000080","109000080_cha","109000080_cha","Chara_icon/109000080","Chara_icon/109000080","Chara_icon_loss/109000080","Chara_icon_loss/109000080","2019/01/01",True,"1", -109000090,9,"ユウキ","黄昏舞う黒翼",4,3,2,0,109000090,202000000,-1,109000090,1760,177410,168,17034,80,7985,130,12900,1,0,0,1,0,0,2,0,0,2,0,0,"彼女の比類なき剣技の根底を成し
ているのは、その天賦の才とVR
適正だけでなく、人には窺い知れ
ぬ強い覚悟なのであろう。",800,10000,100,10,20,"hlog_109000090_yuu_4_01_rap","hlog_109000090_yuu_4_01_rap",False,"Chara_comment/109000090","Chara_comment/109000090","109000090_cha","109000090_cha","Chara_icon/109000090","Chara_icon/109000090","Chara_icon_loss/109000090","Chara_icon_loss/109000090","2019/01/01",True,"1", -109000100,9,"ユウキ","寸刻の微笑",4,1,3,0,109000100,201000000,-1,109000100,1440,145150,168,17034,65,6530,190,19350,1,0,0,1,0,0,2,0,0,2,0,0,"屈託なく笑い、溌剌とした彼女だ
が、時折言葉にし難い儚さが垣間
見える瞬間がある。",800,10000,100,10,20,"hlog_109000100_yuu_4_01_oha","hlog_109000100_yuu_4_01_oha",False,"Chara_comment/109000100","Chara_comment/109000100","109000100_cha","109000100_cha","Chara_icon/109000100","Chara_icon/109000100","Chara_icon_loss/109000100","Chara_icon_loss/109000100","2019/01/01",True,"1", -109000110,9,"ユウキ","神速の黒き閃光",5,1,4,0,109000110,201000000,-1,109000110,1800,181440,240,23952,80,8165,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"不敗の超剣士《絶剣》ユウキ。彼
女が最速最強たりえるのは、彼女
を取り巻く事情と自身の強い意思
の力によるものなのだろう。",1600,20000,100,10,20,"hlog_109000110_yuu_5_01_oha","hlog_109000111_yuu_5_01_oha",False,"Chara_comment/109000110","Chara_comment/109000111","109000110_cha","109000111_cha","Chara_icon/109000110","Chara_icon/109000111","Chara_icon_loss/109000110","Chara_icon_loss/109000111","2019/01/01",True,"1", -109000120,9,"ユウキ","知友の笑み",4,1,4,0,109000120,201000000,-1,109000120,1600,161280,210,21288,70,7260,160,16130,1,0,0,1,0,0,2,0,0,2,0,0,"ALOにて記録された一枚。仮想世
界と、そこで得られる仲間達との
絆は、彼女にとって現実のそれ以
上に大きな意味を持つ。",800,10000,100,10,20,"hlog_109000120_yuu_4_01_oha","hlog_109000120_yuu_4_01_oha",False,"Chara_comment/109000120","Chara_comment/109000120","109000120_cha","109000120_cha","Chara_icon/109000120","Chara_icon/109000120","Chara_icon_loss/109000120","Chara_icon_loss/109000120","2019/01/01",True,"1", -109000130,9,"ユウキ","蒼穹を駆ける英傑",4,1,1,0,109000130,201000000,-1,109000130,1280,129020,252,25548,60,5805,160,16130,1,0,0,1,0,0,2,0,0,2,0,0,"新たな世界、新たな仲間。予想だ
にせぬ驚きと、鮮やかな色彩を纏
い映るそれら全てが、彼女の希望
となる。",800,10000,100,10,20,"hlog_109000130_yuu_4_01_oha","hlog_109000130_yuu_4_01_oha",False,"Chara_comment/109000130","Chara_comment/109000130","109000130_cha","109000130_cha","Chara_icon/109000130","Chara_icon/109000130","Chara_icon_loss/109000130","Chara_icon_loss/109000130","2019/01/01",True,"1", -110000010,10,"アルゴ","智慧賜う鼠",1,8,3,0,110000010,207000000,-1,110000010,860,86180,90,9192,45,4310,140,14520,1,0,0,1,0,0,99999,0,0,99999,0,0,"情報屋《鼠のアルゴ》。SAOβテ
スターでもあった彼女は、中堅以
下のプレイヤーに攻略情報を配布
するなど、支援にも注力した。",100,1000,100,10,20,"hlog_110000010_arg_1_01_clw","hlog_110000010_arg_1_01_clw",False,"Chara_comment/110000010","Chara_comment/110000010","110000010_cha","110000010_cha","Chara_icon/110000010","Chara_icon/110000010","Chara_icon_loss/110000010","Chara_icon_loss/110000010","2019/01/01",True,"1", -110000020,10,"アルゴ","情報屋の矜持",2,9,4,0,110000020,208000000,-1,110000020,1140,114910,138,13788,55,5745,140,14520,1,0,0,1,0,0,2,0,0,99999,0,0,"情報屋《鼠のアルゴ》。SAOβテ
スターでもあった彼女は、中堅以
下のプレイヤーに攻略情報を配布
するなど、支援にも注力した。",200,2000,100,10,20,"hlog_110000020_arg_2_01_axe","hlog_110000020_arg_2_01_axe",False,"Chara_comment/110000020","Chara_comment/110000020","110000020_cha","110000020_cha","Chara_icon/110000020","Chara_icon/110000020","Chara_icon_loss/110000020","Chara_icon_loss/110000020","2019/01/01",True,"1", -110000030,10,"アルゴ","空駆ける猫",2,4,4,0,110000030,203000000,-1,110000030,1140,114910,138,13788,55,5745,140,14520,1,0,0,1,0,0,2,0,0,99999,0,0,"新生ALOにおける平時の記録。
SAOほどの目立った活動こそない
ものの、情報屋としての彼女の腕
はいささかも衰えてはいない。",200,2000,100,10,20,"hlog_110000030_arg_2_01_dag","hlog_110000030_arg_2_01_dag",False,"Chara_comment/110000030","Chara_comment/110000030","110000030_cha","110000030_cha","Chara_icon/110000030","Chara_icon/110000030","Chara_icon_loss/110000030","Chara_icon_loss/110000030","2019/01/01",True,"1", -110000040,10,"アルゴ","疾風の戦爪",2,8,3,0,110000040,207000000,-1,110000040,1030,103420,108,11034,50,5170,170,17420,1,0,0,1,0,0,2,0,0,99999,0,0,"新生ALOにおける平時の記録。
SAOほどの目立った活動こそない
ものの、情報屋としての彼女の腕
はいささかも衰えてはいない。",200,2000,100,10,20,"hlog_110000040_arg_2_01_clw","hlog_110000040_arg_2_01_clw",False,"Chara_comment/110000040","Chara_comment/110000040","110000040_cha","110000040_cha","Chara_icon/110000040","Chara_icon/110000040","Chara_icon_loss/110000040","Chara_icon_loss/110000040","2019/01/01",True,"1", -110000050,10,"アルゴ","迅疾の快足",3,1,3,0,110000050,201000000,-1,110000050,1200,120660,126,12870,60,6035,200,20320,1,0,0,1,0,0,2,0,0,99999,0,0,"情報屋《鼠のアルゴ》。SAOβテ
スターでもあった彼女は、中堅以
下のプレイヤーに攻略情報を配布
するなど、支援にも注力した。",400,6000,100,10,20,"hlog_110000050_arg_3_01_oha","hlog_110000050_arg_3_01_oha",False,"Chara_comment/110000050","Chara_comment/110000050","110000050_cha","110000050_cha","Chara_icon/110000050","Chara_icon/110000050","Chara_icon_loss/110000050","Chara_icon_loss/110000050","2019/01/01",True,"1", -110000060,10,"アルゴ","看破する瞳",3,4,3,0,110000060,203000000,-1,110000060,1200,120660,126,12870,60,6035,200,20320,1,0,0,1,0,0,2,0,0,99999,0,0,"新生ALOにおける平時の記録。
SAOほどの目立った活動こそない
ものの、情報屋としての彼女の腕
はいささかも衰えてはいない。",400,6000,100,10,20,"hlog_110000060_arg_3_01_dag","hlog_110000060_arg_3_01_dag",False,"Chara_comment/110000060","Chara_comment/110000060","110000060_cha","110000060_cha","Chara_icon/110000060","Chara_icon/110000060","Chara_icon_loss/110000060","Chara_icon_loss/110000060","2019/01/01",True,"1", -110000070,10,"アルゴ","地馳せる素破",3,8,1,0,110000070,207000000,-1,110000070,1060,107250,192,19308,55,5365,170,16930,1,0,0,1,0,0,2,0,0,99999,0,0,"情報屋《鼠のアルゴ》。SAOβテ
スターでもあった彼女は、中堅以
下のプレイヤーに攻略情報を配布
するなど、支援にも注力した。",400,6000,100,10,20,"hlog_110000070_arg_3_01_clw","hlog_110000070_arg_3_01_clw",False,"Chara_comment/110000070","Chara_comment/110000070","110000070_cha","110000070_cha","Chara_icon/110000070","Chara_icon/110000070","Chara_icon_loss/110000070","Chara_icon_loss/110000070","2019/01/01",True,"1", -110000080,10,"アルゴ","香染の諜報者",3,8,4,0,110000080,207000000,-1,110000080,1330,134060,162,16086,65,6705,170,16930,1,0,0,1,0,0,2,0,0,99999,0,0,"情報屋《鼠のアルゴ》。SAOβテ
スターでもあった彼女は、中堅以
下のプレイヤーに攻略情報を配布
するなど、支援にも注力した。",400,6000,100,10,20,"hlog_110000080_arg_3_01_clw","hlog_110000080_arg_3_01_clw",False,"Chara_comment/110000080","Chara_comment/110000080","110000080_cha","110000080_cha","Chara_icon/110000080","Chara_icon/110000080","Chara_icon_loss/110000080","Chara_icon_loss/110000080","2019/01/01",True,"1", -110000090,10,"アルゴ","お茶目な情報屋",4,10,3,0,110000090,209000000,-1,110000090,1370,137890,144,14706,70,6895,230,23220,1,0,0,1,0,0,2,0,0,2,0,0,"得られる情報が金になるか否かが
彼女にとって価値判断の基準であ
り、それが無いものにはいささか
の情も持ち合わせていない。",800,10000,100,10,20,"hlog_110000090_arg_4_01_mac","hlog_110000090_arg_4_01_mac",False,"Chara_comment/110000090","Chara_comment/110000090","110000090_cha","110000090_cha","Chara_icon/110000090","Chara_icon/110000090","Chara_icon_loss/110000090","Chara_icon_loss/110000090","2019/01/01",True,"1", -110000100,10,"アルゴ","刻を望む双眸",4,8,4,0,110000100,207000000,-1,110000100,1520,153220,180,18384,75,7660,190,19350,1,0,0,1,0,0,2,0,0,2,0,0,"全ては金の為…そう思わせる言動
の多い彼女だが、その一方でプレ
イヤー全体への利益に繋がるもの
は無償で施すという一面も持つ。",800,10000,100,10,20,"hlog_110000100_arg_4_01_clw","hlog_110000100_arg_4_01_clw",False,"Chara_comment/110000100","Chara_comment/110000100","110000100_cha","110000100_cha","Chara_icon/110000100","Chara_icon/110000100","Chara_icon_loss/110000100","Chara_icon_loss/110000100","2019/01/01",True,"1", -110000110,10,"アルゴ","陽光香る笑み",5,8,3,0,110000110,207000000,-1,110000110,1540,155130,162,16548,75,7755,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"彼女と接する時には注意せねばな
らない、なぜならその無邪気な笑
みとは裏腹に、瞳は虎視眈々と情
報を見定めているからだ。",1600,20000,100,10,20,"hlog_110000110_arg_5_01_clw","hlog_110000111_arg_5_01_clw",False,"Chara_comment/110000110","Chara_comment/110000111","110000110_cha","110000111_cha","Chara_icon/110000110","Chara_icon/110000111","Chara_icon_loss/110000110","Chara_icon_loss/110000111","2019/01/01",True,"1", -110000120,10,"アルゴ","束の間の素顔",4,8,4,0,110000120,207000000,-1,110000120,1520,153220,180,18384,75,7660,190,19350,1,0,0,1,0,0,2,0,0,2,0,0,"おそらく数少ない、信頼した相手
のみへと向ける表情なのだろう。
情報屋でも戦士でもない、一個人
としての彼女が記録された一枚。",800,10000,100,10,20,"hlog_110000120_arg_4_01_clw","hlog_110000120_arg_4_01_clw",False,"Chara_comment/110000120","Chara_comment/110000120","110000120_cha","110000120_cha","Chara_icon/110000120","Chara_icon/110000120","Chara_icon_loss/110000120","Chara_icon_loss/110000120","2019/01/01",True,"1", -110000130,10,"アルゴ","万天に通ずる鼠",4,8,1,0,110000130,207000000,-1,110000130,1220,122570,216,22062,60,6130,190,19350,1,0,0,1,0,0,2,0,0,2,0,0,"精緻な情報を扱う立場や過去の言
動から、成熟した人格が伺える彼
女だが、それ以上に謎が多い人物
の一人でもある。",800,10000,100,10,20,"hlog_110000130_arg_4_01_clw","hlog_110000130_arg_4_01_clw",False,"Chara_comment/110000130","Chara_comment/110000130","110000130_cha","110000130_cha","Chara_icon/110000130","Chara_icon/110000130","Chara_icon_loss/110000130","Chara_icon_loss/110000130","2019/01/01",True,"1", -106000140,6,"シリカ","風光る笑み",4,4,3,0,106000140,203000000,-1,106000140,1150,116120,138,13932,60,5805,230,23220,1,0,0,1,0,0,2,0,0,2,0,0,"過酷なSAOの世界を生き抜いた彼
女だが、垣間見せる笑顔は決して
辛い記憶だけではなかったことを
物語る。",800,10000,100,10,20,"hlog_106000140_sil_4_01_dag","hlog_106000140_sil_4_01_dag",False,"Chara_comment/106000140","Chara_comment/106000140","106000140_cha","106000140_cha","Chara_icon/106000140","Chara_icon/106000140","Chara_icon_loss/106000140","Chara_icon_loss/106000140","2019/01/01",True,"1", -104000150,4,"シノン","一矢心髄の青き瞳",4,11,3,0,104000150,211000000,-1,104000150,1150,116120,168,17034,60,5805,210,21290,1,0,0,1,0,0,2,0,0,2,0,0,"孤独な闘いに別れを告げ、仲間と
ともに歩むことを知った彼女の心
には、いまや一分の乱れもない。",800,10000,100,10,20,"hlog_104000150_sin_4_01_bow","hlog_104000150_sin_4_01_bow",False,"Chara_comment/104000150","Chara_comment/104000150","104000150_cha","104000150_cha","Chara_icon/104000150","Chara_icon/104000150","Chara_icon_loss/104000150","Chara_icon_loss/104000150","2019/01/01",True,"1", -110000140,10,"アルゴ","凌刃の双鉤",4,8,3,0,110000140,207000000,-1,110000140,1370,137890,144,14706,70,6895,230,23220,1,0,0,1,0,0,2,0,0,2,0,0,"情報を扱う生業上、戦闘状態に入
る事は多いのだろう。彼女の身ご
なしは、並のプレイヤーを大きく
上回る。",800,10000,100,10,20,"hlog_110000140_arg_4_01_clw","hlog_110000140_arg_4_01_clw",False,"Chara_comment/110000140","Chara_comment/110000140","110000140_cha","110000140_cha","Chara_icon/110000140","Chara_icon/110000140","Chara_icon_loss/110000140","Chara_icon_loss/110000140","2019/01/01",True,"1", -102000160,2,"アスナ","勇猛なる治癒士",5,3,4,0,102000160,202000000,-1,102000160,1620,163300,216,21774,80,8165,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"新たな絆を得たことで、恐れを
捨て、何者にも正面から向き合
えるようになった彼女には、も
はや敵はいない。",1600,20000,100,10,20,"hlog_102000160_asu_5_01_rap","hlog_102000161_asu_5_01_rap",False,"Chara_comment/102000160","Chara_comment/102000161","102000160_cha","102000161_cha","Chara_icon/102000160","Chara_icon/102000161","Chara_icon_loss/102000160","Chara_icon_loss/102000161","2019/01/01",True,"1", -109000140,9,"ユウキ","絆抱く跳躍",5,1,3,0,109000140,201000000,-1,109000140,1620,163300,192,19158,75,7350,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"類稀なる剣技を誇っていた
《絶剣》だが、ALOにおいて新た
に結ばれた絆により、更なる躍
進を遂げた。",1600,20000,100,10,20,"hlog_109000140_yuu_5_01_oha","hlog_109000141_yuu_5_01_oha",False,"Chara_comment/109000140","Chara_comment/109000141","109000140_cha","109000141_cha","Chara_icon/109000140","Chara_icon/109000141","Chara_icon_loss/109000140","Chara_icon_loss/109000141","2019/01/01",True,"1", -104000140,4,"シノン","未来望む笑み",4,14,4,0,104000140,215000000,-1,104000140,1280,129020,210,21288,65,6450,180,17740,1,0,0,1,0,0,2,0,0,2,0,0,"Unknownという未知の世界へ
挑むとしても、彼女の顔が曇る
ことは無い。今や彼女には多く
の仲間がついているのだから。",800,10000,100,10,20,"hlog_104000140_sin_4_01_rif","hlog_104000140_sin_4_01_rif",False,"Chara_comment/104000140","Chara_comment/104000140","104000140_cha","104000140_cha","Chara_icon/104000140","Chara_icon/104000140","Chara_icon_loss/104000140","Chara_icon_loss/104000140","2019/01/01",True,"1", -102000170,2,"アスナ","真夏の眩耀",5,3,4,1,102000170,202000000,-1,102000170,1620,163300,216,21774,80,8165,220,21770,101000,50,0,1,0,0,2,0,0,2,0,0,"海辺を歩く彼女の姿は、誰が見て
も眩く輝いて見えるだろう。彼女
の瞳が何を捉えているのか、それ
を知る術はどこにも無い。",1600,20000,100,10,20,"hlog_102000170_asu_5_01_rap","hlog_102000171_asu_5_01_rap",False,"Chara_comment/102000170","Chara_comment/102000171","102000170_cha","102000171_cha","Chara_icon/102000170","Chara_icon/102000171","Chara_icon_loss/102000170","Chara_icon_loss/102000171","2019/01/01",True,"1", -103000150,3,"リーファ","魅惑の夏日",5,1,4,1,103000150,201000000,-1,103000150,1800,181440,216,21774,70,7260,220,21770,101000,50,0,1,0,0,2,0,0,2,0,0,"身体能力に長けた彼女だが、唯一
水泳だけは苦手意識が強い。抜か
りない準備運動の折、ふと目が
合ったのは偶然か必然か。",1600,20000,100,10,20,"hlog_103000150_lea_5_01_oha","hlog_103000151_lea_5_01_oha",False,"Chara_comment/103000150","Chara_comment/103000151","103000150_cha","103000151_cha","Chara_icon/103000150","Chara_icon/103000151","Chara_icon_loss/103000150","Chara_icon_loss/103000151","2019/01/01",True,"1", -104000160,4,"シノン","砂浜の女神",5,14,4,1,104000160,215000000,-1,104000160,1440,145150,240,23952,70,7260,200,19960,101000,50,0,1,0,0,2,0,0,2,0,0,"通り名さながらにビーチの視線を
集める彼女。目立つことには慣れ
ていない様子だが、彼女なりに海
を楽しんでいるようだ。",1600,20000,100,10,20,"hlog_104000160_sin_5_01_rif","hlog_104000161_sin_5_01_rif",False,"Chara_comment/104000160","Chara_comment/104000161","104000160_cha","104000161_cha","Chara_icon/104000160","Chara_icon/104000161","Chara_icon_loss/104000160","Chara_icon_loss/104000161","2019/01/01",True,"1", -101000160,1,"キリト","らぶりー・さーびす",5,1,1,0,101000160,201000000,-1,101000160,1440,145150,258,26130,70,7260,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"《GGO》のアバターが美少女然と
した姿である故か、男性である
にも関わらず、一部のプレイヤー
から人気を集めているらしい。",1600,20000,100,10,20,"hlog_101000160_kir_5_01_oha","hlog_101000161_kir_5_01_oha",False,"Chara_comment/101000160","Chara_comment/101000161","101000160_cha","101000161_cha","Chara_icon/101000160","Chara_icon/101000161","Chara_icon_loss/101000160","Chara_icon_loss/101000161","2019/01/01",True,"1", -102000180,2,"アスナ","信頼の寛ぎ",5,3,1,0,102000180,202000000,-1,102000180,1300,130640,258,26130,65,6530,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"完璧に思える彼女が、こうして
リラックスした姿を見せるのは
信頼の置ける相手を前にしたとき
だけであろう。",1600,20000,100,10,20,"hlog_102000180_asu_5_01_rap","hlog_102000181_asu_5_01_rap",False,"Chara_comment/102000180","Chara_comment/102000181","102000180_cha","102000181_cha","Chara_icon/102000180","Chara_icon/102000181","Chara_icon_loss/102000180","Chara_icon_loss/102000181","2019/01/01",True,"1", -101000150,1,"キリト","冒険への誘い",4,1,4,0,101000150,201000000,-1,101000150,1600,161280,192,19356,80,8065,160,16130,1,0,0,1,0,0,2,0,0,2,0,0,"未知の世界であるUnknownへと
足を踏み入れるにあたっても、彼
が怯むことは無い。新たなる冒険
の予感に、自然と笑みが浮かぶ。",800,10000,100,10,20,"hlog_101000150_kir_4_01_oha","hlog_101000150_kir_4_01_oha",False,"Chara_comment/101000150","Chara_comment/101000150","101000150_cha","101000150_cha","Chara_icon/101000150","Chara_icon/101000150","Chara_icon_loss/101000150","Chara_icon_loss/101000150","2019/01/01",True,"1", -103000140,3,"リーファ","胸躍る旅立ち",4,1,3,0,103000140,201000000,-1,103000140,1440,145150,156,15480,60,5805,230,23220,1,0,0,1,0,0,2,0,0,2,0,0,"新たな世界への旅立ちは、誰しも
心躍るものだ。彼女もまた仲間た
ちとの冒険を、新たな出会いを、
楽しみにしていることだろう。",800,10000,100,10,20,"hlog_103000140_lea_4_01_oha","hlog_103000140_lea_4_01_oha",False,"Chara_comment/103000140","Chara_comment/103000140","103000140_cha","103000140_cha","Chara_icon/103000140","Chara_icon/103000140","Chara_icon_loss/103000140","Chara_icon_loss/103000140","2019/01/01",True,"1", -102000150,2,"アスナ","共に歩む光",4,3,4,0,102000150,202000000,-1,102000150,1440,145150,192,19356,70,7260,190,19350,1,0,0,1,0,0,2,0,0,2,0,0,"彼女の真価は、その統率力と思い
やりによって、人々の能力を引き
出せることにある。この冒険にも
遺憾なく発揮されることだろう。",800,10000,100,10,20,"hlog_102000150_asu_4_01_rap","hlog_102000150_asu_4_01_rap",False,"Chara_comment/102000150","Chara_comment/102000150","102000150_cha","102000150_cha","Chara_icon/102000150","Chara_icon/102000150","Chara_icon_loss/102000150","Chara_icon_loss/102000150","2021/10/30 7:00:00",True,"1", -106000150,6,"シリカ","愛しの竜使い",5,4,1,0,106000150,203000000,-1,106000150,1150,116120,234,23514,60,5805,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"《SAO》中層ではアイドル的人気
を誇っていたという彼女。珍しい
竜使いだったせいでもあるが一番
の所以はこの可憐な笑顔だろう。",1600,20000,100,10,20,"hlog_106000150_sil_5_01_dag","hlog_106000151_sil_5_01_dag",False,"Chara_comment/106000150","Chara_comment/106000151","106000150_cha","106000151_cha","Chara_icon/106000150","Chara_icon/106000151","Chara_icon_loss/106000150","Chara_icon_loss/106000151","2019/01/01",True,"1", -101000170,1,"キリト","SAOゲーム攻略会議2019",5,2,4,0,101000170,201000000,201000000,101000170,1800,181440,216,21774,90,9070,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"《SAO》で《ビーター》と呼ばれ
孤高を貫いた彼も、今では仲間に
囲まれ屈託ない笑顔を見せる。次
の攻略も上手く行くに違いない。",1600,20000,100,10,20,"hlog_101000170_kir_5_01_nit","hlog_101000171_kir_5_01_nit",False,"Chara_comment/101000170","Chara_comment/101000171","101000170_cha","101000171_cha","Chara_icon/101000170","Chara_icon/101000171","Chara_icon_loss/101000170","Chara_icon_loss/101000171","2019/01/01",True,"1", -101000180,1,"キリト","かの世界を生きた者",5,2,2,0,101000180,201000000,201000000,101000180,1980,199580,174,17418,100,9980,140,14520,1,0,0,1,0,0,2,0,0,2,0,0,"《SAO》をクリアへと導いた英雄
は、かの浮遊城で現実を生きた者
だった。剣を振るう時も、友と笑
い合う時も、彼は直向きだった。",1600,20000,100,10,20,"hlog_101000180_kir_5_01_nit","hlog_101000181_kir_5_01_nit",False,"Chara_comment/101000180","Chara_comment/101000181","101000180_cha","101000181_cha","Chara_icon/101000180","Chara_icon/101000181","Chara_icon_loss/101000180","Chara_icon_loss/101000181","2019/01/01",True,"1", -104000170,4,"シノン","闘輝の氷弾",3,14,1,0,104000170,215000000,-1,104000170,900,90320,222,22356,45,4515,150,15520,1,0,0,1,0,0,2,0,0,99999,0,0,"女性でソロスナイパーのシノンは
GGOでは珍しい存在である。
その実力も相まって、有名
プレイヤーの一員と言えよう。",400,6000,100,10,20,"hlog_104000170_sin_3_01_rif","hlog_104000170_sin_3_01_rif",False,"Chara_comment/104000170","Chara_comment/104000170","104000170_cha","104000170_cha","Chara_icon/104000170","Chara_icon/104000170","Chara_icon_loss/104000170","Chara_icon_loss/104000170","2019/01/01",True,"1", -104000180,4,"シノン","戦慧の狙撃手",4,14,1,0,104000180,215000000,-1,104000180,1020,103220,252,25548,50,5160,180,17740,1,0,0,1,0,0,2,0,0,2,0,0,"GGO個人戦の最高峰を決める大会
《BoB》において狙撃手が好成績
を収めるのは珍しい。それも彼女
の冷静な判断力あってのことだ。",800,10000,100,10,20,"hlog_104000180_sin_4_01_rif","hlog_104000180_sin_4_01_rif",False,"Chara_comment/104000180","Chara_comment/104000180","104000180_cha","104000180_cha","Chara_icon/104000180","Chara_icon/104000180","Chara_icon_loss/104000180","Chara_icon_loss/104000180","2019/01/01",True,"1", -104000190,4,"シノン","静謐なる覚悟",5,14,1,0,104000190,215000000,-1,104000190,1150,116120,288,28740,60,5805,200,19960,1,0,0,1,0,0,2,0,0,2,0,0,"かつては悲痛なまでの覚悟を胸に
銃を手にしていた彼女だが、今や
その心は穏やかに、静かに標的を
見定めている。",1600,20000,100,10,20,"hlog_104000190_sin_5_01_rif","hlog_104000191_sin_5_01_rif",False,"Chara_comment/104000190","Chara_comment/104000191","104000190_cha","104000191_cha","Chara_icon/104000190","Chara_icon/104000191","Chara_icon_loss/104000190","Chara_icon_loss/104000191","2019/01/01",True,"1", -103000160,3,"リーファ","一陣の疾風",5,1,1,0,103000160,201000000,-1,103000160,1440,145150,258,26130,60,5805,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"現実世界で培った剣技と、風妖精
の瞬発力を併せ持つ彼女の
鋭い剣さばきについてこられる
プレイヤーは数少ないだろう。",1600,20000,100,10,20,"hlog_103000160_lea_5_01_oha","hlog_103000161_lea_5_01_oha",False,"Chara_comment/103000160","Chara_comment/103000161","103000160_cha","103000161_cha","Chara_icon/103000160","Chara_icon/103000161","Chara_icon_loss/103000160","Chara_icon_loss/103000161","2019/01/01",True,"1", -105000140,5,"リズベット","昵懇の語らい",5,10,1,0,105000140,209000000,-1,105000140,1440,145150,234,23514,85,8710,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"親しき友人に彼女が見せる屈託の
ない笑顔は、見る者を励ましてく
れる。そんな彼女も、本心を明か
す時は穏やかに微笑んで見せる。",1600,20000,100,10,20,"hlog_105000140_lis_5_01_mac","hlog_105000141_lis_5_01_mac",False,"Chara_comment/105000140","Chara_comment/105000141","105000140_cha","105000141_cha","Chara_icon/105000140","Chara_icon/105000141","Chara_icon_loss/105000140","Chara_icon_loss/105000141","2019/01/01",True,"1", -102000190,2,"アスナ","生彩放つ飛躍",5,1,3,0,102000190,201000000,-1,102000190,1460,146970,174,17418,75,7350,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"自分の殻を破った彼女は、かけが
えない少女を思わせる色彩に身を
包み、どこまでも飛躍すること
だろう。",1600,20000,100,10,20,"hlog_102000190_asu_5_01_oha","hlog_102000191_asu_5_01_oha",False,"Chara_comment/102000190","Chara_comment/102000191","102000190_cha","102000191_cha","Chara_icon/102000190","Chara_icon/102000191","Chara_icon_loss/102000190","Chara_icon_loss/102000191","2019/01/01",True,"1", -109000150,9,"ユウキ","純真な想望",5,3,1,0,109000150,202000000,-1,109000150,1440,145150,288,28740,65,6530,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"常にない明るい色彩の衣装は、
大切な彼女を思わせる。
2人の絆が窺える装いに、明るい
希望がその胸を満たしてゆく。",1600,20000,100,10,20,"hlog_109000150_yuu_5_01_rap","hlog_109000151_yuu_5_01_rap",False,"Chara_comment/109000150","Chara_comment/109000151","109000150_cha","109000151_cha","Chara_icon/109000150","Chara_icon/109000151","Chara_icon_loss/109000150","Chara_icon_loss/109000151","2019/01/01",True,"1", -101000190,1,"キリト","想い馳せる窓辺",5,1,4,0,101000190,201000000,-1,101000190,1800,181440,216,21774,90,9070,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"繊細で優しい浮遊城の英雄は、
ふとした瞬間に彼の手の届かな
かった人々に想いを馳せ、
その瞳を静かに伏せる。",1600,20000,100,10,20,"hlog_101000190_kir_5_01_oha","hlog_101000191_kir_5_01_oha",False,"Chara_comment/101000190","Chara_comment/101000191","101000190_cha","101000191_cha","Chara_icon/101000190","Chara_icon/101000191","Chara_icon_loss/101000190","Chara_icon_loss/101000191","2019/01/01",True,"1", -110000150,10,"アルゴ","悪戯なオネーサン",5,8,4,0,110000150,207000000,-1,110000150,1710,172370,204,20682,85,8620,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"人に容易く踏み入らない彼女だが
ちょっかいをかけるのは別だ。
気に入った相手をからかっては
するりと離れて行ってしまう。",1600,20000,100,10,20,"hlog_110000150_arg_5_01_clw","hlog_110000151_arg_5_01_clw",False,"Chara_comment/110000150","Chara_comment/110000151","110000150_cha","110000151_cha","Chara_icon/110000150","Chara_icon/110000151","Chara_icon_loss/110000150","Chara_icon_loss/110000151","2019/01/01",True,"1", -111000010,11,"アリス","堅牢な意志",3,1,1,0,111000010,201000000,-1,111000010,1290,129830,210,21336,65,6490,130,13410,1,0,0,1,0,0,2,0,0,99999,0,0,"公理教会に所属する第三十番目の
整合騎士。剣にも神聖術にも天才
的な才能を発揮し、騎士長に次ぐ
実力と噂される。",400,6000,100,10,20,"hlog_111000010_alc_3_01_xxx","hlog_111000010_alc_3_01_xxx",False,"Chara_comment/111000010","Chara_comment/111000010","111000010_cha","111000010_cha","Chara_icon/111000010","Chara_icon/111000010","Chara_icon_loss/111000010","Chara_icon_loss/111000010","2019/01/01",True,"1", -111000020,11,"アリス","煌金の整合騎士",3,1,3,0,111000020,201000000,-1,111000020,1450,146060,144,14226,70,7305,160,16090,1,0,0,1,0,0,2,0,0,99999,0,0,"公理教会に所属する第三十番目の
整合騎士。剣にも神聖術にも天才
的な才能を発揮し、騎士長に次ぐ
実力と噂される。",400,6000,100,10,20,"hlog_111000020_alc_3_01_xxx","hlog_111000020_alc_3_01_xxx",False,"Chara_comment/111000020","Chara_comment/111000020","111000020_cha","111000020_cha","Chara_icon/111000020","Chara_icon/111000020","Chara_icon_loss/111000020","Chara_icon_loss/111000020","2019/01/01",True,"1", -111000030,11,"アリス","人界の守り手",3,1,4,0,111000030,201000000,-1,111000030,1610,162290,174,17784,80,8115,130,13410,1,0,0,1,0,0,2,0,0,99999,0,0,"公理教会に所属する第三十番目の
整合騎士。剣にも神聖術にも天才
的な才能を発揮し、騎士長に次ぐ
実力と噂される。",400,6000,100,10,20,"hlog_111000030_alc_3_01_xxx","hlog_111000030_alc_3_01_xxx",False,"Chara_comment/111000030","Chara_comment/111000030","111000030_cha","111000030_cha","Chara_icon/111000030","Chara_icon/111000030","Chara_icon_loss/111000030","Chara_icon_loss/111000030","2019/01/01",True,"1", -111000040,11,"アリス","黄金の英剣",4,1,2,0,111000040,201000000,-1,111000040,2020,204020,162,16260,100,10200,120,12260,1,0,0,1,0,0,2,0,0,2,0,0,"神器《金木犀の剣》は、アリスが
公理教会最高司祭より賜った物。
《永劫不朽》の性質を持つこの剣
は何物にも負けぬ強靭さを持つ。",800,10000,100,10,20,"hlog_111000040_alc_4_01_xxx","hlog_111000040_alc_4_01_xxx",False,"Chara_comment/111000040","Chara_comment/111000040","111000040_cha","111000040_cha","Chara_icon/111000040","Chara_icon/111000040","Chara_icon_loss/111000040","Chara_icon_loss/111000040","2019/01/01",True,"1", -111000050,11,"アリス","気高き騎士",4,1,1,0,111000050,201000000,-1,111000050,1470,148380,240,24384,75,7420,150,15320,1,0,0,1,0,0,2,0,0,2,0,0,"整合騎士である彼女は、常に高潔
な精神を手放さない。人界のため
全ての民のため、その剣を振るい
続けるであろう。",800,10000,100,10,20,"hlog_111000050_alc_4_01_xxx","hlog_111000050_alc_4_01_xxx",False,"Chara_comment/111000050","Chara_comment/111000050","111000050_cha","111000050_cha","Chara_icon/111000050","Chara_icon/111000050","Chara_icon_loss/111000050","Chara_icon_loss/111000050","2019/01/01",True,"1", -111000060,11,"アリス","金木犀香る少女",5,1,4,0,111000060,201000000,-1,111000060,2070,208660,228,22860,105,10435,170,17240,1,0,0,1,0,0,2,0,0,2,0,0,"整合騎士は歳をとることはない。
彼女もまた例外ではなく、いつま
でも整合騎士となったその日のま
ま、可憐な少女であり続ける。",1600,20000,100,10,20,"hlog_111000060_alc_5_01_xxx","hlog_111000061_alc_5_01_xxx",False,"Chara_comment/111000060","Chara_comment/111000061","111000060_cha","111000061_cha","Chara_icon/111000060","Chara_icon/111000061","Chara_icon_loss/111000060","Chara_icon_loss/111000061","2019/01/01",True,"1", -111000070,11,"アリス","心安らぐひと時",4,1,4,0,111000070,201000000,-1,111000070,1840,185470,204,20322,90,9275,150,15320,1,0,0,1,0,0,2,0,0,2,0,0,"普段は騎士然とし、凛とした態度
を崩さない彼女だが、親しき者の
前ではこうして柔和な表情を見せ
ることもある。",800,10000,100,10,20,"hlog_111000070_alc_4_01_xxx","hlog_111000070_alc_4_01_xxx",False,"Chara_comment/111000070","Chara_comment/111000070","111000070_cha","111000070_cha","Chara_icon/111000070","Chara_icon/111000070","Chara_icon_loss/111000070","Chara_icon_loss/111000070","2019/01/01",True,"1", -111000080,11,"アリス","心通う邂逅",5,1,1,0,111000080,201000000,-1,111000080,1660,166920,270,27432,85,8345,170,17240,1,0,0,1,0,0,2,0,0,2,0,0,"彼女の愛竜《雨縁》は、彼女に
とってかけがえない存在だった。
行方知れずとなった今、相棒を想
う彼女の瞳には憂いが宿る。",1600,20000,100,10,20,"hlog_111000080_alc_5_01_xxx","hlog_111000081_alc_5_01_xxx",False,"Chara_comment/111000080","Chara_comment/111000081","111000080_cha","111000081_cha","Chara_icon/111000080","Chara_icon/111000081","Chara_icon_loss/111000080","Chara_icon_loss/111000081","2019/01/01",True,"1", -112000010,12,"ユージオ","希求の若芽",3,9,4,0,112000010,208000000,-1,112000010,1400,141120,168,16932,70,7055,140,14110,1,0,0,1,0,0,2,0,0,99999,0,0,"ノーランガルス帝立修剣学校に所
属する上級修剣士。元はルーリッ
ド村の《刻み手》であったが、天
職を全うし《剣士》に転身した。",400,6000,100,10,20,"hlog_112000010_ego_3_01_xxx","hlog_112000010_ego_3_01_xxx",False,"Chara_comment/112000010","Chara_comment/112000010","112000010_cha","112000010_cha","Chara_icon/112000010","Chara_icon/112000010","Chara_icon_loss/112000010","Chara_icon_loss/112000010","2019/01/01",True,"1", -112000020,12,"ユージオ","氷銀の整合騎士",3,1,1,0,112000020,201000000,-1,112000020,1120,112900,204,20322,55,5645,140,14110,1,0,0,1,0,0,2,0,0,99999,0,0,"整合騎士となったユージオの姿。
その経緯は不明ながら、強力な神
器《青薔薇の剣》を意のままに操
るその実力は折り紙付きだろう。",400,6000,100,10,20,"hlog_112000020_ego_3_01_xxx","hlog_112000020_ego_3_01_xxx",False,"Chara_comment/112000020","Chara_comment/112000020","112000020_cha","112000020_cha","Chara_icon/112000020","Chara_icon/112000020","Chara_icon_loss/112000020","Chara_icon_loss/112000020","2019/01/01",True,"1", -112000030,12,"ユージオ","精勤す修剣士",3,1,4,0,112000030,201000000,-1,112000030,1400,141120,168,16932,70,7055,140,14110,1,0,0,1,0,0,2,0,0,99999,0,0,"ノーランガルス帝立修剣学校に所
属する上級修剣士。元はルーリッ
ド村の《刻み手》であったが、天
職を全うし《剣士》に転身した。",400,6000,100,10,20,"hlog_112000030_ego_3_01_xxx","hlog_112000030_ego_3_01_xxx",False,"Chara_comment/112000030","Chara_comment/112000030","112000030_cha","112000030_cha","Chara_icon/112000030","Chara_icon/112000030","Chara_icon_loss/112000030","Chara_icon_loss/112000030","2019/01/01",True,"1", -112000040,12,"ユージオ","青銀の英剣",4,1,4,0,112000040,201000000,-1,112000040,1600,161280,192,19356,80,8065,160,16130,1,0,0,1,0,0,2,0,0,2,0,0,"整合騎士となる前に記憶を操作さ
れたのか、この記録における彼が
笑顔を見せることは無い。ただ
淡々と敵を屠るのみである。",800,10000,100,10,20,"hlog_112000040_ego_4_01_xxx","hlog_112000040_ego_4_01_xxx",False,"Chara_comment/112000040","Chara_comment/112000040","112000040_cha","112000040_cha","Chara_icon/112000040","Chara_icon/112000040","Chara_icon_loss/112000040","Chara_icon_loss/112000040","2019/01/01",True,"1", -112000050,12,"ユージオ","安息日の朗笑",4,1,1,0,112000050,201000000,-1,112000050,1280,129020,228,23226,65,6450,160,16130,1,0,0,1,0,0,2,0,0,2,0,0,"努力家な彼は、生まれによる経験
の浅さを勤勉さによってカバーし
てきた。そんな彼も安息日には剣
を手放し穏やかな笑みを見せる。
",800,10000,100,10,20,"hlog_112000050_ego_4_01_xxx","hlog_112000050_ego_4_01_xxx",False,"Chara_comment/112000050","Chara_comment/112000050","112000050_cha","112000050_cha","Chara_icon/112000050","Chara_icon/112000050","Chara_icon_loss/112000050","Chara_icon_loss/112000050","2019/01/01",True,"1", -112000060,12,"ユージオ","飢愛の繰人形",5,1,3,0,112000060,201000000,-1,112000060,1620,163300,174,17418,80,8165,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"大切なものを手放した彼の目に映
るのは虚構の愛ばかりだった。
どこまでも空虚な心を埋めるよう
にその頬を涙が伝う。",1600,20000,100,10,20,"hlog_112000060_ego_5_01_xxx","hlog_112000061_ego_5_01_xxx",False,"Chara_comment/112000060","Chara_comment/112000061","112000060_cha","112000061_cha","Chara_icon/112000060","Chara_icon/112000061","Chara_icon_loss/112000060","Chara_icon_loss/112000061","2019/01/01",True,"1", -112000070,12,"ユージオ","安らぎの素顔",4,1,4,0,112000070,201000000,-1,112000070,1600,161280,192,19356,80,8065,160,16130,1,0,0,1,0,0,2,0,0,2,0,0,"Unknownへ来た後の彼の、無防備
な寝顔を記録した一枚。こうした
一面を見せるのは、彼がこちらの
世界へ慣れてきた証拠だろうか。",800,10000,100,10,20,"hlog_112000070_ego_4_01_xxx","hlog_112000070_ego_4_01_xxx",False,"Chara_comment/112000070","Chara_comment/112000070","112000070_cha","112000070_cha","Chara_icon/112000070","Chara_icon/112000070","Chara_icon_loss/112000070","Chara_icon_loss/112000070","2019/01/01",True,"1", -112000080,12,"ユージオ","かの剣技の継承者",5,2,1,0,112000080,201000000,201000000,112000080,1440,145150,258,26130,70,7260,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"《アインクラッド流》剣術の使い
手である彼は、キリトに師事を仰
ぐ。《二刀流》への挑戦は、無茶
か無謀か、それとも――?",1600,20000,100,10,20,"hlog_112000080_ego_5_01_xxx","hlog_112000081_ego_5_01_xxx",False,"Chara_comment/112000080","Chara_comment/112000081","112000080_cha","112000081_cha","Chara_icon/112000080","Chara_icon/112000081","Chara_icon_loss/112000080","Chara_icon_loss/112000081","2019/01/01",True,"1", -101000200,1,"キリト","修剣の異端児",5,1,4,0,101000200,201000000,-1,101000200,1800,181440,216,21774,90,9070,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"あちらの世界において上級修剣士
となったキリトの姿。持ち前の発
想力は剣の上達以外にも遺憾なく
発揮され相棒を振り回している。",1600,20000,100,10,20,"hlog_101000200_kir_5_01_xxx","hlog_101000201_kir_5_01_xxx",False,"Chara_comment/101000200","Chara_comment/101000201","101000200_cha","101000201_cha","Chara_icon/101000200","Chara_icon/101000201","Chara_icon_loss/101000200","Chara_icon_loss/101000201","2019/01/01",True,"1", -109000160,9,"ユウキ","紅葉の祝福",5,4,1,0,109000160,203000000,-1,109000160,1440,145150,288,28740,65,6530,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"Unknownにおいて紅葉狩りに訪れ
た際の一枚。降り注ぐ紅葉はまる
で祝福のように彼女を彩るが、本
人は美味しいお弁当に夢中。",1600,20000,100,10,20,"hlog_109000160_yuu_5_01_xxx","hlog_109000161_yuu_5_01_xxx",False,"Chara_comment/109000160","Chara_comment/109000161","109000160_cha","109000161_cha","Chara_icon/109000160","Chara_icon/109000161","Chara_icon_loss/109000160","Chara_icon_loss/109000161","2019/01/01",True,"1", -102000200,2,"アスナ","慈愛の光彩",4,10,3,0,102000200,209000000,-1,102000200,1300,130640,156,15480,65,6530,230,23220,1,0,0,1,0,0,2,0,0,2,0,0,"紅葉の下、ピクニックともなれば
料理上手の彼女の腕が鳴る。
思い出のサンドイッチには愛情の
隠し味を添えて。",800,10000,100,10,20,"hlog_102000200_asu_4_01_xxx","hlog_102000200_asu_4_01_xxx",False,"Chara_comment/102000200","Chara_comment/102000200","102000200_cha","102000200_cha","Chara_icon/102000200","Chara_icon/102000200","Chara_icon_loss/102000200","Chara_icon_loss/102000200","2019/01/01",True,"1", -105000150,5,"リズベット","快活なお誘い",4,11,3,0,105000150,211000000,-1,105000150,1440,145150,138,13932,85,8710,190,19350,1,0,0,1,0,0,2,0,0,2,0,0,"紅葉狩りにピッタリのこの季節、
快活な彼女が指し示すは赤々と色
付いた美しい山々。ほら、早くし
ないと置いてっちゃうわよ!",800,10000,100,10,20,"hlog_105000150_lis_4_01_xxx","hlog_105000150_lis_4_01_xxx",False,"Chara_comment/105000150","Chara_comment/105000150","105000150_cha","105000150_cha","Chara_icon/105000150","Chara_icon/105000150","Chara_icon_loss/105000150","Chara_icon_loss/105000150","2019/01/01",True,"1", -106000160,6,"シリカ","至福のひと時",4,8,4,0,106000160,207000000,-1,106000160,1280,129020,174,17418,65,6450,190,19350,1,0,0,1,0,0,2,0,0,2,0,0,"紅葉の下、みんなで持ち寄ったお
弁当を並べればちょっとしたパー
ティの始まり。思い出のチーズ
ケーキを手に、笑みがこぼれる。",800,10000,100,10,20,"hlog_106000160_sil_4_01_xxx","hlog_106000160_sil_4_01_xxx",False,"Chara_comment/106000160","Chara_comment/106000160","106000160_cha","106000160_cha","Chara_icon/106000160","Chara_icon/106000160","Chara_icon_loss/106000160","Chara_icon_loss/106000160","2019/01/01",True,"1", -103000170,3,"リーファ","恥じらい乙女",5,6,1,0,103000170,205000000,-1,103000170,1440,145150,258,26130,60,5805,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"《ALO》では妖精然とした姿の彼
女も、現実世界では思春期只中の
少女に違いない。気になるヒトの
前で上気する頬を隠し切れない。",1600,20000,100,10,20,"hlog_103000170_lea_5_01_xxx","hlog_103000171_lea_5_01_xxx",False,"Chara_comment/103000170","Chara_comment/103000171","103000170_cha","103000171_cha","Chara_icon/103000170","Chara_icon/103000171","Chara_icon_loss/103000170","Chara_icon_loss/103000171","2019/01/01",True,"1", -103000180,3,"リーファ","月夜にかける想い",5,6,3,0,103000180,205000000,-1,103000180,1620,163300,174,17418,65,6530,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"月光を浴びて浮遊する彼女の顔に
は隠し切れない憂いがにじむ。
月を見上げて胸を詰まらせる、
その想いは誰がため。",1600,20000,100,10,20,"hlog_103000180_lea_5_01_xxx","hlog_103000181_lea_5_01_xxx",False,"Chara_comment/103000180","Chara_comment/103000181","103000180_cha","103000181_cha","Chara_icon/103000180","Chara_icon/103000181","Chara_icon_loss/103000180","Chara_icon_loss/103000181","2019/01/01",True,"1", -105000160,5,"リズベット","信頼の口付け",5,10,4,0,105000160,209000000,-1,105000160,1800,181440,192,19596,110,10885,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"根が真面目な彼女だからこそ、
その口付けには自身の武器への、
そして共に戦うあなたへの信頼が
にじむ。",1600,20000,100,10,20,"hlog_105000160_lis_5_01_xxx","hlog_105000161_lis_5_01_xxx",False,"Chara_comment/105000160","Chara_comment/105000161","105000160_cha","105000161_cha","Chara_icon/105000160","Chara_icon/105000161","Chara_icon_loss/105000160","Chara_icon_loss/105000161","2019/01/01",True,"1", -111000090,11,"アリス","光明もたらす者",5,1,1,0,111000090,201000000,-1,111000090,1660,166920,270,27432,85,8345,170,17240,1,0,0,1,0,0,2,0,0,2,0,0,"強靭な意志の力を宿す高潔な彼女
の精神は、やがて己の呪縛を解き
放つに至る。痛みと共に胸に溢れ
るのは確かな温かさだった。",1600,20000,100,10,20,"hlog_111000090_alc_5_01_xxx","hlog_111000091_alc_5_01_xxx",False,"Chara_comment/111000090","Chara_comment/111000091","111000090_cha","111000091_cha","Chara_icon/111000090","Chara_icon/111000091","Chara_icon_loss/111000090","Chara_icon_loss/111000091","2019/01/01",False,"1", -104000200,4,"シノン","湯浴みの紅潮",5,11,4,2,104000200,211000000,-1,104000200,1440,145150,240,23952,70,7260,200,19960,100900,50,0,101000,50,0,2,0,0,2,0,0,"温泉で身も心も温まり、リラック
スした様子の彼女。警戒心の強い
彼女の表情もここではゆるりと
綻ぶことだろう。",1600,20000,100,10,20,"hlog_104000200_sin_5_01_xxx","hlog_104000201_sin_5_01_xxx",False,"Chara_comment/104000200","Chara_comment/104000201","104000200_cha","104000201_cha","Chara_icon/104000200","Chara_icon/104000201","Chara_icon_loss/104000200","Chara_icon_loss/104000201","2019/01/01",True,"1", -106000170,6,"シリカ","秘湯の恥じらい",5,4,4,2,106000170,203000000,-1,106000170,1440,145150,192,19596,70,7260,220,21770,100900,50,0,101000,50,0,2,0,0,2,0,0,"秘湯に辿り着いた彼女。温かな
温泉に思わず気が緩んだのか、何
やらハプニングの様子?はわわ、
み、見ないでください~~っ!",1600,20000,100,10,20,"hlog_106000170_sil_5_01_xxx","hlog_106000171_sil_5_01_xxx",False,"Chara_comment/106000170","Chara_comment/106000171","106000170_cha","106000171_cha","Chara_icon/106000170","Chara_icon/106000171","Chara_icon_loss/106000170","Chara_icon_loss/106000171","2019/01/01",True,"1", -111000100,11,"アリス","火照る温浴",5,1,4,2,111000100,201000000,-1,111000100,2070,208660,228,22860,105,10435,170,17240,100900,50,0,101000,50,0,2,0,0,2,0,0,"初体験の東洋式大浴場で、身体の
芯から温まった彼女。普段の凛と
した佇まいとは裏腹に、ここでは
安らいだ表情が窺える。",1600,20000,100,10,20,"hlog_111000100_alc_5_01_xxx","hlog_111000101_alc_5_01_xxx",False,"Chara_comment/111000100","Chara_comment/111000101","111000100_cha","111000101_cha","Chara_icon/111000100","Chara_icon/111000101","Chara_icon_loss/111000100","Chara_icon_loss/111000101","2019/01/01",True,"1", -102000210,2,"アスナ","愛の誓い",5,3,3,0,102000210,202000000,-1,102000210,1460,146970,174,17418,75,7350,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"彼女が過酷なアインクラッドを
戦い抜く原動力となったのは、
深く強い愛情だった。誓いの指輪
に想いを乗せ、彼女は前を向く。",1600,20000,100,10,20,"hlog_102000210_asu_5_01_xxx","hlog_102000211_asu_5_01_xxx",False,"Chara_comment/102000210","Chara_comment/102000211","102000210_cha","102000211_cha","Chara_icon/102000210","Chara_icon/102000211","Chara_icon_loss/102000210","Chara_icon_loss/102000211","2019/01/01",True,"1", -109000170,9,"ユウキ","揺蕩う夢幻",5,1,1,0,109000170,201000000,-1,109000170,1440,145150,288,28740,65,6530,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"笑顔を絶やさない彼女だが、その
明るさとは裏腹な儚さも併せ
持つ。微睡の中、彼女はどんな夢
を見るのだろう。",1600,20000,100,10,20,"hlog_109000170_yuu_5_01_xxx","hlog_109000171_yuu_5_01_xxx",False,"Chara_comment/109000170","Chara_comment/109000171","109000170_cha","109000171_cha","Chara_icon/109000170","Chara_icon/109000171","Chara_icon_loss/109000170","Chara_icon_loss/109000171","2019/01/01",True,"1", -108000130,8,"エギル","雄大豪壮",5,9,1,0,108000130,208000000,-1,108000130,1730,174180,288,28740,80,7985,160,16330,1,0,0,1,0,0,2,0,0,2,0,0,"大きな体躯と冷静な判断力、懐の
深さを併せ持つ彼は、攻略組の中
でも一目置かれる存在であった。",1600,20000,100,10,20,"hlog_108000130_agi_5_01_xxx","hlog_108000131_agi_5_01_xxx",False,"Chara_comment/108000130","Chara_comment/108000131","108000130_cha","108000131_cha","Chara_icon/108000130","Chara_icon/108000131","Chara_icon_loss/108000130","Chara_icon_loss/108000131","2019/01/01",True,"1", -104000210,4,"シノン","いじっぱりスナイプ",5,14,4,0,104000210,215000000,-1,104000210,1440,145150,240,23952,70,7260,200,19960,1,0,0,1,0,0,2,0,0,2,0,0,"狙撃手として、戦闘中は常に平静
を欠かない彼女だが、親しい友の
前ではペースを乱されることも。
",1600,20000,100,10,20,"hlog_104000210_sin_5_01_xxx","hlog_104000211_sin_5_01_xxx",False,"Chara_comment/104000210","Chara_comment/104000211","104000210_cha","104000211_cha","Chara_icon/104000210","Chara_icon/104000211","Chara_icon_loss/104000210","Chara_icon_loss/104000211","2019/01/01",True,"1", -101000210,1,"キリト","超越する意志力",5,2,1,0,101000210,201000000,201000001,101000210,1440,145150,258,26130,70,7260,180,18140,107000,0,0,1,0,0,2,0,0,2,0,0,"SAOをクリアへと導いたのは
鍛え上げられたステータスでも
特別なスキルでもなく、システム
をも超越する意志の力であった。",1600,20000,100,10,20,"hlog_101000210_kir_5_01_xxx","hlog_101000211_kir_5_01_xxx",False,"Chara_comment/101000210","Chara_comment/101000211","101000210_cha","101000211_cha","Chara_icon/101000210","Chara_icon/101000211","Chara_icon_loss/101000210","Chara_icon_loss/101000211","2019/01/01",True,"1", -102000220,2,"アスナ","奇跡績む想愛",5,3,4,0,102000220,202000000,-1,102000220,1620,163300,216,21774,80,8165,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"麻痺ステータスは、本来アイテム
か時間経過でしか解除されないが
彼女の愛情はシステムの壁をも
破り、想い人の命を救った。",1600,20000,100,10,20,"hlog_102000220_asu_5_01_xxx","hlog_102000221_asu_5_01_xxx",False,"Chara_comment/102000220","Chara_comment/102000221","102000220_cha","102000221_cha","Chara_icon/102000220","Chara_icon/102000221","Chara_icon_loss/102000220","Chara_icon_loss/102000221","2019/01/01",True,"1", -107000130,7,"クライン","緊褌の抜刀",5,6,1,0,107000130,205000000,-1,107000130,1580,159670,288,28740,70,7260,140,14520,1,0,0,1,0,0,2,0,0,2,0,0,"普段は飄々とした彼だが、勝負と
なると一変する。ギルドメンバー
の命も背負い、抜刀するその眼差
しは真剣そのものだ。",1600,20000,100,10,20,"hlog_107000130_kle_5_01_xxx","hlog_107000131_kle_5_01_xxx",False,"Chara_comment/107000130","Chara_comment/107000131","107000130_cha","107000131_cha","Chara_icon/107000130","Chara_icon/107000131","Chara_icon_loss/107000130","Chara_icon_loss/107000131","2019/01/01",True,"1", -101000220,1,"キリト","不屈の誓い",5,2,1,0,101000220,201000000,201000000,101000220,1440,145150,258,26130,70,7260,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"一度は前線から退いたものの、
彼を取り巻く運命は休息を許さな
い。必ずこの世界を終わらせると
誓い、彼は再び剣を手にする。",1600,20000,100,10,20,"hlog_101000220_kir_5_01_xxx","hlog_101000221_kir_5_01_xxx",False,"Chara_comment/101000220","Chara_comment/101000221","101000220_cha","101000221_cha","Chara_icon/101000220","Chara_icon/101000221","Chara_icon_loss/101000220","Chara_icon_loss/101000221","2020/03/10 7:00:00",True,"1", -102000230,2,"アスナ","明日への誓い",5,3,1,0,102000230,202000000,-1,102000230,1300,130640,258,26130,65,6530,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"前線から退いた幸福な日々の中で
も、彼女の脳裏には一抹の不安が
あった。共に未来を歩むため、
彼女は再び立ち上がる。",1600,20000,100,10,20,"hlog_102000230_asu_5_01_xxx","hlog_102000231_asu_5_01_xxx",False,"Chara_comment/102000230","Chara_comment/102000231","102000230_cha","102000231_cha","Chara_icon/102000230","Chara_icon/102000231","Chara_icon_loss/102000230","Chara_icon_loss/102000231","2020/03/10 7:00:00",True,"1", -106000180,6,"シリカ","宙吊りプラント",5,4,2,0,106000180,203000000,-1,106000180,1580,159670,156,15678,80,7985,170,17420,1,0,0,1,0,0,2,0,0,2,0,0,"植物系モンスターとの対面は、
彼女にとってはトラウマだ。蔓に
よる攻撃と、彼女の衣装との相性
は、最悪と言って良い。",1600,20000,100,10,20,"hlog_106000180_sil_5_01_xxx","hlog_106000181_sil_5_01_xxx",True,"Chara_comment/106000180","Chara_comment/106000181","106000180_cha","106000181_cha","Chara_icon/106000180","Chara_icon/106000181","Chara_icon_loss/106000180","Chara_icon_loss/106000181","2019/01/01",True,"1", -109000180,9,"ユウキ","鋭敏なる剣技",5,1,2,0,109000180,201000000,-1,109000180,1980,199580,192,19158,90,8980,140,14520,1,0,0,1,0,0,2,0,0,2,0,0,"凛然たる構えから繰り出される
技の鋭利さ、素早さ、正確さは
誰もが度肝を抜かれるほどだ。",1600,20000,100,10,20,"hlog_109000180_yuu_5_01_xxx","hlog_109000181_yuu_5_01_xxx",False,"Chara_comment/109000180","Chara_comment/109000181","109000180_cha","109000181_cha","Chara_icon/109000180","Chara_icon/109000181","Chara_icon_loss/109000180","Chara_icon_loss/109000181","2019/01/01",True,"1", -101000230,1,"キリト","空舞う双剣",5,2,4,0,101000230,201000000,201000000,101000230,1800,181440,216,21774,90,9070,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"《ALO》特有の空中戦においても
彼の類まれなる強さに変わりは無
い。自由に宙を舞い繰り出される
剣技に誰もが圧倒されるだろう。",1600,20000,100,10,20,"hlog_101000230_kir_5_01_xxx","hlog_101000231_kir_5_01_xxx",False,"Chara_comment/101000230","Chara_comment/101000231","101000230_cha","101000231_cha","Chara_icon/101000230","Chara_icon/101000231","Chara_icon_loss/101000230","Chara_icon_loss/101000231","2019/01/01",True,"1", -102000240,2,"アスナ","ハッピークリスマス",5,3,3,3,102000240,202000000,-1,102000240,1460,146970,174,17418,75,7350,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"厳格な家庭に育った彼女にとって
友と語らう聖夜の集いは、幸福な
ひと時だ。その眩しい笑顔は、
周囲の者にも幸せを分け与える。",1600,20000,100,10,20,"hlog_102000240_asu_5_01_xxx","hlog_102000241_asu_5_01_xxx",False,"Chara_comment/102000240","Chara_comment/102000241","102000240_cha","102000241_cha","Chara_icon/102000240","Chara_icon/102000241","Chara_icon_loss/102000240","Chara_icon_loss/102000241","2019/01/01",True,"1", -105000170,5,"リズベット","ホーリーデコレイト",5,10,2,3,105000170,209000000,-1,105000170,1890,190510,156,15678,125,12520,140,14520,1,0,0,1,0,0,2,0,0,2,0,0,"可愛すぎる服装に、以前は戸惑い
を見せていた彼女も、今宵は楽し
気に衣装を纏う。肩から下げた
トナカイがチャーミングだ。",1600,20000,100,10,20,"hlog_105000170_lis_5_01_xxx","hlog_105000171_lis_5_01_xxx",False,"Chara_comment/105000170","Chara_comment/105000171","105000170_cha","105000171_cha","Chara_icon/105000170","Chara_icon/105000171","Chara_icon_loss/105000170","Chara_icon_loss/105000171","2019/01/01",True,"1", -109000190,9,"ユウキ","ギフト・トゥギャザー!",5,1,1,3,109000190,201000000,-1,109000190,1440,145150,288,28740,65,6530,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"聖夜だけの特別な使命をこなす
べく、コスチュームも揃え気合い
は十分。サンタに扮する彼女と
共に、皆に夢を届けに行こう。",1600,20000,100,10,20,"hlog_109000190_yuu_5_01_xxx","hlog_109000191_yuu_5_01_xxx",False,"Chara_comment/109000190","Chara_comment/109000191","109000190_cha","109000191_cha","Chara_icon/109000190","Chara_icon/109000191","Chara_icon_loss/109000190","Chara_icon_loss/109000191","2019/01/01",True,"1", -105000180,5,"リズベット","温かな心",4,10,3,0,105000180,209000000,-1,105000180,1440,145150,138,13932,85,8710,190,19350,1,0,0,1,0,0,2,0,0,2,0,0,"極寒の雪山地帯である西の山で
渡されたマントのぬくもりは、
身体だけでなく心までもじんわり
と温める。",800,10000,100,10,20,"hlog_105000180_lis_4_01_xxx","hlog_105000180_lis_4_01_xxx",False,"Chara_comment/105000180","Chara_comment/105000180","105000180_cha","105000180_cha","Chara_icon/105000180","Chara_icon/105000180","Chara_icon_loss/105000180","Chara_icon_loss/105000180","2019/01/01",True,"1", -103000190,3,"リーファ","戯れの海辺",4,1,3,0,103000190,201000000,-1,103000190,1440,145150,156,15480,60,5805,230,23220,1,0,0,1,0,0,2,0,0,2,0,0,"水泳が苦手だった彼女にとって、
ALOの海は大切な仲間とそれを
克服した思い出の地だ。今や彼女
の表情に恐れは感じられない。",800,10000,100,10,20,"hlog_103000190_lea_4_01_xxx","hlog_103000190_lea_4_01_xxx",False,"Chara_comment/103000190","Chara_comment/103000190","103000190_cha","103000190_cha","Chara_icon/103000190","Chara_icon/103000190","Chara_icon_loss/103000190","Chara_icon_loss/103000190","2019/01/01",True,"1", -109000200,9,"ユウキ","想い出の甘味",4,1,2,0,109000200,201000000,-1,109000200,1760,177410,168,17034,80,7985,130,12900,1,0,0,1,0,0,2,0,0,2,0,0,"ここは、今や彼女だけが知ってい
る特別な場所だ。大事な仲間たち
を想う彼女の瞳は、曇り無く空を
見上げる。",800,10000,100,10,20,"hlog_109000200_yuu_4_01_xxx","hlog_109000200_yuu_4_01_xxx",False,"Chara_comment/109000200","Chara_comment/109000200","109000200_cha","109000200_cha","Chara_icon/109000200","Chara_icon/109000200","Chara_icon_loss/109000200","Chara_icon_loss/109000200","2019/01/01",True,"1", -112000090,12,"ユージオ","奇想の相棒",5,1,1,0,112000090,201000000,-1,112000090,1440,145150,258,26130,70,7260,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"突飛な発想力でお馴染みのキリト
から受け継いだ剣技を操る彼も又
真面目な気質はそのままに、独創
的な戦法で皆を驚かせてきた。",1600,20000,100,10,20,"hlog_112000090_ego_5_01_xxx","hlog_112000091_ego_5_01_xxx",False,"Chara_comment/112000090","Chara_comment/112000091","112000090_cha","112000091_cha","Chara_icon/112000090","Chara_icon/112000091","Chara_icon_loss/112000090","Chara_icon_loss/112000091","2019/01/01",True,"1", -104000220,4,"シノン","華綻ぶはにかみ",5,11,4,0,104000220,211000000,-1,104000220,1440,145150,240,23952,70,7260,200,19960,1,0,0,1,0,0,2,0,0,2,0,0,"大きな借りのお返しにと用意され
たのは和風の装飾品。一点ものの
それは、はにかむ彼女によく似合
う。",1600,20000,100,10,20,"hlog_104000220_sin_5_01_xxx","hlog_104000221_sin_5_01_xxx",False,"Chara_comment/104000220","Chara_comment/104000221","104000220_cha","104000221_cha","Chara_icon/104000220","Chara_icon/104000221","Chara_icon_loss/104000220","Chara_icon_loss/104000221","2019/01/01",True,"1", -103000200,3,"リーファ","親愛なる漆黒",5,2,3,0,103000200,201000000,201000000,103000200,1620,163300,174,17418,65,6530,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"普段の柔らかな緑と違い、漆黒の
衣装を纏う彼女だが、その姿から
冷酷さは感じられない。敬愛する
人を思わせる色に心も温かだ。",1600,20000,100,10,20,"hlog_103000200_lea_5_01_xxx","hlog_103000201_lea_5_01_xxx",False,"Chara_comment/103000200","Chara_comment/103000201","103000200_cha","103000201_cha","Chara_icon/103000200","Chara_icon/103000201","Chara_icon_loss/103000200","Chara_icon_loss/103000201","2019/01/01",True,"1", -111000110,11,"アリス","黒衣の聖騎士",5,2,1,0,111000110,201000000,201000000,111000110,1660,166920,270,27432,85,8345,170,17240,1,0,0,1,0,0,2,0,0,2,0,0,"黒色の甲冑を着こなす彼女には、
常よりも穏やかな雰囲気が漂う。
それは、黒を纏うかの剣士への
信頼が故なのであろう。",1600,20000,100,10,20,"hlog_111000110_alc_5_01_xxx","hlog_111000111_alc_5_01_xxx",False,"Chara_comment/111000110","Chara_comment/111000111","111000110_cha","111000111_cha","Chara_icon/111000110","Chara_icon/111000111","Chara_icon_loss/111000110","Chara_icon_loss/111000111","2019/01/01",True,"1", -104000230,4,"シノン","疾風迅雷",5,11,3,0,104000230,211000000,-1,104000230,1300,130640,192,19158,65,6530,240,23950,1,0,0,1,0,0,2,0,0,2,0,0,"怒涛の弓捌きで放たれる攻撃で
計算違わず敵を仕留める。
《ALO》において長距離戦で彼女
の右に出る者はない。",1600,20000,100,10,20,"hlog_104000230_sin_5_01_xxx","hlog_104000231_sin_5_01_xxx",False,"Chara_comment/104000230","Chara_comment/104000231","104000230_cha","104000231_cha","Chara_icon/104000230","Chara_icon/104000231","Chara_icon_loss/104000230","Chara_icon_loss/104000231","2019/01/01",True,"1", -110000160,10,"アルゴ","休息の爪先",5,4,3,0,110000160,203000000,-1,110000160,1540,155130,162,16548,75,7755,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"休みの無い情報屋家業を続ける
彼女にも、素に戻る時間は存在す
る。足先を寛げて、無垢な表情を
見せる穏やかな日常。",1600,20000,100,10,20,"hlog_110000160_arg_5_01_xxx","hlog_110000161_arg_5_01_xxx",False,"Chara_comment/110000160","Chara_comment/110000161","110000160_cha","110000161_cha","Chara_icon/110000160","Chara_icon/110000161","Chara_icon_loss/110000160","Chara_icon_loss/110000161","2020/02/26 7:00:00",True,"1", -112000100,12,"ユージオ","背氷の気迫",5,1,4,0,112000100,201000000,-1,112000100,1800,181440,216,21774,90,9070,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"想い出を失った少年は、戦う意味
も空虚なまま、脅威たる刃を振る
う。帰る場所など既に無く、ただ
ひたすら眼前の敵を屠るのみ。",1600,20000,100,10,20,"hlog_112000100_ego_5_01_xxx","hlog_112000101_ego_5_01_xxx",False,"Chara_comment/112000100","Chara_comment/112000101","112000100_cha","112000101_cha","Chara_icon/112000100","Chara_icon/112000101","Chara_icon_loss/112000100","Chara_icon_loss/112000101","2020/02/26 7:00:00",True,"1", -111000120,11,"アリス","熟達の志士",5,1,1,0,111000120,201000000,-1,111000120,1660,166920,270,27432,85,8345,170,17240,1,0,0,1,0,0,2,0,0,2,0,0,"整合騎士として第三位の実力を
身に着けるに至ったのは、地道な
鍛錬の賜物だ。皆を、世界を守る
ため、今日も少女は刃を振るう。",1600,20000,100,10,20,"hlog_111000120_alc_5_01_xxx","hlog_111000121_alc_5_01_xxx",False,"Chara_comment/111000120","Chara_comment/111000121","111000120_cha","111000121_cha","Chara_icon/111000120","Chara_icon/111000121","Chara_icon_loss/111000120","Chara_icon_loss/111000121","2019/01/01",True,"1", -111000130,11,"アリス","シカクシメンな彼女",5,1,3,0,111000130,201000000,-1,111000130,1860,187790,180,18288,95,9390,210,20680,1,0,0,1,0,0,2,0,0,2,0,0,"生真面目ゆえに甘えた態度をとる
ことは不得手な様子。そんな彼女
に角の取れた表情を見せてもらえ
る日はくるのだろうか。",1600,20000,100,10,20,"hlog_111000130_alc_5_01_xxx","hlog_111000131_alc_5_01_xxx",False,"Chara_comment/111000130","Chara_comment/111000131","111000130_cha","111000131_cha","Chara_icon/111000130","Chara_icon/111000131","Chara_icon_loss/111000130","Chara_icon_loss/111000131","2019/01/01",True,"1", -105000190,5,"リズベット","ハツコイ同盟",5,4,3,0,105000190,203000000,-1,105000190,1620,163300,156,15678,95,9800,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"恋心を同じくする少女とは、同盟
を組む内に気付けば息もピッタリ
に。相方の色を身に纏い、揃って
ばっちり決めポーズ。",1600,20000,100,10,20,"hlog_105000190_lis_5_01_xxx","hlog_105000191_lis_5_01_xxx",False,"Chara_comment/105000190","Chara_comment/105000191","105000190_cha","105000191_cha","Chara_icon/105000190","Chara_icon/105000191","Chara_icon_loss/105000190","Chara_icon_loss/105000191","2020/06/09 7:00:00",True,"1", -106000190,6,"シリカ","トキメキ同盟",5,10,3,0,106000190,209000000,-1,106000190,1300,130640,156,15678,65,6530,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"同じときめきを抱く同志の彼女は
いつの間にやら、誰より気の置け
ない友人に。相方の色に身を包み
楽しい一時を過ごす。",1600,20000,100,10,20,"hlog_106000190_sil_5_01_xxx","hlog_106000191_sil_5_01_xxx",False,"Chara_comment/106000190","Chara_comment/106000191","106000190_cha","106000191_cha","Chara_icon/106000190","Chara_icon/106000191","Chara_icon_loss/106000190","Chara_icon_loss/106000191","2020/06/09 7:00:00",True,"1", -101000240,1,"キリト","気儘な黒猫",5,8,1,4,101000240,207000000,-1,101000240,1440,145150,258,26130,70,7260,180,18140,107800,2019,0,1,0,0,2,0,0,2,0,0,"ランダムにアバターが猫換装?!
そんなピンチもなんのその、気儘
なブラッキーはのんびりごろ寝…
とはいえ少しは恥ずかしい様子?",1600,20000,100,10,20,"hlog_101000240_kir_5_01_xxx","hlog_101000241_kir_5_01_xxx",False,"Chara_comment/101000240","Chara_comment/101000241","101000240_cha","101000241_cha","Chara_icon/101000240","Chara_icon/101000241","Chara_icon_loss/101000240","Chara_icon_loss/101000241","2019/01/01",True,"1", -102000250,2,"アスナ","ねころび愛猫",5,8,3,4,102000250,207000000,-1,102000250,1460,146970,174,17418,75,7350,260,26130,107800,2019,0,1,0,0,2,0,0,2,0,0,"ランダムにアバターが猫換装?!
普段はおしとやかな彼女も、今は
マイペースな猫気分。寝ころんで
見る世界はちょっぴり新鮮かも。",1600,20000,100,10,20,"hlog_102000250_asu_5_01_xxx","hlog_102000251_asu_5_01_xxx",True,"Chara_comment/102000250","Chara_comment/102000251","102000250_cha","102000251_cha","Chara_icon/102000250","Chara_icon/102000251","Chara_icon_loss/102000250","Chara_icon_loss/102000251","2019/01/01",True,"1", -103000210,3,"リーファ","奔放な萌猫",5,8,4,4,103000210,207000000,-1,103000210,1800,181440,216,21774,70,7260,220,21770,107800,2019,0,1,0,0,2,0,0,2,0,0,"ランダムにアバターが猫換装?!
どんな逆境にも負けないしなやか
な彼女。ネコチャン気分をめ一杯
楽しんじゃおう!",1600,20000,100,10,20,"hlog_103000210_lea_5_01_xxx","hlog_103000211_lea_5_01_xxx",False,"Chara_comment/103000210","Chara_comment/103000211","103000210_cha","103000211_cha","Chara_icon/103000210","Chara_icon/103000211","Chara_icon_loss/103000210","Chara_icon_loss/103000211","2019/01/01",True,"1", -109000210,9,"ユウキ","またたび仔猫",5,8,1,4,109000210,207000000,-1,109000210,1440,145150,288,28740,65,6530,180,18140,107800,2019,0,1,0,0,2,0,0,2,0,0,"ランダムにアバターが猫換装?!
素直な彼女は身も心もネコそのも
の?毛繕いして、髪もくくって、
狩りの準備はバッチリ!",1600,20000,100,10,20,"hlog_109000210_yuu_5_01_xxx","hlog_109000211_yuu_5_01_xxx",True,"Chara_comment/109000210","Chara_comment/109000211","109000210_cha","109000211_cha","Chara_icon/109000210","Chara_icon/109000211","Chara_icon_loss/109000210","Chara_icon_loss/109000211","2019/01/01",True,"1", -104000240,4,"シノン","慧眼の弓猫",4,8,1,4,104000240,207000000,-1,104000240,1020,103220,252,25548,50,5160,180,17740,107800,2019,0,1,0,0,2,0,0,2,0,0,"ランダムにアバターが猫換装?!
猫妖精のアバターを持つものの、
この姿で猫になるのは落ち着かず
戸惑いを隠しきれない様子。",800,10000,100,10,20,"hlog_104000240_sin_4_01_xxx","hlog_104000240_sin_4_01_xxx",False,"Chara_comment/104000240","Chara_comment/104000240","104000240_cha","104000240_cha","Chara_icon/104000240","Chara_icon/104000240","Chara_icon_loss/104000240","Chara_icon_loss/104000240","2019/01/01",True,"1", -105000200,5,"リズベット","友情の桃猫",4,8,3,4,105000200,207000000,-1,105000200,1440,145150,138,13932,85,8710,190,19350,107800,2019,0,1,0,0,2,0,0,2,0,0,"ランダムにアバターが猫換装?!
異常事態とはいえ、みんなと一緒
ならなんだか可笑しく思えてしま
う。今は猫を楽しんじゃおう!",800,10000,100,10,20,"hlog_105000200_lis_4_01_xxx","hlog_105000200_lis_4_01_xxx",False,"Chara_comment/105000200","Chara_comment/105000200","105000200_cha","105000200_cha","Chara_icon/105000200","Chara_icon/105000200","Chara_icon_loss/105000200","Chara_icon_loss/105000200","2019/01/01",True,"1", -106000200,6,"シリカ","可憐な幼猫",4,8,4,4,106000200,207000000,-1,106000200,1280,129020,174,17418,65,6450,190,19350,107800,2019,0,1,0,0,2,0,0,2,0,0,"ランダムにアバターが猫換装?!
普段も猫妖精ではあるものの、こ
ちらの身体につくと話は別。恥ず
かし気な表情が愛くるしい。",800,10000,100,10,20,"hlog_106000200_sil_4_01_xxx","hlog_106000200_sil_4_01_xxx",False,"Chara_comment/106000200","Chara_comment/106000200","106000200_cha","106000200_cha","Chara_icon/106000200","Chara_icon/106000200","Chara_icon_loss/106000200","Chara_icon_loss/106000200","2019/01/01",True,"1", -108000140,8,"エギル","猫耳の豪商",4,8,1,4,108000140,207000000,-1,108000140,1540,154830,252,25548,70,7095,140,14520,107800,2019,0,1,0,0,2,0,0,2,0,0,"ランダムにアバターが猫換装…?
毛髪パーツの足りない彼は、なん
と猫耳カチューシャで参戦。堂々
たる様子なのは人生経験の賜物?",800,10000,100,10,20,"hlog_108000140_agi_4_01_xxx","hlog_108000140_agi_4_01_xxx",False,"Chara_comment/108000140","Chara_comment/108000140","108000140_cha","108000140_cha","Chara_icon/108000140","Chara_icon/108000140","Chara_icon_loss/108000140","Chara_icon_loss/108000140","2019/01/01",True,"1", -109000220,9,"ユウキ","まごころショコラティエ",5,6,1,0,109000220,205000000,-1,109000220,1440,145150,288,28740,65,6530,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"チョコレート作りに初挑戦する手
付きは覚束ないながら、想いを伝
えるため、懸命に練習を続ける。
全ては大切な君のため。",1600,20000,100,10,20,"hlog_109000220_yuu_5_01_xxx","hlog_109000221_yuu_5_01_xxx",False,"Chara_comment/109000220","Chara_comment/109000221","109000220_cha","109000221_cha","Chara_icon/109000220","Chara_icon/109000221","Chara_icon_loss/109000220","Chara_icon_loss/109000221","2019/01/01",True,"1", -101000250,1,"キリト","愛受のはにかみ",4,2,4,0,101000250,201000000,201000000,101000250,1600,161280,192,19356,80,8065,160,16130,1,0,0,1,0,0,2,0,0,2,0,0,"チョコレートを受け取った際の
記録。想いを受け取り、彼もまた
照れた様子ではにかんだ笑顔を
見せてくれる。",800,10000,100,10,20,"hlog_101000250_kir_4_01_xxx","hlog_101000250_kir_4_01_xxx",False,"Chara_comment/101000250","Chara_comment/101000250","101000250_cha","101000250_cha","Chara_icon/101000250","Chara_icon/101000250","Chara_icon_loss/101000250","Chara_icon_loss/101000250","2019/01/01",True,"1", -102000260,2,"アスナ","甘い贈り物",4,3,3,0,102000260,202000000,-1,102000260,1300,130640,156,15480,65,6530,230,23220,1,0,0,1,0,0,2,0,0,2,0,0,"チョコレートを渡した際の記録。
料理上手な彼女は手作りお菓子を
用意。美味しさはもちろん、彼女
の想いもたっぷり詰まっている。",800,10000,100,10,20,"hlog_102000260_asu_4_01_xxx","hlog_102000260_asu_4_01_xxx",False,"Chara_comment/102000260","Chara_comment/102000260","102000260_cha","102000260_cha","Chara_icon/102000260","Chara_icon/102000260","Chara_icon_loss/102000260","Chara_icon_loss/102000260","2019/01/01",True,"1", -103000220,3,"リーファ","恋慕の微笑",4,1,4,0,103000220,201000000,-1,103000220,1600,161280,192,19356,65,6450,190,19350,1,0,0,1,0,0,2,0,0,2,0,0,"チョコレートを渡した際の記録。
意表をついてクレープを用意した
彼女。一緒に共有した甘い思い出
そのものがプレゼントだ。",800,10000,100,10,20,"hlog_103000220_lea_4_01_xxx","hlog_103000220_lea_4_01_xxx",False,"Chara_comment/103000220","Chara_comment/103000220","103000220_cha","103000220_cha","Chara_icon/103000220","Chara_icon/103000220","Chara_icon_loss/103000220","Chara_icon_loss/103000220","2019/01/01",True,"1", -104000250,4,"シノン","静穏の秘め事",4,11,1,0,104000250,211000000,-1,104000250,1020,103220,252,25548,50,5160,180,17740,1,0,0,1,0,0,2,0,0,2,0,0,"チョコレートを渡した際の記録。
穏やかな笑みで背に隠すのは、
不慣れながら練習を重ねて作った
手作りの贈り物。",800,10000,100,10,20,"hlog_104000250_sin_4_01_xxx","hlog_104000250_sin_4_01_xxx",False,"Chara_comment/104000250","Chara_comment/104000250","104000250_cha","104000250_cha","Chara_icon/104000250","Chara_icon/104000250","Chara_icon_loss/104000250","Chara_icon_loss/104000250","2019/01/01",True,"1", -105000210,5,"リズベット","親愛の鍛冶屋",4,10,3,0,105000210,209000000,-1,105000210,1440,145150,138,13932,85,8710,190,19350,1,0,0,1,0,0,2,0,0,2,0,0,"チョコレートを渡した際の記録。
友を大事にする彼女は沢山のチョ
コを配るようだが、手渡すそれ
にはとびきり甘い想いが籠る。",800,10000,100,10,20,"hlog_105000210_lis_4_01_xxx","hlog_105000210_lis_4_01_xxx",False,"Chara_comment/105000210","Chara_comment/105000210","105000210_cha","105000210_cha","Chara_icon/105000210","Chara_icon/105000210","Chara_icon_loss/105000210","Chara_icon_loss/105000210","2019/01/01",True,"1", -106000210,6,"シリカ","純情な恋路",4,4,4,0,106000210,203000000,-1,106000210,1280,129020,174,17418,65,6450,190,19350,1,0,0,1,0,0,2,0,0,2,0,0,"チョコレートを渡した際の記録。
幼い恋心をいっぱいに膨らませた
甘い甘い一品を渡すその手は、
緊張と恥じらいに震えている。",800,10000,100,10,20,"hlog_106000210_sil_4_01_xxx","hlog_106000210_sil_4_01_xxx",False,"Chara_comment/106000210","Chara_comment/106000210","106000210_cha","106000210_cha","Chara_icon/106000210","Chara_icon/106000210","Chara_icon_loss/106000210","Chara_icon_loss/106000210","2019/01/01",True,"1", -107000140,7,"クライン","歓喜の照笑",4,6,1,0,107000140,205000000,-1,107000140,1410,141930,252,25548,65,6450,130,12900,1,0,0,1,0,0,2,0,0,2,0,0,"チョコレートを受け取った際の
記録。待ち望んだ甘い贈り物に、
思わず浮足立っているのが、表情
からも漏れ出てしまっている。",800,10000,100,10,20,"hlog_107000140_kle_4_01_xxx","hlog_107000140_kle_4_01_xxx",False,"Chara_comment/107000140","Chara_comment/107000140","107000140_cha","107000140_cha","Chara_icon/107000140","Chara_icon/107000140","Chara_icon_loss/107000140","Chara_icon_loss/107000140","2019/01/01",True,"1", -108000150,8,"エギル","義理堅い甘露",4,9,2,0,108000150,208000000,-1,108000150,2210,222570,168,17034,90,9315,120,11610,1,0,0,1,0,0,2,0,0,2,0,0,"チョコレートを受け取った際の
記録。妻帯者である彼だが、受け
取った贈り物を無下にはしない。
誠実に感謝を伝えてくれる。",800,10000,100,10,20,"hlog_108000150_agi_4_01_xxx","hlog_108000150_agi_4_01_xxx",False,"Chara_comment/108000150","Chara_comment/108000150","108000150_cha","108000150_cha","Chara_icon/108000150","Chara_icon/108000150","Chara_icon_loss/108000150","Chara_icon_loss/108000150","2019/01/01",True,"1", -109000230,9,"ユウキ","純粋な愛情",4,1,1,0,109000230,201000000,-1,109000230,1280,129020,252,25548,60,5805,160,16130,1,0,0,1,0,0,2,0,0,2,0,0,"チョコレートを渡した際の記録。
小さな箱に、ぎゅぎゅっと想いを
詰め込んで。一生懸命作ったから
食べてみてほしいんだ。",800,10000,100,10,20,"hlog_109000230_yuu_4_01_xxx","hlog_109000230_yuu_4_01_xxx",False,"Chara_comment/109000230","Chara_comment/109000230","109000230_cha","109000230_cha","Chara_icon/109000230","Chara_icon/109000230","Chara_icon_loss/109000230","Chara_icon_loss/109000230","2019/01/01",True,"1", -110000170,10,"アルゴ","秘密の贈り物",4,8,3,0,110000170,207000000,-1,110000170,1370,137890,144,14706,70,6895,230,23220,1,0,0,1,0,0,2,0,0,2,0,0,"チョコレートを渡した際の記録。
今日限りの特別大サービス。お得
意様だから、との言葉に隠された
想いは、今はまだ秘密のまま。",800,10000,100,10,20,"hlog_110000170_arg_4_01_xxx","hlog_110000170_arg_4_01_xxx",False,"Chara_comment/110000170","Chara_comment/110000170","110000170_cha","110000170_cha","Chara_icon/110000170","Chara_icon/110000170","Chara_icon_loss/110000170","Chara_icon_loss/110000170","2019/01/01",True,"1", -111000140,11,"アリス","裏腹な恋心",4,1,4,0,111000140,201000000,-1,111000140,1840,185470,204,20322,90,9275,150,15320,1,0,0,1,0,0,2,0,0,2,0,0,"チョコレートを渡した際の記録。
プレゼントに込めた想いと、騎士
としての矜持。揺れ動く彼女が、
素直になれるのはまだ先の話。",800,10000,100,10,20,"hlog_111000140_alc_4_01_xxx","hlog_111000140_alc_4_01_xxx",False,"Chara_comment/111000140","Chara_comment/111000140","111000140_cha","111000140_cha","Chara_icon/111000140","Chara_icon/111000140","Chara_icon_loss/111000140","Chara_icon_loss/111000140","2019/01/01",True,"1", -112000110,12,"ユージオ","心届く笑顔",4,1,4,0,112000110,201000000,-1,112000110,1600,161280,192,19356,80,8065,160,16130,1,0,0,1,0,0,2,0,0,2,0,0,"チョコレートを受け取った際の
記録。心のこもった贈り物に、
心から嬉しそうな笑顔をこぼして
こたえてくれる。",800,10000,100,10,20,"hlog_112000110_ego_4_01_xxx","hlog_112000110_ego_4_01_xxx",False,"Chara_comment/112000110","Chara_comment/112000110","112000110_cha","112000110_cha","Chara_icon/112000110","Chara_icon/112000110","Chara_icon_loss/112000110","Chara_icon_loss/112000110","2019/01/01",True,"1", -104000260,4,"シノン","冥界の女神",3,14,4,0,104000260,215000000,-1,104000260,1120,112900,186,18630,55,5645,150,15520,1,0,0,1,0,0,2,0,0,99999,0,0,"女性でソロスナイパーのシノンは
GGOでは珍しい存在である。
その実力も相まって、有名
プレイヤーの一員と言えよう。",400,6000,100,10,20,"hlog_104000260_sin_3_01_xxx","hlog_104000260_sin_3_01_xxx",False,"Chara_comment/104000260","Chara_comment/104000260","104000260_cha","104000260_cha","Chara_icon/104000260","Chara_icon/104000260","Chara_icon_loss/104000260","Chara_icon_loss/104000260","2020/02/26 7:00:00",True,"1", -104000270,4,"シノン","親昵の相棒",4,14,4,0,104000270,215000000,-1,104000270,1280,129020,210,21288,65,6450,180,17740,1,0,0,1,0,0,2,0,0,2,0,0,"Unknownで発見された《GGO》
リソース群から、ようやく自身の
分身たるアバターと信頼する愛銃
を得て、彼女の顔も自然と綻ぶ。",800,10000,100,10,20,"hlog_104000270_sin_4_01_xxx","hlog_104000270_sin_4_01_xxx",False,"Chara_comment/104000270","Chara_comment/104000270","104000270_cha","104000270_cha","Chara_icon/104000270","Chara_icon/104000270","Chara_icon_loss/104000270","Chara_icon_loss/104000270","2020/02/26 7:00:00",True,"1", -103000230,3,"リーファ","秘めた胸奥",5,3,1,0,103000230,202000000,-1,103000230,1440,145150,258,26130,60,5805,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"乙女の心はトップシークレット。
駆け引きのつもりはないけれど、
慕わしい相手だからこそ、教えら
れない秘密もあるのです。",1600,20000,100,10,20,"hlog_103000230_lea_5_01_xxx","hlog_103000231_lea_5_01_xxx",False,"Chara_comment/103000230","Chara_comment/103000231","103000230_cha","103000231_cha","Chara_icon/103000230","Chara_icon/103000231","Chara_icon_loss/103000230","Chara_icon_loss/103000231","2020/03/01 7:00:00",True,"1", -101000260,1,"キリト","一望千里",5,2,4,0,101000260,201000000,201000000,101000260,1800,181440,216,21774,90,9070,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"世界の謎を追って1年、気付けば
遥かな高みに至りて、眼下に広が
るは千里の眺望。冒険はまだまだ
始まったばかり。",1600,20000,100,10,20,"hlog_101000260_kir_5_01_xxx","hlog_101000261_kir_5_01_xxx",False,"Chara_comment/101000260","Chara_comment/101000261","101000260_cha","101000261_cha","Chara_icon/101000260","Chara_icon/101000261","Chara_icon_loss/101000260","Chara_icon_loss/101000261","2020/06/23 7:00:00",True,"1", -102000270,2,"アスナ","一心同体",5,3,4,0,102000270,202000000,-1,102000270,1620,163300,216,21774,80,8165,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"志を同じくするあなたと出会って
早1年。いつしか結束も強固にな
り、皆の心は一つに。これからも
一緒に駆け抜けていこうね。",1600,20000,100,10,20,"hlog_102000270_asu_5_01_xxx","hlog_102000271_asu_5_01_xxx",False,"Chara_comment/102000270","Chara_comment/102000271","102000270_cha","102000271_cha","Chara_icon/102000270","Chara_icon/102000271","Chara_icon_loss/102000270","Chara_icon_loss/102000271","2020/06/23 7:00:00",True,"1", -104000280,4,"シノン","一意専心",5,14,4,0,104000280,215000000,-1,104000280,1440,145150,240,23952,70,7260,200,19960,1,0,0,1,0,0,2,0,0,2,0,0,"世界を見通す彼女の慧眼は、真っ
直ぐに真実を射抜く。初めの1年
が終わろうと、彼女のひたむきな
姿勢はずっと変わらない。",1600,20000,100,10,20,"hlog_104000280_sin_5_01_xxx","hlog_104000281_sin_5_01_xxx",False,"Chara_comment/104000280","Chara_comment/104000281","104000280_cha","104000281_cha","Chara_icon/104000280","Chara_icon/104000281","Chara_icon_loss/104000280","Chara_icon_loss/104000281","2020/06/23 7:00:00",True,"1", -111000150,11,"アリス","一糸不乱",5,1,4,0,111000150,201000000,-1,111000150,2070,208660,228,22860,105,10435,170,17240,1,0,0,1,0,0,2,0,0,2,0,0,"初めはこちらの世界に戸惑うこと
も多い彼女だったが、いまや迷い
は無い。それも、いつも支えてく
れるあなたが居てこそ。",1600,20000,100,10,20,"hlog_111000150_alc_5_01_xxx","hlog_111000151_alc_5_01_xxx",False,"Chara_comment/111000150","Chara_comment/111000151","111000150_cha","111000151_cha","Chara_icon/111000150","Chara_icon/111000151","Chara_icon_loss/111000150","Chara_icon_loss/111000151","2020/06/23 7:00:00",True,"1", -118000010,18,"レイン","天水の剣光",2,2,4,0,118000010,201000000,201000000,118000010,1080,108860,150,15240,60,6050,120,12100,1,0,0,1,0,0,2,0,0,99999,0,0,"《ALO》の《スヴァルトアール
ヴヘイム》でキリト達と出会った
多刀流使い。セブンこと七色博士
の実の姉であり、妹想い。",200,2000,100,10,20,"hlog_118000010_rai_2_01_xxx","hlog_118000010_rai_2_01_xxx",False,"Chara_comment/118000010","Chara_comment/118000010","118000010_cha","118000010_cha","Chara_icon/118000010","Chara_icon/118000010","Chara_icon_loss/118000010","Chara_icon_loss/118000010","2020/06/09 7:00:00",True,"1", -118000020,18,"レイン","紅奏の斬響",3,2,3,0,118000020,201000000,201000000,118000020,1130,114310,144,14226,65,6350,170,16930,1,0,0,1,0,0,2,0,0,99999,0,0,"《ALO》の《スヴァルトアール
ヴヘイム》でキリト達と出会った
多刀流使い。セブンこと七色博士
の実の姉であり、妹想い。",400,6000,100,10,20,"hlog_118000020_rai_3_01_xxx","hlog_118000020_rai_3_01_xxx",False,"Chara_comment/118000020","Chara_comment/118000020","118000020_cha","118000020_cha","Chara_icon/118000020","Chara_icon/118000020","Chara_icon_loss/118000020","Chara_icon_loss/118000020","2020/06/09 7:00:00",True,"1", -118000030,18,"レイン","新天地の追跡者",3,2,1,0,118000030,201000000,201000000,118000030,1010,101610,210,21336,55,5645,140,14110,1,0,0,1,0,0,2,0,0,99999,0,0,"《ALO》の《スヴァルトアール
ヴヘイム》でキリト達と出会った
多刀流使い。セブンこと七色博士
の実の姉であり、妹想い。",400,6000,100,10,20,"hlog_118000030_rai_3_01_xxx","hlog_118000030_rai_3_01_xxx",False,"Chara_comment/118000030","Chara_comment/118000030","118000030_cha","118000030_cha","Chara_icon/118000030","Chara_icon/118000030","Chara_icon_loss/118000030","Chara_icon_loss/118000030","2020/06/09 7:00:00",True,"1", -118000040,18,"レイン","多刀流の使い手",3,2,4,0,118000040,201000000,201000000,118000040,1260,127010,174,17784,70,7055,140,14110,1,0,0,1,0,0,2,0,0,99999,0,0,"《ALO》の《スヴァルトアール
ヴヘイム》でキリト達と出会った
多刀流使い。セブンこと七色博士
の実の姉であり、妹想い。",400,6000,100,10,20,"hlog_118000040_rai_3_01_xxx","hlog_118000040_rai_3_01_xxx",False,"Chara_comment/118000040","Chara_comment/118000040","118000040_cha","118000040_cha","Chara_icon/118000040","Chara_icon/118000040","Chara_icon_loss/118000040","Chara_icon_loss/118000040","2020/06/09 7:00:00",True,"1", -118000050,18,"レイン","一騎多閃",4,2,3,0,118000050,201000000,201000000,118000050,1300,130640,162,16260,70,7260,190,19350,1,0,0,1,0,0,2,0,0,2,0,0,"レプラコーンのスキルを活かし、
1人でありながら数多の武器を同
時に操る。その様はさながら剣の
雨のよう。",800,10000,100,10,20,"hlog_118000050_rai_4_01_xxx","hlog_118000050_rai_4_01_xxx",False,"Chara_comment/118000050","Chara_comment/118000050","118000050_cha","118000050_cha","Chara_icon/118000050","Chara_icon/118000050","Chara_icon_loss/118000050","Chara_icon_loss/118000050","2020/06/09 7:00:00",True,"1", -118000060,18,"レイン","温かな眼差し",4,2,1,0,118000060,201000000,201000000,118000060,1150,116120,240,24384,65,6450,160,16130,1,0,0,1,0,0,2,0,0,2,0,0,"妹想いの彼女の眼差しは、どこか
大人びていて、いつも温かく柔ら
かい。そんな優しい彼女だからこ
そ、その歌声に誰もが共感する。",800,10000,100,10,20,"hlog_118000060_rai_4_01_xxx","hlog_118000060_rai_4_01_xxx",False,"Chara_comment/118000060","Chara_comment/118000060","118000060_cha","118000060_cha","Chara_icon/118000060","Chara_icon/118000060","Chara_icon_loss/118000060","Chara_icon_loss/118000060","2020/06/09 7:00:00",True,"1", -118000070,18,"レイン","空島繋ぐ架け橋",5,2,4,0,118000070,201000000,201000000,118000070,1620,163300,228,22860,90,9070,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"《スヴァルトアールヴヘイム》を
駆け抜けた日々は、彼女にとって
宝物だ。共鳴する姉妹を祝福する
ように、空に虹が架かる。",1600,20000,100,10,20,"hlog_118000070_rai_5_01_xxx","hlog_118000071_rai_5_01_xxx",False,"Chara_comment/118000070","Chara_comment/118000071","118000070_cha","118000071_cha","Chara_icon/118000070","Chara_icon/118000071","Chara_icon_loss/118000070","Chara_icon_loss/118000071","2020/06/09 7:00:00",True,"1", -118000080,18,"レイン","虹架ける微笑み",4,2,4,0,118000080,201000000,201000000,118000080,1440,145150,204,20322,80,8065,160,16130,1,0,0,1,0,0,2,0,0,2,0,0,"日々邁進するアイドルの卵も、
今日は完全オフ。穏やかに綻んだ
表情に、心を奪われない者はいな
いだろう。",800,10000,100,10,20,"hlog_118000080_rai_4_01_xxx","hlog_118000080_rai_4_01_xxx",False,"Chara_comment/118000080","Chara_comment/118000080","118000080_cha","118000080_cha","Chara_icon/118000080","Chara_icon/118000080","Chara_icon_loss/118000080","Chara_icon_loss/118000080","2020/06/09 7:00:00",True,"1", -118000090,18,"レイン","かろやかスキップ",5,2,3,0,118000090,201000000,201000000,118000090,1460,146970,180,18288,80,8165,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"《ALO》ではレプラコーンを選択
したが、領内の交流が苦手だった
彼女。それでも友の手引きを受け
て、今ではその足取りも軽い。",1600,20000,100,10,20,"hlog_118000090_rai_5_01_xxx","hlog_118000091_rai_5_01_xxx",False,"Chara_comment/118000090","Chara_comment/118000091","118000090_cha","118000091_cha","Chara_icon/118000090","Chara_icon/118000091","Chara_icon_loss/118000090","Chara_icon_loss/118000091","2020/06/09 7:00:00",True,"1", -105000220,5,"リズベット","はつらつステップ",5,3,3,0,105000220,202000000,-1,105000220,1620,163300,156,15678,95,9800,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"《ALO》で同種族のレプラコーン
を選んだ友人に、先輩として張り
切って世話を焼く彼女。鍛冶仲間
が出来て嬉しいのだとか。",1600,20000,100,10,20,"hlog_105000220_lis_5_01_xxx","hlog_105000221_lis_5_01_xxx",False,"Chara_comment/105000220","Chara_comment/105000221","105000220_cha","105000221_cha","Chara_icon/105000220","Chara_icon/105000221","Chara_icon_loss/105000220","Chara_icon_loss/105000221","2020/06/09 7:00:00",True,"1", -102000280,2,"アスナ","異界の女神",5,3,4,0,102000280,202000000,-1,102000280,1620,163300,216,21774,80,8165,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"突如見知らぬアバターに換装され
てしまったアスナ。その姿は神々
しく、まるで女神のよう。",1600,20000,100,10,20,"hlog_102000280_asu_5_01_xxx","hlog_102000281_asu_5_01_xxx",False,"Chara_comment/102000280","Chara_comment/102000281","102000280_cha","102000281_cha","Chara_icon/102000280","Chara_icon/102000281","Chara_icon_loss/102000280","Chara_icon_loss/102000281","2020/06/16 7:00:00",True,"1", -111000160,11,"アリス","固守の決意",5,1,4,0,111000160,201000000,-1,111000160,2070,208660,228,22860,105,10435,170,17240,1,0,0,1,0,0,2,0,0,2,0,0,"心惑いつつも穏やかな日々を過ご
した彼女。今や迷いは晴れ、再び
剣をとる。家族を、そして彼が愛
す世界を、この手で守るために。",1600,20000,100,10,20,"hlog_111000160_alc_5_01_xxx","hlog_111000161_alc_5_01_xxx",False,"Chara_comment/111000160","Chara_comment/111000161","111000160_cha","111000161_cha","Chara_icon/111000160","Chara_icon/111000161","Chara_icon_loss/111000160","Chara_icon_loss/111000161","2020/06/09 7:00:00",True,"1", -103000240,3,"リーファ","風の魔剣士",5,1,1,0,103000240,201000000,-1,103000240,1440,145150,258,26130,60,5805,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"《ALO》プレイヤーとしてキャリ
アが長く、現実世界で鍛えた剣術
センスと、卓越した魔法スキルを
併せ持つ彼女は心強い存在だ。",1600,20000,100,10,20,"hlog_103000240_lea_5_01_xxx","hlog_103000241_lea_5_01_xxx",False,"Chara_comment/103000240","Chara_comment/103000241","103000240_cha","103000241_cha","Chara_icon/103000240","Chara_icon/103000241","Chara_icon_loss/103000240","Chara_icon_loss/103000241","2020/06/23 7:00:00",True,"1", -106000220,6,"シリカ","獣の竜使い",5,9,1,0,106000220,208000000,-1,106000220,1150,116120,234,23514,60,5805,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"テイミングスキルを超えた絆で
結ばれた愛竜・ピナとの連携は
《ALO》でも健在。豊富なブレス
スキルで攻略を助ける。",1600,20000,100,10,20,"hlog_106000220_sil_5_01_xxx","hlog_106000221_sil_5_01_xxx",False,"Chara_comment/106000220","Chara_comment/106000221","106000220_cha","106000221_cha","Chara_icon/106000220","Chara_icon/106000221","Chara_icon_loss/106000220","Chara_icon_loss/106000221","2020/06/23 7:00:00",True,"1", -107000150,7,"クライン","武人の少憩",5,6,4,0,107000150,205000000,-1,107000150,1980,199580,240,23952,90,9070,140,14520,1,0,0,1,0,0,2,0,0,2,0,0,"明るいムードメーカーな彼は一見
猪突猛進なイメージがあるが、
統率者としての経験も豊富。休息
中も、仲間の様子に心を配る。",1600,20000,100,10,20,"hlog_107000150_kle_5_01_xxx","hlog_107000151_kle_5_01_xxx",False,"Chara_comment/107000150","Chara_comment/107000151","107000150_cha","107000151_cha","Chara_icon/107000150","Chara_icon/107000151","Chara_icon_loss/107000150","Chara_icon_loss/107000151","2020/07/01 7:00:00",True,"1", -108000160,8,"エギル","金剛なる体躯",5,9,4,0,108000160,208000000,-1,108000160,2160,217730,240,23952,100,9980,160,16330,1,0,0,1,0,0,2,0,0,2,0,0,"VR世界において肉体の強靭さは
数値化され、外見に依存しない。
だが、鍛え上げられた身体は時に
共に戦う者に安心感を与える。",1600,20000,100,10,20,"hlog_108000160_agi_5_01_xxx","hlog_108000161_agi_5_01_xxx",False,"Chara_comment/108000160","Chara_comment/108000161","108000160_cha","108000161_cha","Chara_icon/108000160","Chara_icon/108000161","Chara_icon_loss/108000160","Chara_icon_loss/108000161","2020/09/01 7:00:00",True,"1", -103000250,3,"リーファ","兄想う妖精",5,1,3,0,103000250,201000000,-1,103000250,1620,163300,174,17418,65,6530,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"危険を顧みず、兄を助けるため
SAO稼働後にダイブした彼女。
ALOからコンバートされた妖精
アバターはひときわ目を引く。",1600,20000,100,10,20,"hlog_103000250_lea_5_01_xxx","hlog_103000251_lea_5_01_xxx",False,"Chara_comment/103000250","Chara_comment/103000251","103000250_cha","103000251_cha","Chara_icon/103000250","Chara_icon/103000251","Chara_icon_loss/103000250","Chara_icon_loss/103000251","2020/09/22 7:00:00",True,"1", -104000290,4,"シノン","向き合う勇気",5,11,1,0,104000290,211000000,-1,104000290,1150,116120,288,28740,60,5805,200,19960,1,0,0,1,0,0,2,0,0,2,0,0,"治療のためメディキュボイドを用
いた所、誤ってSAOに来てしまっ
た彼女だが、出会った仲間達との
交流の中で痛みを克服してゆく。",1600,20000,100,10,20,"hlog_104000290_sin_5_01_xxx","hlog_104000291_sin_5_01_xxx",False,"Chara_comment/104000290","Chara_comment/104000291","104000290_cha","104000291_cha","Chara_icon/104000290","Chara_icon/104000291","Chara_icon_loss/104000290","Chara_icon_loss/104000291","2020/09/22 7:00:00",True,"1", -101000270,1,"キリト","クロス・イメージ",5,2,1,0,101000270,201000000,201000000,101000270,1440,145150,258,26130,70,7260,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"相棒の剣と、自身の剣。Unknown
で発見されたそれらは見覚えが無
いはずなのに、よく手に馴染む。
二刀を携え、英雄は何を想う。",1600,20000,100,10,20,"hlog_101000270_kir_5_01_xxx","hlog_101000271_kir_5_01_xxx",False,"Chara_comment/101000270","Chara_comment/101000271","101000270_cha","101000271_cha","Chara_icon/101000270","Chara_icon/101000271","Chara_icon_loss/101000270","Chara_icon_loss/101000271","2020/07/14 7:00:00",True,"1", -104000300,4,"シノン","戦場に咲く花",5,14,1,0,104000300,215000000,-1,104000300,1150,116120,288,28740,60,5805,200,19960,1,0,0,1,0,0,2,0,0,2,0,0,"GGOの苛烈な戦場において、女性
プレイヤーは珍しい存在だ。上位
の実力者でありながら可憐な容姿
の彼女は、特に目を引く存在だ。",1600,20000,100,10,20,"hlog_104000300_sin_5_01_xxx","hlog_104000301_sin_5_01_xxx",False,"Chara_comment/104000300","Chara_comment/104000301","104000300_cha","104000301_cha","Chara_icon/104000300","Chara_icon/104000301","Chara_icon_loss/104000300","Chara_icon_loss/104000301","2020/08/11 7:00:00",True,"1", -104000310,4,"シノン","予期せぬ接触",5,11,4,0,104000310,211000000,-1,104000310,1440,145150,240,23952,70,7260,200,19960,1,0,0,1,0,0,2,0,0,2,0,0,"現実の身体には存在しない器官で
ある三角耳と尻尾。触られると
《すっごいヘンな感じ》がすると
のことだが、彼女の場合は――?",1600,20000,100,10,20,"hlog_104000310_sin_5_01_xxx","hlog_104000311_sin_5_01_xxx",False,"Chara_comment/104000310","Chara_comment/104000311","104000310_cha","104000311_cha","Chara_icon/104000310","Chara_icon/104000311","Chara_icon_loss/104000310","Chara_icon_loss/104000311","2020/08/11 7:00:00",True,"1", -109000240,9,"ユウキ","君との内緒事",5,11,1,0,109000240,211000000,-1,109000240,1440,145150,288,28740,65,6530,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"普段は天真爛漫な彼女だが、ふい
に見せる大人びた仕草には心を
揺さぶられる。ボクと君、ふたり
だけの秘密…だよ?",1600,20000,100,10,20,"hlog_109000240_yuu_5_01_xxx","hlog_109000241_yuu_5_01_xxx",False,"Chara_comment/109000240","Chara_comment/109000241","109000240_cha","109000241_cha","Chara_icon/109000240","Chara_icon/109000241","Chara_icon_loss/109000240","Chara_icon_loss/109000241","2020/08/01 7:00:00",True,"1", -118000100,18,"レイン","赤面プラクティカ",5,1,3,0,118000100,201000000,-1,118000100,1460,146970,180,18288,80,8165,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"アイドルを目指す上で必要な事は
歌唱、演技だけに留まらない。皆
を魅了する振る舞いも重要だが、
まだ羞恥が滲み…練習あるのみ!",1600,20000,100,10,20,"hlog_118000100_rai_5_01_xxx","hlog_118000101_rai_5_01_xxx",False,"Chara_comment/118000100","Chara_comment/118000101","118000100_cha","118000101_cha","Chara_icon/118000100","Chara_icon/118000101","Chara_icon_loss/118000100","Chara_icon_loss/118000101","2020/10/01 7:00:00",True,"1", -111000170,11,"アリス","物思い耽る午後",5,1,1,0,111000170,201000000,-1,111000170,1660,166920,270,27432,85,8345,170,17240,1,0,0,1,0,0,2,0,0,2,0,0,"Unknownでの休息の記録。鎧も外
し安らぐ午後。物思いに耽る彼女
は無防備に穏やかな笑みを浮かべ
る。しかし目撃されたと知ると…",1600,20000,100,10,20,"hlog_111000170_alc_5_01_xxx","hlog_111000171_alc_5_01_xxx",False,"Chara_comment/111000170","Chara_comment/111000171","111000170_cha","111000171_cha","Chara_icon/111000170","Chara_icon/111000171","Chara_icon_loss/111000170","Chara_icon_loss/111000171","2020/06/23 7:00:00",True,"1", -118000110,18,"レイン","想い滲む眼差し",5,2,1,0,118000110,201000000,201000000,118000110,1300,130640,270,27432,70,7260,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"人見知りがちな彼女だが、親しく
なれば砕けた様子で笑顔を見せて
くれる。その眼差しに浮かぶのは
信頼か、それとも――。",1600,20000,100,10,20,"hlog_118000110_rai_5_01_xxx","hlog_118000111_rai_5_01_xxx",False,"Chara_comment/118000110","Chara_comment/118000111","118000110_cha","118000111_cha","Chara_icon/118000110","Chara_icon/118000111","Chara_icon_loss/118000110","Chara_icon_loss/118000111","2020/08/25 7:00:00",True,"1", -102000290,2,"アスナ","導きの女神",5,3,4,0,102000290,202000000,-1,102000290,1620,163300,216,21774,80,8165,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"美しい御姿と、強大な権限を併せ
持つ彼女は、人界の人々にとって
正しく女神そのものだ。女神の導
きの先に待つは如何なる結末か。",1600,20000,100,10,20,"hlog_102000290_asu_5_01_xxx","hlog_102000291_asu_5_01_xxx",False,"Chara_comment/102000290","Chara_comment/102000291","102000290_cha","102000291_cha","Chara_icon/102000290","Chara_icon/102000291","Chara_icon_loss/102000290","Chara_icon_loss/102000291","2020/07/09 7:00:00",False,"1", -102000300,2,"アスナ","創世神ステイシア",5,3,3,0,102000300,202000000,-1,102000300,1460,146970,174,17418,75,7350,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"地形を意のままに操るその権能は
創世の神に相応しい。しかし人智
の及ばぬ強大な力は、振るう者に
大きな痛みをも与える。",1600,20000,100,10,20,"hlog_102000300_asu_5_01_xxx","hlog_102000301_asu_5_01_xxx",False,"Chara_comment/102000300","Chara_comment/102000301","102000300_cha","102000301_cha","Chara_icon/102000300","Chara_icon/102000301","Chara_icon_loss/102000300","Chara_icon_loss/102000301","2020/07/14 7:00:00",True,"1", -103000260,3,"リーファ","地神テラリア",5,1,4,0,103000260,201000000,-1,103000260,1800,181440,216,21774,70,7260,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"大地の力を受けた回復能力により
無限とも思える程の体力を誇る。
だが痛覚はあり、立ち続けるには
並みならぬ精神力が要求される。",1600,20000,100,10,20,"hlog_103000260_lea_5_01_xxx","hlog_103000261_lea_5_01_xxx",False,"Chara_comment/103000260","Chara_comment/103000261","103000260_cha","103000261_cha","Chara_icon/103000260","Chara_icon/103000261","Chara_icon_loss/103000260","Chara_icon_loss/103000261","2020/07/14 7:00:00",True,"1", -104000320,4,"シノン","太陽神ソルス",5,11,1,0,104000320,211000000,-1,104000320,1150,116120,288,28740,60,5805,200,19960,1,0,0,1,0,0,2,0,0,2,0,0,"広範囲殲滅攻撃と無制限飛行能力
を持つ特別なアバター。大規模な
能力を使いこなすには、研ぎ澄ま
されたイメージ力を必要とする。",1600,20000,100,10,20,"hlog_104000320_sin_5_01_xxx","hlog_104000321_sin_5_01_xxx",False,"Chara_comment/104000320","Chara_comment/104000321","104000320_cha","104000321_cha","Chara_icon/104000320","Chara_icon/104000321","Chara_icon_loss/104000320","Chara_icon_loss/104000321","2020/07/14 7:00:00",True,"1", -109000250,9,"ユウキ","英雄纏う剣才",5,2,1,0,109000250,201000000,201000000,109000250,1440,145150,288,28740,65,6530,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"《絶剣》と呼ばれた少女の剣才は
他の追随を許さない。英雄の色彩
に身を包み、二刀を振るう姿は
神速の如し。",1600,20000,100,10,20,"hlog_109000250_yuu_5_01_xxx","hlog_109000251_yuu_5_01_xxx",False,"Chara_comment/109000250","Chara_comment/109000251","109000250_cha","109000251_cha","Chara_icon/109000250","Chara_icon/109000251","Chara_icon_loss/109000250","Chara_icon_loss/109000251","2020/07/21 7:00:00",True,"1", -112000120,12,"ユージオ","忘却の憧憬",5,2,3,0,112000120,201000000,201000000,112000120,1620,163300,174,17418,80,8165,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"かつて憧れた、友であり師でもあ
る最強の剣士。例え記憶は無くし
ても、その温かな黒は、凍てつい
た騎士の胸を静かに焦がす。",1600,20000,100,10,20,"hlog_112000120_ego_5_01_xxx","hlog_112000121_ego_5_01_xxx",False,"Chara_comment/112000120","Chara_comment/112000121","112000120_cha","112000121_cha","Chara_icon/112000120","Chara_icon/112000121","Chara_icon_loss/112000120","Chara_icon_loss/112000121","2020/07/21 7:00:00",True,"1", -111000180,11,"アリス","劇甚なる金木犀",5,1,4,0,111000180,201000000,-1,111000180,2070,208660,228,22860,105,10435,170,17240,1,0,0,1,0,0,2,0,0,2,0,0,"アリスの持つ《金木犀の剣》は
強力な神器だが、その真価は応用
力の高さにある。使い手の意志が
この神器を最強たらしめるのだ。",1600,20000,100,10,20,"hlog_111000180_alc_5_01_xxx","hlog_111000181_alc_5_01_xxx",False,"Chara_comment/111000180","Chara_comment/111000181","111000180_cha","111000181_cha","Chara_icon/111000180","Chara_icon/111000181","Chara_icon_loss/111000180","Chara_icon_loss/111000181","2020/11/10 7:00:00",True,"1", -106000230,6,"シリカ","蒼撃の竜姫",4,4,3,0,106000230,203000000,-1,106000230,1150,116120,138,13932,60,5805,230,23220,1,0,0,1,0,0,2,0,0,2,0,0,"SAOの中では年少に分類されるシ
リカ。幼い彼女が戦線に立ち、心
身無事に事件を生き延びたのは極
めて稀なケースである。",800,10000,100,10,20,"hlog_106000230_sil_4_01_xxx","hlog_106000230_sil_4_01_xxx",False,"Chara_comment/106000230","Chara_comment/106000230","106000230_cha","106000230_cha","Chara_icon/106000230","Chara_icon/106000230","Chara_icon_loss/106000230","Chara_icon_loss/106000230","2020/11/10 7:00:00",True,"1", -109000260,9,"ユウキ","光速の絶技",4,1,4,0,109000260,201000000,-1,109000260,1600,161280,210,21288,70,7260,160,16130,1,0,0,1,0,0,2,0,0,2,0,0,"新生ALOに突如現れた闇妖精族の
剣士。目で追うことすら敵わぬ剣
の武を指し、人は彼女を《絶剣》
と呼んだ。",800,10000,100,10,20,"hlog_109000260_yuu_4_01_xxx","hlog_109000260_yuu_4_01_xxx",False,"Chara_comment/109000260","Chara_comment/109000260","109000260_cha","109000260_cha","Chara_icon/109000260","Chara_icon/109000260","Chara_icon_loss/109000260","Chara_icon_loss/109000260","2020/11/10 7:00:00",True,"1", -110000180,10,"アルゴ","疾鼠の刻爪",4,8,3,0,110000180,207000000,-1,110000180,1370,137890,144,14706,70,6895,230,23220,1,0,0,1,0,0,2,0,0,2,0,0,"情報屋《鼠のアルゴ》。SAOβテ
スターでもあった彼女は、中堅以
下のプレイヤーに攻略情報を配布
するなど、支援にも注力した。",800,10000,100,10,20,"hlog_110000180_arg_4_01_xxx","hlog_110000180_arg_4_01_xxx",False,"Chara_comment/110000180","Chara_comment/110000180","110000180_cha","110000180_cha","Chara_icon/110000180","Chara_icon/110000180","Chara_icon_loss/110000180","Chara_icon_loss/110000180","2020/11/10 7:00:00",True,"1", -111000190,11,"アリス","陽だまりの円舞曲",5,3,1,0,111000190,202000000,-1,111000190,1660,166920,270,27432,85,8345,170,17240,1,0,0,1,0,0,2,0,0,2,0,0,"1曲踊ろうと手を伸べれば、躊躇
いながら手を取ってくれる。俗世
の文化には疎い騎士様だが、興味
が無い訳ではないのかも。",1600,20000,100,10,20,"hlog_111000190_alc_5_01_xxx","hlog_111000191_alc_5_01_xxx",False,"Chara_comment/111000190","Chara_comment/111000191","111000190_cha","111000191_cha","Chara_icon/111000190","Chara_icon/111000191","Chara_icon_loss/111000190","Chara_icon_loss/111000191","2020/08/25 7:00:00",True,"1", -104000330,4,"シノン","悪戯な視線",5,4,1,0,104000330,203000000,-1,104000330,1150,116120,288,28740,60,5805,200,19960,1,0,0,1,0,0,2,0,0,2,0,0,"Unknownでの休息の記録。慣れ親
しんだキリトとアスナの家で皆思
い思いに寛ぐ。ふと視線を感じる
と、笑みを浮かべた彼女が…。",1600,20000,100,10,20,"hlog_104000330_sin_5_01_xxx","hlog_104000331_sin_5_01_xxx",False,"Chara_comment/104000330","Chara_comment/104000331","104000330_cha","104000331_cha","Chara_icon/104000330","Chara_icon/104000331","Chara_icon_loss/104000330","Chara_icon_loss/104000331","2020/08/01 7:00:00",True,"1", -112000130,12,"ユージオ","花舞う昼下がり",5,1,3,0,112000130,201000000,-1,112000130,1620,163300,174,17418,80,8165,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"Unknownでの休息の記録。穏やか
な日差しの中、ふいに風が吹き抜
ける。花弁が舞い上がり、誘われ
るように掌に一枚ふわり。",1600,20000,100,10,20,"hlog_112000130_ego_5_01_xxx","hlog_112000131_ego_5_01_xxx",False,"Chara_comment/112000130","Chara_comment/112000131","112000130_cha","112000131_cha","Chara_icon/112000130","Chara_icon/112000131","Chara_icon_loss/112000130","Chara_icon_loss/112000131","2020/09/01 7:00:00",True,"1", -106000240,6,"シリカ","おやつ・タイム!",5,3,3,0,106000240,202000000,-1,106000240,1300,130640,156,15678,65,6530,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"Unknownでの休息の記録。シリカ
とピナにとって食事は大切な触れ
合いの時間。大好きなご主人から
大好物のナッツを貰ってご満悦。",1600,20000,100,10,20,"hlog_106000240_sil_5_01_xxx","hlog_106000241_sil_5_01_xxx",False,"Chara_comment/106000240","Chara_comment/106000241","106000240_cha","106000241_cha","Chara_icon/106000240","Chara_icon/106000241","Chara_icon_loss/106000240","Chara_icon_loss/106000241","2020/10/01 7:00:00",True,"1", -105000230,5,"リズベット","目映い太陽",5,10,4,1,105000230,209000000,-1,105000230,1800,181440,192,19596,110,10885,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"日差しを受けて煌めく、弾けるよ
うなとびきりの笑顔。真夏が良く
似合う太陽のような彼女となら、
最高の思い出が作れるはず。",1600,20000,100,10,20,"hlog_105000230_lis_5_01_xxx","hlog_105000231_lis_5_01_xxx",False,"Chara_comment/105000230","Chara_comment/105000231","105000230_cha","105000231_cha","Chara_icon/105000230","Chara_icon/105000231","Chara_icon_loss/105000230","Chara_icon_loss/105000231","2020/08/11 7:00:00",True,"1", -106000250,6,"シリカ","白砂の少女",5,4,4,1,106000250,203000000,-1,106000250,1440,145150,192,19596,70,7260,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"ちょこんと座り、少女はこちらを
見上げる。その姿は、日差しに輝
く白い砂浜よりも眩しく愛らしく
見る者を魅了することだろう。",1600,20000,100,10,20,"hlog_106000250_sil_5_01_xxx","hlog_106000251_sil_5_01_xxx",False,"Chara_comment/106000250","Chara_comment/106000251","106000250_cha","106000251_cha","Chara_icon/106000250","Chara_icon/106000251","Chara_icon_loss/106000250","Chara_icon_loss/106000251","2020/08/11 7:00:00",True,"1", -111000200,11,"アリス","透き通る青",5,1,4,1,111000200,201000000,-1,111000200,2070,208660,228,22860,105,10435,170,17240,1,0,0,1,0,0,2,0,0,2,0,0,"普段は鎧に隠された透き通るよう
な肌に、金糸のような髪が風にな
びく。鮮やかな青い水着が良く似
合う、海に相応しい出で立ちだ。",1600,20000,100,10,20,"hlog_111000200_alc_5_01_xxx","hlog_111000201_alc_5_01_xxx",False,"Chara_comment/111000200","Chara_comment/111000201","111000200_cha","111000201_cha","Chara_icon/111000200","Chara_icon/111000201","Chara_icon_loss/111000200","Chara_icon_loss/111000201","2020/08/11 7:00:00",True,"1", -103000270,3,"リーファ","灼熱!アタックガール",5,6,4,1,103000270,205000000,-1,103000270,1800,181440,216,21774,70,7260,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"Unknownで行われることになった
ビーチバレー大会。競技は違えど
体育会系の血が騒ぐ!!この夏、
誰よりも高く、少女は跳ぶ!",1600,20000,100,10,20,"hlog_103000270_lea_5_01_xxx","hlog_103000271_lea_5_01_xxx",False,"Chara_comment/103000270","Chara_comment/103000271","103000270_cha","103000271_cha","Chara_icon/103000270","Chara_icon/103000271","Chara_icon_loss/103000270","Chara_icon_loss/103000271","2020/08/11 7:00:00",True,"1", -102000310,2,"アスナ","潮風の悪戯",5,3,3,1,102000310,202000000,-1,102000310,1460,146970,174,17418,75,7350,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"磯の香りに誘われて、波打ち際へ
と歩いてみれば、悪戯な潮風が頬
を撫でる。穏やかで美しい、大切
な海の思い出。",1600,20000,100,10,20,"hlog_102000310_asu_5_01_xxx","hlog_102000311_asu_5_01_xxx",False,"Chara_comment/102000310","Chara_comment/102000311","102000310_cha","102000311_cha","Chara_icon/102000310","Chara_icon/102000311","Chara_icon_loss/102000310","Chara_icon_loss/102000311","2020/08/11 7:00:00",True,"1", -101000280,1,"キリト","現実を超えて",5,1,3,0,101000280,201000000,-1,101000280,1620,163300,174,17418,80,8165,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"仮想世界において伝説となってい
る彼だが、この《OS》には乗り気
ではなかった。守るべき者のため
現実も仮想も超え、彼は駆ける。",1600,20000,100,10,20,"hlog_101000280_kir_5_01_xxx","hlog_101000281_kir_5_01_xxx",False,"Chara_comment/101000280","Chara_comment/101000281","101000280_cha","101000281_cha","Chara_icon/101000280","Chara_icon/101000281","Chara_icon_loss/101000280","Chara_icon_loss/101000281","2020/09/08 7:00:00",True,"1", -102000320,2,"アスナ","鮮やかな閃光",5,3,1,0,102000320,202000000,-1,102000320,1300,130640,258,26130,65,6530,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"かつて《SAO》攻略を指揮した副
団長。その鮮やかな手腕は、現実
の肉体を用いて戦う《OS》におい
ても、色褪せることは無い。",1600,20000,100,10,20,"hlog_102000320_asu_5_01_xxx","hlog_102000321_asu_5_01_xxx",False,"Chara_comment/102000320","Chara_comment/102000321","102000320_cha","102000321_cha","Chara_icon/102000320","Chara_icon/102000321","Chara_icon_loss/102000320","Chara_icon_loss/102000321","2020/09/08 7:00:00",True,"1", -103000280,3,"リーファ","剣に乗せた想い",5,6,1,0,103000280,205000000,-1,103000280,1440,145150,258,26130,60,5805,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"剣道の全国大会で入賞するほどの
実力者である彼女。《OS》でも
その剣捌きに迷いはない。現実の
肉体で繰り出す技の数々は圧巻。",1600,20000,100,10,20,"hlog_103000280_lea_5_01_xxx","hlog_103000281_lea_5_01_xxx",False,"Chara_comment/103000280","Chara_comment/103000281","103000280_cha","103000281_cha","Chara_icon/103000280","Chara_icon/103000281","Chara_icon_loss/103000280","Chara_icon_loss/103000281","2020/09/08 7:00:00",True,"1", -106000260,6,"シリカ","0と1の狭間で",5,4,3,0,106000260,203000000,-1,106000260,1300,130640,156,15678,65,6530,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"彼女をドラゴンテイマーたらしめ
た相棒・ピナは《OS》には存在し
ない。仮想と現実の狭間で、不安
に揺れる彼女の瞳は何を映す。",1600,20000,100,10,20,"hlog_106000260_sil_5_01_xxx","hlog_106000261_sil_5_01_xxx",False,"Chara_comment/106000260","Chara_comment/106000261","106000260_cha","106000261_cha","Chara_icon/106000260","Chara_icon/106000261","Chara_icon_loss/106000260","Chara_icon_loss/106000261","2020/09/08 7:00:00",True,"1", -104000340,4,"シノン","現を射抜く眼",5,14,1,0,104000340,215000000,-1,104000340,1150,116120,288,28740,60,5805,200,19960,1,0,0,1,0,0,2,0,0,2,0,0,"彼女の冷静な観察眼は、現実世界
においても変わらない。姿は変わ
れども、必中の弾丸は敵を屠る
のみだ。",1600,20000,100,10,20,"hlog_104000340_sin_5_01_xxx","hlog_104000341_sin_5_01_xxx",False,"Chara_comment/104000340","Chara_comment/104000341","104000340_cha","104000341_cha","Chara_icon/104000340","Chara_icon/104000341","Chara_icon_loss/104000340","Chara_icon_loss/104000341","2020/10/20 7:00:00",True,"1", -107000160,7,"クライン","同志と共に",5,6,3,0,107000160,205000000,-1,107000160,1780,179630,192,19158,80,8165,170,17420,1,0,0,1,0,0,2,0,0,2,0,0,"現実での交流もある《風林火山》
のメンバーは、ゲーム内外で揃い
の装備に身を包み、攻略組らしい
息の合ったプレイングを見せる。",1600,20000,100,10,20,"hlog_107000160_kle_5_01_xxx","hlog_107000161_kle_5_01_xxx",False,"Chara_comment/107000160","Chara_comment/107000161","107000160_cha","107000161_cha","Chara_icon/107000160","Chara_icon/107000161","Chara_icon_loss/107000160","Chara_icon_loss/107000161","2020/10/20 7:00:00",True,"1", -109000270,9,"ユウキ","仮想を駆けて",5,1,1,0,109000270,201000000,-1,109000270,1440,145150,288,28740,65,6530,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"現実の自由な肉体を必要とする
《オーディナル・スケール》。
たとえ今は叶わずとも、いつか共
に現実を駆け抜ける日を夢見て。",1600,20000,100,10,20,"hlog_109000270_yuu_5_01_xxx","hlog_109000271_yuu_5_01_xxx",False,"Chara_comment/109000270","Chara_comment/109000271","109000270_cha","109000271_cha","Chara_icon/109000270","Chara_icon/109000271","Chara_icon_loss/109000270","Chara_icon_loss/109000271","2020/10/20 7:00:00",True,"1", -105000240,5,"リズベット","変わらないもの",5,10,1,0,105000240,209000000,-1,105000240,1440,145150,234,23514,85,8710,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"現実であろうと、仮想世界であろ
うと、心の在り処は変わらない。
大切な友を思う気持ちは、舞台が
どこであれ関係ないものだ。",1600,20000,100,10,20,"hlog_105000240_lis_5_01_xxx","hlog_105000241_lis_5_01_xxx",False,"Chara_comment/105000240","Chara_comment/105000241","105000240_cha","105000241_cha","Chara_icon/105000240","Chara_icon/105000241","Chara_icon_loss/105000240","Chara_icon_loss/105000241","2020/10/20 7:00:00",True,"1", -102000330,2,"アスナ","羽ばたく希望",5,3,1,0,102000330,202000000,-1,102000330,1300,130640,258,26130,65,6530,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"強大な邪悪を前に、立ち向かう
勇気を宿した時、女神は大きく
飛翔する。世界中に希望を与える
大切な剣技を、今――。",1600,20000,100,10,20,"hlog_102000330_asu_5_01_xxx","hlog_102000331_asu_5_01_xxx",False,"Chara_comment/102000330","Chara_comment/102000331","102000330_cha","102000331_cha","Chara_icon/102000330","Chara_icon/102000331","Chara_icon_loss/102000330","Chara_icon_loss/102000331","2020/11/24 7:00:00",True,"1", -103000290,3,"リーファ","倒れぬ意志",5,1,1,0,103000290,201000000,-1,103000290,1440,145150,258,26130,60,5805,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"テラリアの《無制限自動回復》は
どんなに傷ついても倒れる事が許
されぬ呪いのような力だ。全ての
友のため、その意志は折れない。",1600,20000,100,10,20,"hlog_103000290_lea_5_01_xxx","hlog_103000291_lea_5_01_xxx",False,"Chara_comment/103000290","Chara_comment/103000291","103000290_cha","103000291_cha","Chara_icon/103000290","Chara_icon/103000291","Chara_icon_loss/103000290","Chara_icon_loss/103000291","2020/10/06 7:00:00",True,"1", -104000350,4,"シノン","見据える勝利",5,14,1,0,104000350,215000000,-1,104000350,1150,116120,288,28740,60,5805,200,19960,1,0,0,1,0,0,2,0,0,2,0,0,"諦めない心、強い意志力が呼び出
したのは、彼女の歩みを常に支え
てきた愛銃だった。己を、愛銃を
信じて、放つは勝利の弾丸。",1600,20000,100,10,20,"hlog_104000350_sin_5_01_xxx","hlog_104000351_sin_5_01_xxx",False,"Chara_comment/104000350","Chara_comment/104000351","104000350_cha","104000351_cha","Chara_icon/104000350","Chara_icon/104000351","Chara_icon_loss/104000350","Chara_icon_loss/104000351","2020/10/06 7:00:00",True,"1", -109000280,9,"ユウキ","輝く勇気",5,1,1,0,109000280,201000000,-1,109000280,1440,145150,288,28740,65,6530,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"少女の意志は、想いは、美しく
力強い剣技の中で永遠に輝き続け
る。隣に寄り添う魂が、何物にも
汚される事のない勇気を与える。",1600,20000,100,10,20,"hlog_109000280_yuu_5_01_xxx","hlog_109000281_yuu_5_01_xxx",False,"Chara_comment/109000280","Chara_comment/109000281","109000280_cha","109000281_cha","Chara_icon/109000280","Chara_icon/109000281","Chara_icon_loss/109000280","Chara_icon_loss/109000281","2020/11/24 7:00:00",True,"1", -110000190,10,"アルゴ","隠された素顔",5,8,3,0,110000190,207000000,-1,110000190,1540,155130,162,16548,75,7755,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"情報屋を営む彼女は、その性質故
ミステリアスな存在だ。彼女が
何を想い、何を成そうとしている
のか――それは誰にも秘密。",1600,20000,100,10,20,"hlog_110000190_arg_5_01_xxx","hlog_110000191_arg_5_01_xxx",False,"Chara_comment/110000190","Chara_comment/110000191","110000190_cha","110000191_cha","Chara_icon/110000190","Chara_icon/110000191","Chara_icon_loss/110000190","Chara_icon_loss/110000191","2020/11/01 7:00:00",True,"1", -106000270,6,"シリカ","あどけない笑み",5,4,3,0,106000270,203000000,-1,106000270,1300,130640,156,15678,65,6530,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"まだ幼さの残る少女は、無邪気に
輝く笑顔を見せてくれる。けれど
不意に見せる、親愛滲む大人びた
笑みも成長しゆく彼女の魅力だ。",1600,20000,100,10,20,"hlog_106000270_sil_5_01_xxx","hlog_106000271_sil_5_01_xxx",False,"Chara_comment/106000270","Chara_comment/106000271","106000270_cha","106000271_cha","Chara_icon/106000270","Chara_icon/106000271","Chara_icon_loss/106000270","Chara_icon_loss/106000271","2020/12/01 7:00:00",True,"1", -106000280,6,"シリカ","駆け出す闘志",5,4,3,0,106000280,203000000,-1,106000280,1300,130640,156,15678,65,6530,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"SAOでの経験は、彼女を強くして
くれた。戦闘にも自信をつけて
意気込んで駆け出す彼女だが、
後ろのガードが緩んでいるかも?",1600,20000,100,10,20,"hlog_106000280_sil_5_01_xxx","hlog_106000281_sil_5_01_xxx",False,"Chara_comment/106000280","Chara_comment/106000281","106000280_cha","106000281_cha","Chara_icon/106000280","Chara_icon/106000281","Chara_icon_loss/106000280","Chara_icon_loss/106000281","2020/10/06 7:00:00",True,"1", -106000290,6,"シリカ","ちゃーみー・けっとしー",5,4,1,0,106000290,203000000,-1,106000290,1150,116120,234,23514,60,5805,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"相棒のピナのため選択した猫妖精
だが、その見た目の愛らしさも
お気に入り?恥ずかしがりながら
も、可愛い仕草がよく似合う。",1600,20000,100,10,20,"hlog_106000290_sil_5_01_xxx","hlog_106000291_sil_5_01_xxx",False,"Chara_comment/106000290","Chara_comment/106000291","106000290_cha","106000291_cha","Chara_icon/106000290","Chara_icon/106000291","Chara_icon_loss/106000290","Chara_icon_loss/106000291","2020/10/06 7:00:00",True,"1", -101000290,1,"キリト","可憐な銃剣士",5,1,1,0,101000290,201000000,-1,101000290,1440,145150,258,26130,70,7260,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"レアリティの高い可憐なアバター
もさることながら、BoBでの
剣を主とした異質な戦闘方法と
あまりの強さで一躍有名に。",1600,20000,100,10,20,"hlog_101000290_kir_5_01_xxx","hlog_101000291_kir_5_01_xxx",False,"Chara_comment/101000290","Chara_comment/101000291","101000290_cha","101000291_cha","Chara_icon/101000290","Chara_icon/101000291","Chara_icon_loss/101000290","Chara_icon_loss/101000291","2020/11/03 7:00:00",True,"1", -104000360,4,"シノン","美麗な狙撃手",5,14,1,0,104000360,215000000,-1,104000360,1150,116120,288,28740,60,5805,200,19960,1,0,0,1,0,0,2,0,0,2,0,0,"美しさと強さを兼ね備えた狙撃手
としてGGO内でも知らぬ者は
いない。そんな彼女がペースを崩
される相手がいるそうで…?",1600,20000,100,10,20,"hlog_104000360_sin_5_01_xxx","hlog_104000361_sin_5_01_xxx",False,"Chara_comment/104000360","Chara_comment/104000361","104000360_cha","104000361_cha","Chara_icon/104000360","Chara_icon/104000361","Chara_icon_loss/104000360","Chara_icon_loss/104000361","2020/11/03 7:00:00",True,"1", -103000300,3,"リーファ","妖精の戯れ",5,10,1,0,103000300,209000000,-1,103000300,1440,145150,258,26130,60,5805,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"Unknownでの休息の記録。
花畑で無邪気にはしゃぐその姿は
SAOで妖精に間違えられたことも
頷ける可愛らしさだ。",1600,20000,100,10,20,"hlog_103000300_lea_5_01_xxx","hlog_103000301_lea_5_01_xxx",False,"Chara_comment/103000300","Chara_comment/103000301","103000300_cha","103000301_cha","Chara_icon/103000300","Chara_icon/103000301","Chara_icon_loss/103000300","Chara_icon_loss/103000301","2020/11/01 7:00:00",True,"1", -102000340,2,"アスナ","穏やかな一時",5,11,3,0,102000340,211000000,-1,102000340,1460,146970,174,17418,75,7350,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"Unknownでの休息の記録。住み慣
れた我が家で、温かな紅茶を片手
に穏やかな時を過ごす。束の間の
日常が心を癒してくれるだろう。",1600,20000,100,10,20,"hlog_102000340_asu_5_01_xxx","hlog_102000341_asu_5_01_xxx",False,"Chara_comment/102000340","Chara_comment/102000341","102000340_cha","102000341_cha","Chara_icon/102000340","Chara_icon/102000341","Chara_icon_loss/102000340","Chara_icon_loss/102000341","2020/12/01 7:00:00",True,"1", -102000350,2,"アスナ","思い出の味",4,3,3,0,102000350,202000000,-1,102000350,1300,130640,156,15480,65,6530,230,23220,1,0,0,1,0,0,2,0,0,2,0,0,"Unknownに現れた思い出の味に
舌つづみ。皆思い思いの料理を
口に運び、幸せそうに顔を
綻ばせる。",800,10000,100,10,20,"hlog_102000350_asu_4_01_xxx","hlog_102000350_asu_4_01_xxx",False,"Chara_comment/102000350","Chara_comment/102000350","102000350_cha","102000350_cha","Chara_icon/102000350","Chara_icon/102000350","Chara_icon_loss/102000350","Chara_icon_loss/102000350","2020/11/03 7:00:00",True,"1", -101000300,1,"キリト","共に歩む時",5,2,4,0,101000300,201000000,201000000,101000300,1800,181440,216,21774,90,9070,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"剣に宿った意志が英雄の背中を
優しく、力強く押す。そうして
英雄は再び歩み始める。思い出は
ここに――永遠に、ここにある。",1600,20000,100,10,20,"hlog_101000300_kir_5_01_xxx","hlog_101000301_kir_5_01_xxx",False,"Chara_comment/101000300","Chara_comment/101000301","101000300_cha","101000301_cha","Chara_icon/101000300","Chara_icon/101000301","Chara_icon_loss/101000300","Chara_icon_loss/101000301","2020/10/20 7:00:00",True,"1", -102000360,2,"アスナ","立ち上がる時",5,3,4,0,102000360,202000000,-1,102000360,1620,163300,216,21774,80,8165,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"世界の全てを覆うような苛烈な
絶望の渦に、一筋の光が射す。
かつて託された希望のロザリオが
立ち上がる力をくれる。",1600,20000,100,10,20,"hlog_102000360_asu_5_01_xxx","hlog_102000361_asu_5_01_xxx",False,"Chara_comment/102000360","Chara_comment/102000361","102000360_cha","102000361_cha","Chara_icon/102000360","Chara_icon/102000361","Chara_icon_loss/102000360","Chara_icon_loss/102000361","2020/10/20 7:00:00",True,"1", -103000310,3,"リーファ","心繋がる時",5,1,4,0,103000310,201000000,-1,103000310,1800,181440,216,21774,70,7260,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"彼女にとっては相手の姿形も、
それが人間であるかさえ、些細な
要素に過ぎない。心結んだ全ての
者を守るため、彼女は屈しない。",1600,20000,100,10,20,"hlog_103000310_lea_5_01_xxx","hlog_103000311_lea_5_01_xxx",False,"Chara_comment/103000310","Chara_comment/103000311","103000310_cha","103000311_cha","Chara_icon/103000310","Chara_icon/103000311","Chara_icon_loss/103000310","Chara_icon_loss/103000311","2020/10/20 7:00:00",True,"1", -104000370,4,"シノン","意志示す時",5,11,4,0,104000370,211000000,-1,104000370,1440,145150,240,23952,70,7260,200,19960,1,0,0,1,0,0,2,0,0,2,0,0,"圧倒的な強さを前に、魂すら食ら
われそうになろうとも、決して諦
めることはない。本当の強さとは
何か、既に知っているから。",1600,20000,100,10,20,"hlog_104000370_sin_5_01_xxx","hlog_104000371_sin_5_01_xxx",False,"Chara_comment/104000370","Chara_comment/104000371","104000370_cha","104000371_cha","Chara_icon/104000370","Chara_icon/104000371","Chara_icon_loss/104000370","Chara_icon_loss/104000371","2020/10/20 7:00:00",True,"1", -109000290,9,"ユウキ","背中押す時",5,1,4,0,109000290,201000000,-1,109000290,1800,181440,240,23952,80,8165,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"いつでも寄り添ってくれる少女の
心は、世界の命運を背負う女神に
前を向く力を与える。君なら大丈
夫と励ます声が聞こえるようだ。",1600,20000,100,10,20,"hlog_109000290_yuu_5_01_xxx","hlog_109000291_yuu_5_01_xxx",False,"Chara_comment/109000290","Chara_comment/109000291","109000290_cha","109000291_cha","Chara_icon/109000290","Chara_icon/109000291","Chara_icon_loss/109000290","Chara_icon_loss/109000291","2020/10/20 7:00:00",True,"1", -103000320,3,"リーファ","ジョイフルキャロル",5,1,3,3,103000320,201000000,-1,103000320,1620,163300,174,17418,65,6530,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"幸せの音色に乗せて、楽しさを
お届け!しがらみなんて忘れて、
皆で手を取り合って、聖なる夜を
お祝いしよう。",1600,20000,100,10,20,"hlog_103000320_lea_5_01_xxx","hlog_103000321_lea_5_01_xxx",False,"Chara_comment/103000320","Chara_comment/103000321","103000320_cha","103000321_cha","Chara_icon/103000320","Chara_icon/103000321","Chara_icon_loss/103000320","Chara_icon_loss/103000321","2020/12/08 7:00:00",True,"1", -104000380,4,"シノン","ホワイトジングル",5,14,1,3,104000380,215000000,-1,104000380,1150,116120,288,28740,60,5805,200,19960,1,0,0,1,0,0,2,0,0,2,0,0,"厳かなホワイトクリスマス。耳を
すませば、微かに鈴の音が響く。
ちゃんと寝ていた良い子には、
素敵なプレゼントがあるかも?",1600,20000,100,10,20,"hlog_104000380_sin_5_01_xxx","hlog_104000381_sin_5_01_xxx",False,"Chara_comment/104000380","Chara_comment/104000381","104000380_cha","104000381_cha","Chara_icon/104000380","Chara_icon/104000381","Chara_icon_loss/104000380","Chara_icon_loss/104000381","2020/12/08 7:00:00",True,"1", -111000210,11,"アリス","スノープレゼント",5,1,1,3,111000210,201000000,-1,111000210,1660,166920,270,27432,85,8345,170,17240,1,0,0,1,0,0,2,0,0,2,0,0,"雪降る聖夜を彩るは、心を込めた
贈り物。善行に報いるべく、人々
に喜びを届けるサンタの使命を
帯びて。",1600,20000,100,10,20,"hlog_111000210_alc_5_01_xxx","hlog_111000211_alc_5_01_xxx",False,"Chara_comment/111000210","Chara_comment/111000211","111000210_cha","111000211_cha","Chara_icon/111000210","Chara_icon/111000211","Chara_icon_loss/111000210","Chara_icon_loss/111000211","2020/12/08 7:00:00",True,"1", -109000300,9,"ユウキ","サンタさん大作戦!",5,1,4,3,109000300,201000000,-1,109000300,1800,181440,240,23952,80,8165,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"クリスマスのわくわくを届けるた
め、サンタさんのドキドキを届け
るために。少女の夢は、小さな
サンタさんに託された!",1600,20000,100,10,20,"hlog_109000300_yuu_5_01_xxx","hlog_109000301_yuu_5_01_xxx",False,"Chara_comment/109000300","Chara_comment/109000301","109000300_cha","109000301_cha","Chara_icon/109000300","Chara_icon/109000301","Chara_icon_loss/109000300","Chara_icon_loss/109000301","2020/12/08 7:00:00",True,"1", -102000370,2,"アスナ","プレゼント大作戦!",5,3,1,3,102000370,202000000,-1,102000370,1300,130640,258,26130,65,6530,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"クリスマスのお楽しみと言えば、
いつの間にか枕元に置かれた秘密
のプレゼント。少女に夢を与える
ため、一大計画が始まる!",1600,20000,100,10,20,"hlog_102000370_asu_5_01_xxx","hlog_102000371_asu_5_01_xxx",False,"Chara_comment/102000370","Chara_comment/102000371","102000370_cha","102000371_cha","Chara_icon/102000370","Chara_icon/102000371","Chara_icon_loss/102000370","Chara_icon_loss/102000371","2020/12/08 7:00:00",True,"1", -102000380,2,"アスナ","荒野の閃光",5,14,1,0,102000380,215000000,-1,102000380,1300,130640,258,26130,65,6530,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"フィールドは違えど、その攻撃の
鋭さは変わらない。2人の美しき
猛者が交わるとき、新たな歴史が
生まれるだろう。",1600,20000,100,10,20,"hlog_102000380_asu_5_01_xxx","hlog_102000381_asu_5_01_xxx",False,"Chara_comment/102000380","Chara_comment/102000381","102000380_cha","102000381_cha","Chara_icon/102000380","Chara_icon/102000381","Chara_icon_loss/102000380","Chara_icon_loss/102000381","2021/01/12 7:00:00",True,"1", -104000390,4,"シノン","血盟の狙撃手",5,3,3,0,104000390,202000000,-1,104000390,1300,130640,192,19158,65,6530,240,23950,1,0,0,1,0,0,2,0,0,2,0,0,"孤独な戦士であった彼女は、今、
友の誓いを纏い新たな強さを手に
入れる。武器は違えど、的確に
急所を貫く戦い方は変わらない。",1600,20000,100,10,20,"hlog_104000390_sin_5_01_xxx","hlog_104000391_sin_5_01_xxx",False,"Chara_comment/104000390","Chara_comment/104000391","104000390_cha","104000391_cha","Chara_icon/104000390","Chara_icon/104000391","Chara_icon_loss/104000390","Chara_icon_loss/104000391","2021/01/12 7:00:00",True,"1", -102000390,2,"アスナ","厳正な副団長",5,3,4,0,102000390,202000000,-1,102000390,1620,163300,216,21774,80,8165,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"SAOにおいて攻略組を率いるこ
とが出来たのも、彼女の厳正さが
あってのことだろう。確実に勝利
に導くために不可欠な能力だ。",1600,20000,100,10,20,"hlog_102000390_asu_5_01_xxx","hlog_102000391_asu_5_01_xxx",False,"Chara_comment/102000390","Chara_comment/102000391","102000390_cha","102000391_cha","Chara_icon/102000390","Chara_icon/102000391","Chara_icon_loss/102000390","Chara_icon_loss/102000391","2020/12/01 7:00:00",True,"1", -102000400,2,"アスナ","麗姿の眼差し",5,3,3,0,102000400,202000000,-1,102000400,1460,146970,174,17418,75,7350,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"その麗しい視線につかまれば、
誰もが心奪われることだろう。
それはさながら、伝承の
ウンディーネのよう。",1600,20000,100,10,20,"hlog_102000400_asu_5_01_xxx","hlog_102000401_asu_5_01_xxx",False,"Chara_comment/102000400","Chara_comment/102000401","102000400_cha","102000401_cha","Chara_icon/102000400","Chara_icon/102000401","Chara_icon_loss/102000400","Chara_icon_loss/102000401","2020/12/01 7:00:00",True,"1", -111000220,11,"アリス","光の巫女",5,4,3,0,111000220,203000000,-1,111000220,1860,187790,180,18288,95,9390,210,20680,1,0,0,1,0,0,2,0,0,2,0,0,"彼女の高潔な魂は、光の巫女と
呼ばれるに相応しい。大切な人の
ため、世界のために、少女は
ひたむきな祈りを捧げる。",1600,20000,100,10,20,"hlog_111000220_alc_5_01_xxx","hlog_111000221_alc_5_01_xxx",False,"Chara_comment/111000220","Chara_comment/111000221","111000220_cha","111000221_cha","Chara_icon/111000220","Chara_icon/111000221","Chara_icon_loss/111000220","Chara_icon_loss/111000221","2021/01/01 7:00:00",True,"1", -112000140,12,"ユージオ","休剣の手入れ",5,1,3,0,112000140,201000000,-1,112000140,1620,163300,174,17418,80,8165,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"剣士にとって、愛剣は自分自身の
写し身のような、はたまた相棒の
ような、特別な存在。日々の手入
れは欠かせない。",1600,20000,100,10,20,"hlog_112000140_ego_5_01_xxx","hlog_112000141_ego_5_01_xxx",False,"Chara_comment/112000140","Chara_comment/112000141","112000140_cha","112000141_cha","Chara_icon/112000140","Chara_icon/112000141","Chara_icon_loss/112000140","Chara_icon_loss/112000141","2021/02/01 7:00:00",True,"1", -103000330,3,"リーファ","乙女の髪留め",5,1,3,0,103000330,201000000,-1,103000330,1620,163300,174,17418,65,6530,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"どんな時だって身支度を整えて
おくのが乙女の嗜み。きたる戦い
に備え、髪を留めなおして心を
落ち着けるのだ。",1600,20000,100,10,20,"hlog_103000330_lea_5_01_xxx","hlog_103000331_lea_5_01_xxx",False,"Chara_comment/103000330","Chara_comment/103000331","103000330_cha","103000331_cha","Chara_icon/103000330","Chara_icon/103000331","Chara_icon_loss/103000330","Chara_icon_loss/103000331","2021/01/12 7:00:00",True,"1", -107000170,7,"クライン","頭領の音頭",5,6,1,0,107000170,205000000,-1,107000170,1580,159670,288,28740,70,7260,140,14520,1,0,0,1,0,0,2,0,0,2,0,0,"攻略組の一角を担う《風林火山》
のリーダーたる彼の指揮能力は
かなりのものだ。磨いた連携と
結束力で強敵も難なく屠る。",1600,20000,100,10,20,"hlog_107000170_kle_5_01_xxx","hlog_107000171_kle_5_01_xxx",False,"Chara_comment/107000170","Chara_comment/107000171","107000170_cha","107000171_cha","Chara_icon/107000170","Chara_icon/107000171","Chara_icon_loss/107000170","Chara_icon_loss/107000171","2021/01/12 7:00:00",True,"1", -105000250,5,"リズベット","休日の談笑",5,8,1,0,105000250,207000000,-1,105000250,1440,145150,234,23514,85,8710,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"Unknownでの休息の記録。憩い
の家で友と語らう時間こそが、
彼女にとって何より大切な時間。
今日も賑やかな談笑が聞こえる。",1600,20000,100,10,20,"hlog_105000250_lis_5_01_xxx","hlog_105000251_lis_5_01_xxx",False,"Chara_comment/105000250","Chara_comment/105000251","105000250_cha","105000251_cha","Chara_icon/105000250","Chara_icon/105000251","Chara_icon_loss/105000250","Chara_icon_loss/105000251","2021/02/01 7:00:00",True,"1", -109000310,9,"ユウキ","さかさま花世界",5,10,1,0,109000310,209000000,-1,109000310,1440,145150,288,28740,65,6530,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"Unknownでの休息の記録。花畑
に寝ころんで、たまにはのんびり
過ごしてみよう。ほら、君がさか
さまになってるよ。",1600,20000,100,10,20,"hlog_109000310_yuu_5_01_xxx","hlog_109000311_yuu_5_01_xxx",True,"Chara_comment/109000310","Chara_comment/109000311","109000310_cha","109000311_cha","Chara_icon/109000310","Chara_icon/109000311","Chara_icon_loss/109000310","Chara_icon_loss/109000311","2021/01/01 7:00:00",True,"1", -101000310,1,"キリト","伝説の始まり",5,2,4,0,101000310,201000000,201000000,101000310,1800,181440,216,21774,90,9070,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"《SAO》攻略組の中でも一目
おかれる実力者。自分が伝説を
生み出すことになるとは、彼自身
もまだ知らない。",1600,20000,100,10,20,"hlog_101000310_kir_5_01_xxx","hlog_101000311_kir_5_01_xxx",False,"Chara_comment/101000310","Chara_comment/101000311","101000310_cha","101000311_cha","Chara_icon/101000310","Chara_icon/101000311","Chara_icon_loss/101000310","Chara_icon_loss/101000311","2020/12/08 7:00:00",True,"1", -102000410,2,"アスナ","閃光の救世主",5,3,4,0,102000410,202000000,-1,102000410,1620,163300,216,21774,80,8165,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"《SAO》攻略組の中枢を担う
血盟騎士団副団長。彼女が陣頭
指揮をとったことで失われずに
済んだ命は数知れず。",1600,20000,100,10,20,"hlog_102000410_asu_5_01_xxx","hlog_102000411_asu_5_01_xxx",False,"Chara_comment/102000410","Chara_comment/102000411","102000410_cha","102000411_cha","Chara_icon/102000410","Chara_icon/102000411","Chara_icon_loss/102000410","Chara_icon_loss/102000411","2020/12/08 7:00:00",True,"1", -103000340,3,"リーファ","豊潤な湯浴み",5,1,4,2,103000340,201000000,-1,103000340,1890,190510,240,23952,75,7620,240,23950,100900,100,0,107900,0,0,2,0,0,2,0,0,"豊かな大地の恵みを浴びて、身も
心もリフレッシュ。なんだか現実
の身体まで癒される気分?元気を
チャージして明日も頑張ろう!",1600,20000,100,10,20,"hlog_103000340_lea_5_01_xxx","hlog_103000341_lea_5_01_xxx",False,"Chara_comment/103000340","Chara_comment/103000341","103000340_cha","103000341_cha","Chara_icon/103000340","Chara_icon/103000341","Chara_icon_loss/103000340","Chara_icon_loss/103000341","2021/02/02 7:00:00",True,"1", -110000200,10,"アルゴ","秘境の癒湯",5,4,3,2,110000200,203000000,-1,110000200,1620,163750,186,18618,80,8185,280,28300,100900,100,0,107900,0,0,2,0,0,2,0,0,"なんでも知ってる情報屋さんが骨
休めするのは、誰も知らない秘境
の温泉。駆け回って疲れた足を
密かに休めているのかも。",1600,20000,100,10,20,"hlog_110000200_arg_5_01_xxx","hlog_110000201_arg_5_01_xxx",False,"Chara_comment/110000200","Chara_comment/110000201","110000200_cha","110000201_cha","Chara_icon/110000200","Chara_icon/110000201","Chara_icon_loss/110000200","Chara_icon_loss/110000201","2021/02/02 7:00:00",True,"1", -118000120,18,"レイン","湯上り素顔",5,2,1,2,118000120,201000000,201000000,118000120,1380,138800,294,29718,75,7710,200,19960,100900,100,0,107900,0,0,2,0,0,2,0,0,"《SAO》では師匠の下、修行
に励んでいた彼女。兄弟弟子と共
に疲れを癒した天然温泉は、楽し
い思い出の場所だ。",1600,20000,100,10,20,"hlog_118000120_rai_5_01_xxx","hlog_118000121_rai_5_01_xxx",False,"Chara_comment/118000120","Chara_comment/118000121","118000120_cha","118000121_cha","Chara_icon/118000120","Chara_icon/118000121","Chara_icon_loss/118000120","Chara_icon_loss/118000121","2021/02/02 7:00:00",True,"1", -101000320,1,"キリト","世界を包む星空",5,2,1,0,101000320,201000000,201000000,101000320,1530,154220,282,28302,75,7710,200,19960,1,0,0,1,0,0,2,0,0,2,0,0,"痛みと悲しみに溢れた世界を包み
こむ、優しく温かい夜空の煌め
き。人々に希望を与えられるのは
自身も皆に支えられているから。",1600,20000,100,10,20,"hlog_101000320_kir_5_01_xxx","hlog_101000321_kir_5_01_xxx",False,"Chara_comment/101000320","Chara_comment/101000321","101000320_cha","101000321_cha","Chara_icon/101000320","Chara_icon/101000321","Chara_icon_loss/101000320","Chara_icon_loss/101000321","2021/01/26 7:00:00",True,"1", -111000230,11,"アリス","世界を導く光",5,1,3,0,111000230,201000000,-1,111000230,1970,198220,204,20574,100,9910,220,22410,1,0,0,1,0,0,2,0,0,2,0,0,"過酷な運命に翻弄されながら、
前を向けるのは守るべき世界が
あればこそ。二つの世界の結び手
たる彼女は、人々の希望だ。",1600,20000,100,10,20,"hlog_111000230_alc_5_01_xxx","hlog_111000231_alc_5_01_xxx",False,"Chara_comment/111000230","Chara_comment/111000231","111000230_cha","111000231_cha","Chara_icon/111000230","Chara_icon/111000231","Chara_icon_loss/111000230","Chara_icon_loss/111000231","2021/01/26 7:00:00",True,"1", -112000150,12,"ユージオ","世界を見守る剣",5,1,4,0,112000150,201000000,-1,112000150,1890,190510,240,23952,95,9525,200,19960,111500,0,0,1,0,0,2,0,0,2,0,0,"例え形は失われようとも、記憶だ
けは永遠に失われない。この世界
を救う英雄を、人々を、見守り
勇気を与えてくれる。",1600,20000,100,10,20,"hlog_112000150_ego_5_01_xxx","hlog_112000151_ego_5_01_xxx",False,"Chara_comment/112000150","Chara_comment/112000151","112000150_cha","112000151_cha","Chara_icon/112000150","Chara_icon/112000151","Chara_icon_loss/112000150","Chara_icon_loss/112000151","2021/01/26 7:00:00",True,"1", -101000330,1,"キリト","ふたとせ求めて",5,2,4,0,101000330,201000000,201000000,101000330,1890,190510,240,23952,95,9525,200,19960,111500,0,0,1,0,0,2,0,0,2,0,0,"2年が経ち、しかしなお世界は
謎に満ちている。君と共に、この
世界の真実を求めて。これからも
冒険は続いていく。",1600,20000,100,10,20,"hlog_101000330_kir_5_01_xxx","hlog_101000331_kir_5_01_xxx",False,"Chara_comment/101000330","Chara_comment/101000331","101000330_cha","101000331_cha","Chara_icon/101000330","Chara_icon/101000331","Chara_icon_loss/101000330","Chara_icon_loss/101000331","2021/03/16 7:00:00",True,"1", -102000420,2,"アスナ","ふたとせ歩みて",5,3,4,0,102000420,202000000,-1,102000420,1700,171460,240,23952,85,8575,240,23950,111500,0,0,1,0,0,2,0,0,2,0,0,"2年が経ち、歩んできた道のりは
気付けばこんなにも長く、遠く。
君と歩む果てなき道は、今も目の
前に広がっている。",1600,20000,100,10,20,"hlog_102000420_asu_5_01_xxx","hlog_102000421_asu_5_01_xxx",False,"Chara_comment/102000420","Chara_comment/102000421","102000420_cha","102000421_cha","Chara_icon/102000420","Chara_icon/102000421","Chara_icon_loss/102000420","Chara_icon_loss/102000421","2021/03/16 7:00:00",True,"1", -103000350,3,"リーファ","ふたとせ守りて",5,1,4,0,103000350,201000000,-1,103000350,1800,181440,216,21774,70,7260,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"2年が経ち、たくさんの仲間に囲
まれて、新しい友達も出来た。守
るべき人々との絆は、彼女を、そ
して君を強くする。",1600,20000,100,10,20,"hlog_103000350_lea_5_01_xxx","hlog_103000351_lea_5_01_xxx",False,"Chara_comment/103000350","Chara_comment/103000351","103000350_cha","103000351_cha","Chara_icon/103000350","Chara_icon/103000351","Chara_icon_loss/103000350","Chara_icon_loss/103000351","2021/03/23 7:00:00",True,"1", -104000400,4,"シノン","ふたとせ挑みて",5,14,4,0,104000400,215000000,-1,104000400,1440,145150,240,23952,70,7260,200,19960,1,0,0,1,0,0,2,0,0,2,0,0,"2年が経ち、打ち倒してきた敵も
数知れず。しかし挑戦は終わらな
い。まだ見ぬ強敵との邂逅を思い
己を磨き続ける。",1600,20000,100,10,20,"hlog_104000400_sin_5_01_xxx","hlog_104000401_sin_5_01_xxx",False,"Chara_comment/104000400","Chara_comment/104000401","104000400_cha","104000401_cha","Chara_icon/104000400","Chara_icon/104000401","Chara_icon_loss/104000400","Chara_icon_loss/104000401","2021/03/23 7:00:00",True,"1", -109000320,9,"ユウキ","ふたとせ笑いて",5,1,4,0,109000320,201000000,-1,109000320,1800,181440,240,23952,80,8165,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"2年が経ち、皆で笑顔をかわした
楽しい思い出がいっぱいできた。
これからも君と一緒に、皆と一緒
に、笑顔を増やしていこう。",1600,20000,100,10,20,"hlog_109000320_yuu_5_01_xxx","hlog_109000321_yuu_5_01_xxx",False,"Chara_comment/109000320","Chara_comment/109000321","109000320_cha","109000321_cha","Chara_icon/109000320","Chara_icon/109000321","Chara_icon_loss/109000320","Chara_icon_loss/109000321","2021/03/23 7:00:00",True,"1", -111000240,11,"アリス","ふたとせ廻りて",5,1,4,0,111000240,201000000,-1,111000240,2070,208660,228,22860,105,10435,170,17240,1,0,0,1,0,0,2,0,0,2,0,0,"2年が経ち、ふと立ち止まってみ
れば、廻り廻った年月が蘇る。
様々な想いを胸に、これからも
共に時を重ねていくことだろう。",1600,20000,100,10,20,"hlog_111000240_alc_5_01_xxx","hlog_111000241_alc_5_01_xxx",False,"Chara_comment/111000240","Chara_comment/111000241","111000240_cha","111000241_cha","Chara_icon/111000240","Chara_icon/111000241","Chara_icon_loss/111000240","Chara_icon_loss/111000241","2021/03/30 7:00:00",True,"1", -102000430,2,"アスナ","いとしのショコラティエ",5,11,1,0,102000430,211000000,-1,102000430,1300,130640,258,26130,65,6530,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"お料理マスターの彼女にかかれば
チョコレート作りもお手の物。
丹精込めて、ほっぺたが落ちるほ
どの愛をあなたに。",1600,20000,100,10,20,"hlog_102000430_asu_5_01_xxx","hlog_102000431_asu_5_01_xxx",False,"Chara_comment/102000430","Chara_comment/102000431","102000430_cha","102000431_cha","Chara_icon/102000430","Chara_icon/102000431","Chara_icon_loss/102000430","Chara_icon_loss/102000431","2021/02/02 7:00:00",True,"1", -105000260,5,"リズベット","鍛冶師の平穏",5,10,3,0,105000260,209000000,-1,105000260,1620,163300,156,15678,95,9800,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"お店を経営するにはやることが山
積み。そんな忙しい日々でも、た
まに空いた時間が出来ることも。
少しくらい休憩もしなくちゃね。",1600,20000,100,10,20,"hlog_105000260_lis_5_01_xxx","hlog_105000261_lis_5_01_xxx",False,"Chara_comment/105000260","Chara_comment/105000261","105000260_cha","105000261_cha","Chara_icon/105000260","Chara_icon/105000261","Chara_icon_loss/105000260","Chara_icon_loss/105000261","2021/01/26 7:00:00",True,"1", -105000270,5,"リズベット","闊達な誘い",5,10,2,0,105000270,209000000,-1,105000270,1890,190510,156,15678,125,12520,140,14520,1,0,0,1,0,0,2,0,0,2,0,0,"普段は自身の店に籠ることが多い
彼女だったが、戦闘も華麗にこな
す。今日は素材集め、付き合って
よね!",1600,20000,100,10,20,"hlog_105000270_lis_5_01_xxx","hlog_105000271_lis_5_01_xxx",False,"Chara_comment/105000270","Chara_comment/105000271","105000270_cha","105000271_cha","Chara_icon/105000270","Chara_icon/105000271","Chara_icon_loss/105000270","Chara_icon_loss/105000271","2021/01/26 7:00:00",True,"1", -105000280,5,"リズベット","恋するお人形",5,10,3,0,105000280,209000000,-1,105000280,1620,163300,156,15678,95,9800,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"ピンクの髪に大きな瞳。エプロン
ドレスも相まって、その姿は人形
めいた雰囲気を醸し出す。目が
合ったのは、愛しのあの人?",1600,20000,100,10,20,"hlog_105000280_lis_5_01_xxx","hlog_105000281_lis_5_01_xxx",False,"Chara_comment/105000280","Chara_comment/105000281","105000280_cha","105000281_cha","Chara_icon/105000280","Chara_icon/105000281","Chara_icon_loss/105000280","Chara_icon_loss/105000281","2021/04/01 7:00:00",True,"1", -108000170,8,"エギル","勇猛な巨躯",5,9,2,0,108000170,208000000,-1,108000170,2480,250390,192,19158,105,10480,130,13060,1,0,0,1,0,0,2,0,0,2,0,0,"巨大な身体から繰り出される攻撃
は、恐ろしさを感じさせるほどの
大迫力。味方にとっては、頼りに
なることこの上ない。",1600,20000,100,10,20,"hlog_108000170_agi_5_01_xxx","hlog_108000171_agi_5_01_xxx",False,"Chara_comment/108000170","Chara_comment/108000171","108000170_cha","108000171_cha","Chara_icon/108000170","Chara_icon/108000171","Chara_icon_loss/108000170","Chara_icon_loss/108000171","2021/03/01 7:00:00",True,"1", -104000410,4,"シノン","女神の羽休め",5,11,4,0,104000410,211000000,-1,104000410,1440,145150,240,23952,70,7260,200,19960,1,0,0,1,0,0,2,0,0,2,0,0,"《無制限飛行》能力を持つ女神た
る彼女にも、時には休息が必要。
神々しいその御姿は、羽を休めて
いる様すら美しい。",1600,20000,100,10,20,"hlog_104000410_sin_5_01_xxx","hlog_104000411_sin_5_01_xxx",False,"Chara_comment/104000410","Chara_comment/104000411","104000410_cha","104000411_cha","Chara_icon/104000410","Chara_icon/104000411","Chara_icon_loss/104000410","Chara_icon_loss/104000411","2021/03/09 7:00:00",True,"1", -109000330,9,"ユウキ","遠望の彼方",5,1,4,0,109000330,201000000,-1,109000330,1800,181440,240,23952,80,8165,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"遥か遠く、未知なる世界を少女は
見通す。どこまで行けるのかは、
誰にもわからないけれど――一緒
ならどこまでも行ける気がする。",1600,20000,100,10,20,"hlog_109000330_yuu_5_01_xxx","hlog_109000331_yuu_5_01_xxx",False,"Chara_comment/109000330","Chara_comment/109000331","109000330_cha","109000331_cha","Chara_icon/109000330","Chara_icon/109000331","Chara_icon_loss/109000330","Chara_icon_loss/109000331","2021/03/09 7:00:00",True,"1", -110000210,10,"アルゴ","月宙の飛躍",5,8,3,0,110000210,207000000,-1,110000210,1540,155130,162,16548,75,7755,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"彼女の情報収集力は、その身軽さ
の成せるもの。水の上を数歩だけ
走ることすら可能なその敏捷性は
他の追随を許さない。",1600,20000,100,10,20,"hlog_110000210_arg_5_01_xxx","hlog_110000211_arg_5_01_xxx",False,"Chara_comment/110000210","Chara_comment/110000211","110000210_cha","110000211_cha","Chara_icon/110000210","Chara_icon/110000211","Chara_icon_loss/110000210","Chara_icon_loss/110000211","2021/03/09 7:00:00",True,"1", -101000340,1,"キリト","安らぎの我が家",5,2,1,0,101000340,201000000,201000000,101000340,1440,145150,258,26130,70,7260,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"自宅の揺り椅子は彼の特等席。英
雄もここでは穏やかな寝顔を見せ
る。彼がお昼寝に勤しむと周囲の
人にまで睡魔が襲い来るとか…。",1600,20000,100,10,20,"hlog_101000340_kir_5_01_xxx","hlog_101000341_kir_5_01_xxx",False,"Chara_comment/101000340","Chara_comment/101000341","101000340_cha","101000341_cha","Chara_icon/101000340","Chara_icon/101000341","Chara_icon_loss/101000340","Chara_icon_loss/101000341","2021/04/01 7:00:00",True,"1", -118000130,18,"レイン","晴天の君",5,1,1,0,118000130,201000000,-1,118000130,1300,130640,270,27432,70,7260,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"雨上がりの晴天のごとく、明るく
眩しい自然な笑顔に、誰もが元気
をもらえることだろう。本人も気
付かないアイドルの資質だ。",1600,20000,100,10,20,"hlog_118000130_rai_5_01_xxx","hlog_118000131_rai_5_01_xxx",False,"Chara_comment/118000130","Chara_comment/118000131","118000130_cha","118000131_cha","Chara_icon/118000130","Chara_icon/118000131","Chara_icon_loss/118000130","Chara_icon_loss/118000131","2021/03/01 7:00:00",True,"1", -102000440,2,"アスナ","湯の華咲いた",5,3,3,2,102000440,202000000,-1,102000440,1540,155130,192,19596,75,7755,280,28300,100900,100,0,107900,0,0,2,0,0,2,0,0,"湯煙に包まれた温泉に浮かぶは湯
の華、華を添えるは一人の女性。
一緒にお湯につかれば、たちまち
身も心も癒されるだろう。",1600,20000,100,10,20,"hlog_102000440_asu_5_01_xxx","hlog_102000441_asu_5_01_xxx",False,"Chara_comment/102000440","Chara_comment/102000441","102000440_cha","102000441_cha","Chara_icon/102000440","Chara_icon/102000441","Chara_icon_loss/102000440","Chara_icon_loss/102000441","2021/02/02 7:00:00",True,"1", -109000340,9,"ユウキ","温浴の寛ぎ",5,1,1,2,109000340,201000000,-1,109000340,1530,154220,306,31134,70,6940,200,19960,100900,100,0,107900,0,0,2,0,0,2,0,0,"あったかい温泉で安らぐひと時。
いつも元気な彼女も、大きく伸び
をしてのんびり過ごす。皆で入る
温泉は心にもしみわたる。",1600,20000,100,10,20,"hlog_109000340_yuu_5_01_xxx","hlog_109000341_yuu_5_01_xxx",False,"Chara_comment/109000340","Chara_comment/109000341","109000340_cha","109000341_cha","Chara_icon/109000340","Chara_icon/109000341","Chara_icon_loss/109000340","Chara_icon_loss/109000341","2021/02/02 7:00:00",True,"1", -101000350,1,"キリト","笑顔守るために",5,1,1,0,101000350,201000000,-1,101000350,1530,154220,282,28302,75,7710,200,19960,102200,10,0,1,0,0,2,0,0,2,0,0,"《OS》で刃を交えた2人の
ランカー。両者に共通するのは、
愛する者のために戦ったこと。
命を賭して守りたいものがある。",1600,20000,100,10,20,"hlog_101000350_kir_5_01_xxx","hlog_101000351_kir_5_01_xxx",False,"Chara_comment/101000350","Chara_comment/101000351","101000350_cha","101000351_cha","Chara_icon/101000350","Chara_icon/101000351","Chara_icon_loss/101000350","Chara_icon_loss/101000351","2021/03/02 7:00:00",True,"1", -106000300,6,"シリカ","電脳の歌姫",5,4,3,0,106000300,203000000,-1,106000300,1370,137890,174,17634,70,6895,280,28300,102200,10,0,1,0,0,2,0,0,2,0,0,"今日は憧れの歌姫の色を身に纏
い、夢見る少女がオンステージ!
ゲリラライブに敵も味方も思わず
見とれてしまうかも?",1600,20000,100,10,20,"hlog_106000300_sil_5_01_xxx","hlog_106000301_sil_5_01_xxx",False,"Chara_comment/106000300","Chara_comment/106000301","106000300_cha","106000301_cha","Chara_icon/106000300","Chara_icon/106000301","Chara_icon_loss/106000300","Chara_icon_loss/106000301","2021/03/02 7:00:00",True,"1", -118000140,18,"レイン","繋ぐ想い",4,2,4,0,118000140,201000000,201000000,118000140,1440,145150,204,20322,80,8065,160,16130,1,0,0,1,0,0,2,0,0,2,0,0,"チョコレートを渡した際の記録。
皆のアイドルである彼女から、
大切に送られたプレゼント。その
意味を知るのは、まだ先のこと。",800,10000,100,10,20,"hlog_118000140_rai_4_01_xxx","hlog_118000140_rai_4_01_xxx",False,"Chara_comment/118000140","Chara_comment/118000140","118000140_cha","118000140_cha","Chara_icon/118000140","Chara_icon/118000140","Chara_icon_loss/118000140","Chara_icon_loss/118000140","2021/02/02 7:00:00",True,"1", -101000360,1,"キリト","運命の相棒",5,1,1,0,101000360,201000000,-1,101000360,1440,145150,258,26130,70,7260,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"少年の人生を変えたのは、彼との
運命的な出会い。師として友とし
て、いつも傍らで運命を切り開く
彼は眩しく誇らしい。",1600,20000,100,10,20,"hlog_101000360_kir_5_01_xxx","hlog_101000361_kir_5_01_xxx",False,"Chara_comment/101000360","Chara_comment/101000361","101000360_cha","101000361_cha","Chara_icon/101000360","Chara_icon/101000361","Chara_icon_loss/101000360","Chara_icon_loss/101000361","2021/03/23 7:00:00",True,"1", -112000160,12,"ユージオ","無二の相棒",5,1,3,0,112000160,201000000,-1,112000160,1620,163300,174,17418,80,8165,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"英雄と呼ばれた少年の、唯一無二
の親友にして相棒。その剣才と魂
に刻み込まれた思い出は、彼らの
絆をより強固にする。",1600,20000,100,10,20,"hlog_112000160_ego_5_01_xxx","hlog_112000161_ego_5_01_xxx",False,"Chara_comment/112000160","Chara_comment/112000161","112000160_cha","112000161_cha","Chara_icon/112000160","Chara_icon/112000161","Chara_icon_loss/112000160","Chara_icon_loss/112000161","2021/03/23 7:00:00",True,"1", -105000290,5,"リズベット","頼れる盟友",5,10,3,0,105000290,209000000,-1,105000290,1710,172370,174,17634,105,10340,230,23590,102200,10,0,1,0,0,2,0,0,2,0,0,"普段から明るく、周りをよく見て
いるムードメーカーな彼女。現実
世界を舞台としたOSでも、仲間
想いな彼女は頼りになる存在だ。",1600,20000,100,10,20,"hlog_105000290_lis_5_01_xxx","hlog_105000291_lis_5_01_xxx",False,"Chara_comment/105000290","Chara_comment/105000291","105000290_cha","105000291_cha","Chara_icon/105000290","Chara_icon/105000291","Chara_icon_loss/105000290","Chara_icon_loss/105000291","2021/03/23 7:00:00",True,"1", -106000310,6,"シリカ","勇気の跳躍",5,4,1,0,106000310,203000000,-1,106000310,1220,123380,252,25476,60,6170,240,23950,102200,10,0,1,0,0,2,0,0,2,0,0,"今度こそ仲間を守りたい。そんな
決意を胸に秘め、少女はどんな敵
にも立ち向かう。あらん限りの
勇気を、この跳躍に込めて。",1600,20000,100,10,20,"hlog_106000310_sil_5_01_xxx","hlog_106000311_sil_5_01_xxx",False,"Chara_comment/106000310","Chara_comment/106000311","106000310_cha","106000311_cha","Chara_icon/106000310","Chara_icon/106000311","Chara_icon_loss/106000310","Chara_icon_loss/106000311","2021/03/23 7:00:00",True,"1", -102000450,2,"アスナ","うららかジャンプ",5,3,4,5,102000450,202000000,-1,102000450,1700,171460,240,23952,85,8575,240,23950,111500,0,0,1,0,0,2,0,0,2,0,0,"うららかな春の日差しを受けて、
友人たちと手を取り合える日常こ
そ、愛すべきものだ。この平穏が
ずっと続きますように。",1600,20000,100,10,20,"hlog_102000450_asu_5_01_xxx","hlog_102000451_asu_5_01_xxx",False,"Chara_comment/102000450","Chara_comment/102000451","102000450_cha","102000451_cha","Chara_icon/102000450","Chara_icon/102000451","Chara_icon_loss/102000450","Chara_icon_loss/102000451","2021/03/30 7:00:00",True,"1", -109000350,9,"ユウキ","はるかぜジャンプ",5,1,4,5,109000350,201000000,-1,109000350,1890,190510,264,26346,85,8575,200,19960,111500,0,0,1,0,0,2,0,0,2,0,0,"春風に背中を押されて、今こそ
希望を抱いて飛び出す時。ずっと
叶わなかった夢も、大好きな友と
なら、叶えられるかもしれない。",1600,20000,100,10,20,"hlog_109000350_yuu_5_01_xxx","hlog_109000351_yuu_5_01_xxx",False,"Chara_comment/109000350","Chara_comment/109000351","109000350_cha","109000351_cha","Chara_icon/109000350","Chara_icon/109000351","Chara_icon_loss/109000350","Chara_icon_loss/109000351","2021/03/30 7:00:00",True,"1", -111000250,11,"アリス","ゆきどけジャンプ",5,1,4,5,111000250,201000000,-1,111000250,2170,219090,252,25146,110,10955,190,18960,111500,0,0,1,0,0,2,0,0,2,0,0,"初めは騎士の高潔さで人を寄せつ
けなかった彼女も、友の手をとり
やがて柔らかな笑顔を見せる。暖
かな陽に雪が解けてゆくように。",1600,20000,100,10,20,"hlog_111000250_alc_5_01_xxx","hlog_111000251_alc_5_01_xxx",False,"Chara_comment/111000250","Chara_comment/111000251","111000250_cha","111000251_cha","Chara_icon/111000250","Chara_icon/111000251","Chara_icon_loss/111000250","Chara_icon_loss/111000251","2021/03/30 7:00:00",True,"1", -101000370,1,"キリト","黒衣の英雄",5,1,1,0,101000370,201000000,-1,101000370,1530,154220,282,28302,75,7710,200,19960,105600,0,0,1,0,0,2,0,0,2,0,0,"他者を守り、自ら孤独なビーター
としての道を選んだ優しい少年は
やがて世界を救う英雄となった。
翻る黒衣は彼の象徴だ。",1600,20000,100,10,20,"hlog_101000370_kir_5_01_xxx","hlog_101000371_kir_5_01_xxx",False,"Chara_comment/101000370","Chara_comment/101000371","101000370_cha","101000371_cha","Chara_icon/101000370","Chara_icon/101000371","Chara_icon_loss/101000370","Chara_icon_loss/101000371","2021/04/13 7:00:00",True,"1", -105000300,5,"リズベット","英雄の剣匠",5,10,1,0,105000300,209000000,-1,105000300,1530,154220,252,25476,90,9255,200,19960,105600,0,0,1,0,0,2,0,0,2,0,0,"世界を救うに至った二人の英雄の
剣を鍛え、その活躍を支えた彼女
の功績は大きい。彼女無くして、
救済は成しえなかっただろう。",1600,20000,100,10,20,"hlog_105000300_lis_5_01_xxx","hlog_105000301_lis_5_01_xxx",False,"Chara_comment/105000300","Chara_comment/105000301","105000300_cha","105000301_cha","Chara_icon/105000300","Chara_icon/105000301","Chara_icon_loss/105000300","Chara_icon_loss/105000301","2021/04/13 7:00:00",True,"1", -104000420,4,"シノン","私の現実",5,14,4,0,104000420,215000000,-1,104000420,1440,145150,240,23952,70,7260,200,19960,1,0,0,1,0,0,2,0,0,2,0,0,"強さを求め戦場へ飛び込んだ少女
は、やがて「本当の強さ」を知っ
た。仮想と現実の差は今や意味を
成さず、彼女はもう揺るがない。",1600,20000,100,10,20,"hlog_104000420_sin_5_01_xxx","hlog_104000421_sin_5_01_xxx",False,"Chara_comment/104000420","Chara_comment/104000421","104000420_cha","104000421_cha","Chara_icon/104000420","Chara_icon/104000421","Chara_icon_loss/104000420","Chara_icon_loss/104000421","2021/04/27 7:00:00",True,"1", -111000260,11,"アリス","世界を越えて",5,1,1,0,111000260,201000000,-1,111000260,1760,177360,294,29718,90,8870,190,18960,104000,10,0,1,0,0,2,0,0,2,0,0,"現実の身体は無くとも、魂はいつ
だって共にある。世界を越えた絆
を胸に、新たな戦場で彼女は
駆ける。",1600,20000,100,10,20,"hlog_111000260_alc_5_01_xxx","hlog_111000261_alc_5_01_xxx",False,"Chara_comment/111000260","Chara_comment/111000261","111000260_cha","111000261_cha","Chara_icon/111000260","Chara_icon/111000261","Chara_icon_loss/111000260","Chara_icon_loss/111000261","2021/04/27 7:00:00",True,"1", -112000170,12,"ユージオ","新たな挑戦",5,1,1,0,112000170,201000000,-1,112000170,1530,154220,282,28302,75,7710,200,19960,104000,10,0,1,0,0,2,0,0,2,0,0,"新たな戦いの場に身を躍らせて、
少年の挑戦が始まる。どこであろ
うと、その身に刻まれたアイン
クラッド流が助けとなるだろう。",1600,20000,100,10,20,"hlog_112000170_ego_5_01_xxx","hlog_112000171_ego_5_01_xxx",False,"Chara_comment/112000170","Chara_comment/112000171","112000170_cha","112000171_cha","Chara_icon/112000170","Chara_icon/112000171","Chara_icon_loss/112000170","Chara_icon_loss/112000171","2021/04/27 7:00:00",True,"1", -118000150,18,"レイン","共に駆ける今",5,2,3,0,118000150,201000000,201000000,118000150,1540,155130,204,20574,85,8620,230,23590,105100,10,0,1,0,0,2,0,0,2,0,0,"オーグマーを手に入れられず、
寂しい思いをした日もあったけれ
ど。今は皆と共に、眩しい笑顔で
駆け抜けてゆく。",1600,20000,100,10,20,"hlog_118000150_rai_5_01_xxx","hlog_118000151_rai_5_01_xxx",False,"Chara_comment/118000150","Chara_comment/118000151","118000150_cha","118000151_cha","Chara_icon/118000150","Chara_icon/118000151","Chara_icon_loss/118000150","Chara_icon_loss/118000151","2021/04/27 7:00:00",True,"1", -103000360,3,"リーファ","南風に吹かれて",5,1,3,0,103000360,201000000,-1,103000360,1620,163300,174,17418,65,6530,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"穏やかな日差し、暖かな南風。
豊かな自然を感じながら、ただ風
に吹かれて物思いに耽る姿は、
本物の妖精のような可憐さだ。",1600,20000,100,10,20,"hlog_103000360_lea_5_01_xxx","hlog_103000361_lea_5_01_xxx",False,"Chara_comment/103000360","Chara_comment/103000361","103000360_cha","103000361_cha","Chara_icon/103000360","Chara_icon/103000361","Chara_icon_loss/103000360","Chara_icon_loss/103000361","2021/05/01 7:00:00",True,"1", -102000460,2,"アスナ","親友スマイル",5,3,3,0,102000460,202000000,-1,102000460,1460,146970,174,17418,75,7350,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"大切な友人が傍に居れば、きっと
いつだって笑顔で楽しめるはず!
SAOでの過酷な日々を、共に乗
り越えた2人に怖いものはない。",1600,20000,100,10,20,"hlog_102000460_asu_5_01_xxx","hlog_102000461_asu_5_01_xxx",False,"Chara_comment/102000460","Chara_comment/102000461","102000460_cha","102000461_cha","Chara_icon/102000460","Chara_icon/102000461","Chara_icon_loss/102000460","Chara_icon_loss/102000461","2021/05/11 7:00:00",True,"1", -105000310,5,"リズベット","親友ハピネス",5,10,4,0,105000310,209000000,-1,105000310,1800,181440,192,19596,110,10885,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"自分の恋を差し置いても、幸せを
願ってしまう。それくらい大切な
友人の存在は、彼女自身にも幸せ
を運んでくれる。",1600,20000,100,10,20,"hlog_105000310_lis_5_01_xxx","hlog_105000311_lis_5_01_xxx",False,"Chara_comment/105000310","Chara_comment/105000311","105000310_cha","105000311_cha","Chara_icon/105000310","Chara_icon/105000311","Chara_icon_loss/105000310","Chara_icon_loss/105000311","2021/05/11 7:00:00",True,"1", -103000370,3,"リーファ","無防備な女神",5,1,4,0,103000370,201000000,-1,103000370,1800,181440,216,21774,70,7260,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"少し窮屈だった鎧を外し、休息を
とる大地の女神。その豊かな身体
に皆が釘付けになるに違いない。
何も知らぬは本人のみ。",1600,20000,100,10,20,"hlog_103000370_lea_5_01_xxx","hlog_103000371_lea_5_01_xxx",False,"Chara_comment/103000370","Chara_comment/103000371","103000370_cha","103000371_cha","Chara_icon/103000370","Chara_icon/103000371","Chara_icon_loss/103000370","Chara_icon_loss/103000371","2021/05/11 7:00:00",True,"1", -102000470,2,"アスナ","フェアリー・ワルツ",5,3,1,0,102000470,202000000,-1,102000470,1380,138800,282,28302,70,6940,240,23950,104000,10,0,1,0,0,2,0,0,2,0,0,"まるで姉妹のように仲睦まじい
2人の妖精。戯れる姿は、まるで
御伽噺の一幕のよう。彼女たちに
出会えたら幸運に恵まれるかも?",1600,20000,100,10,20,"hlog_102000470_asu_5_01_xxx","hlog_102000471_asu_5_01_xxx",False,"Chara_comment/102000470","Chara_comment/102000471","102000470_cha","102000471_cha","Chara_icon/102000470","Chara_icon/102000471","Chara_icon_loss/102000470","Chara_icon_loss/102000471","2021/05/25 7:00:00",True,"1", -103000380,3,"リーファ","フェアリー・マージ",5,6,1,0,103000380,205000000,-1,103000380,1530,154220,282,28302,60,6170,240,23950,104000,10,0,1,0,0,2,0,0,2,0,0,"麗しい2人の妖精の透明感ある姿
に、誰しもが目を奪われる。こち
らをみとめた途端に見せる、無邪
気な笑顔もまた妖精そのもの。",1600,20000,100,10,20,"hlog_103000380_lea_5_01_xxx","hlog_103000381_lea_5_01_xxx",False,"Chara_comment/103000380","Chara_comment/103000381","103000380_cha","103000381_cha","Chara_icon/103000380","Chara_icon/103000381","Chara_icon_loss/103000380","Chara_icon_loss/103000381","2021/05/25 7:00:00",True,"1", -101000380,1,"キリト","妖精の決闘",5,2,4,0,101000380,201000000,201000000,101000380,1800,181440,216,21774,90,9070,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"愛する少女の行方を求め、ALO
へとログインした英雄。様々な
出会いを経て、彼もまた成長を
続けてゆく。",1600,20000,100,10,20,"hlog_101000380_kir_5_01_xxx","hlog_101000381_kir_5_01_xxx",False,"Chara_comment/101000380","Chara_comment/101000381","101000380_cha","101000381_cha","Chara_icon/101000380","Chara_icon/101000381","Chara_icon_loss/101000380","Chara_icon_loss/101000381","2021/06/01 7:00:00",True,"1", -104000430,4,"シノン","幸せの青い鳥",5,14,4,0,104000430,215000000,-1,104000430,1440,145150,240,23952,70,7260,200,19960,1,0,0,1,0,0,2,0,0,2,0,0,"青い鳥は吉兆の知らせ。そんな鳥
が懐くのだから、彼女の心が清い
証拠であろう。突然の邂逅に、
戸惑いつつも顔を綻ばせる。",1600,20000,100,10,20,"hlog_104000430_sin_5_01_xxx","hlog_104000431_sin_5_01_xxx",False,"Chara_comment/104000430","Chara_comment/104000431","104000430_cha","104000431_cha","Chara_icon/104000430","Chara_icon/104000431","Chara_icon_loss/104000430","Chara_icon_loss/104000431","2021/05/01 7:00:00",True,"1", -110000220,10,"アルゴ","秘密の甘邸",5,4,4,0,110000220,203000000,-1,110000220,1710,172370,204,20682,85,8620,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"夫婦の結婚前に、共にこのログハ
ウスのクエストに挑んだのは良い
思い出。犬は懲り懲りだと零しつ
つ、彼女もどこか楽しそうだ。",1600,20000,100,10,20,"hlog_110000220_arg_5_01_xxx","hlog_110000221_arg_5_01_xxx",False,"Chara_comment/110000220","Chara_comment/110000221","110000220_cha","110000221_cha","Chara_icon/110000220","Chara_icon/110000221","Chara_icon_loss/110000220","Chara_icon_loss/110000221","2021/06/01 7:00:00",True,"1", -110000230,10,"アルゴ","俊鼠の隠密",5,4,3,0,110000230,203000000,-1,110000230,1540,155130,162,16548,75,7755,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"危険な場所へも自ら赴き、新鮮な
情報を仕入れる彼女にとって、隠
密行動は生命線。音もなく戦場を
駆け巡る様は正に《鼠》だ。",1600,20000,100,10,20,"hlog_110000230_arg_5_01_xxx","hlog_110000231_arg_5_01_xxx",False,"Chara_comment/110000230","Chara_comment/110000231","110000230_cha","110000231_cha","Chara_icon/110000230","Chara_icon/110000231","Chara_icon_loss/110000230","Chara_icon_loss/110000231","2021/06/01 7:00:00",True,"1", -110000240,10,"アルゴ","見通す眼差し",5,8,1,0,110000240,207000000,-1,110000240,1370,137890,246,24822,70,6895,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"VR世界を股にかけ、あらゆる情報
を揃える敏腕情報屋。彼女の瞳は
世界の隅々まで見通し、人々を
助けてくれることだろう。",1600,20000,100,10,20,"hlog_110000240_arg_5_01_xxx","hlog_110000241_arg_5_01_xxx",False,"Chara_comment/110000240","Chara_comment/110000241","110000240_cha","110000241_cha","Chara_icon/110000240","Chara_icon/110000241","Chara_icon_loss/110000240","Chara_icon_loss/110000241","2021/06/01 7:00:00",True,"1", -118000160,18,"レイン","にじさす未来",5,2,4,0,118000160,201000000,201000000,118000160,1620,163300,228,22860,90,9070,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"一人で悩まずに、皆と一緒に考え
れば、きっと前へ進めるはず。夢
に邁進する彼女の前には、明るい
未来へ続く虹が架かる。",1600,20000,100,10,20,"hlog_118000160_rai_5_01_xxx","hlog_118000161_rai_5_01_xxx",False,"Chara_comment/118000160","Chara_comment/118000161","118000160_cha","118000161_cha","Chara_icon/118000160","Chara_icon/118000161","Chara_icon_loss/118000160","Chara_icon_loss/118000161","2021/06/08 7:00:00",True,"1", -104000440,4,"シノン","麗凛な獣猫",5,8,1,4,104000440,207000000,-1,104000440,1220,123380,306,31134,60,6170,220,21950,104000,10,0,1,0,0,2,0,0,2,0,0,"ランダムにアバターが猫換装?!
孤高な戦士を思わせるクールな彼
女は、猫化しても動じない…と、
思いきや、意外と照れてる様子?",1600,20000,100,10,20,"hlog_104000440_sin_5_01_xxx","hlog_104000441_sin_5_01_xxx",False,"Chara_comment/104000440","Chara_comment/104000441","104000440_cha","104000441_cha","Chara_icon/104000440","Chara_icon/104000441","Chara_icon_loss/104000440","Chara_icon_loss/104000441","2021/06/08 7:00:00",True,"1", -105000320,5,"リズベット","毛繕い桃猫",5,8,3,4,105000320,207000000,-1,105000320,1710,172370,174,17634,105,10340,230,23590,105100,10,0,1,0,0,2,0,0,2,0,0,"ランダムにアバターが猫換装?!
でも、これはこれでお客さんが喜
ぶかも…?入念な毛繕いで、身だ
しなみはバッチリ。",1600,20000,100,10,20,"hlog_105000320_lis_5_01_xxx","hlog_105000321_lis_5_01_xxx",False,"Chara_comment/105000320","Chara_comment/105000321","105000320_cha","105000321_cha","Chara_icon/105000320","Chara_icon/105000321","Chara_icon_loss/105000320","Chara_icon_loss/105000321","2021/06/08 7:00:00",True,"1", -106000320,6,"シリカ","まったり愛猫",5,8,3,4,106000320,207000000,-1,106000320,1370,137890,174,17634,70,6895,280,28300,105100,10,0,1,0,0,2,0,0,2,0,0,"ランダムにアバターが猫換装?!
今日はのんびり気儘な猫ちゃん
モード。こうしてると心地いいか
ら、あなたも一緒にどうですか?",1600,20000,100,10,20,"hlog_106000320_sil_5_01_xxx","hlog_106000321_sil_5_01_xxx",False,"Chara_comment/106000320","Chara_comment/106000321","106000320_cha","106000321_cha","Chara_icon/106000320","Chara_icon/106000321","Chara_icon_loss/106000320","Chara_icon_loss/106000321","2021/06/08 7:00:00",True,"1", -108000180,8,"エギル","隆隆狩猫",5,8,1,4,108000180,207000000,-1,108000180,1840,185070,306,31134,85,8480,180,17960,104000,10,0,1,0,0,2,0,0,2,0,0,"ランダムにアバターが猫換装?!
猫の狩猟本能も呼び覚まされたの
か、いつもより更に野生的に?!
大型肉食獣さながらの迫力だ。",1600,20000,100,10,20,"hlog_108000180_agi_5_01_xxx","hlog_108000181_agi_5_01_xxx",False,"Chara_comment/108000180","Chara_comment/108000181","108000180_cha","108000181_cha","Chara_icon/108000180","Chara_icon/108000181","Chara_icon_loss/108000180","Chara_icon_loss/108000181","2021/06/08 7:00:00",True,"1", -109000360,9,"ユウキ","とびつき活猫",5,8,4,4,109000360,207000000,-1,109000360,1890,190510,264,26346,85,8575,200,19960,102200,10,0,1,0,0,2,0,0,2,0,0,"ランダムにアバターが猫換装?!
猫化の影響か、野生の本能を刺激
されいつも以上に元気いっぱい!
獲物に飛びついちゃえ!",1600,20000,100,10,20,"hlog_109000360_yuu_5_01_xxx","hlog_109000361_yuu_5_01_xxx",False,"Chara_comment/109000360","Chara_comment/109000361","109000360_cha","109000361_cha","Chara_icon/109000360","Chara_icon/109000361","Chara_icon_loss/109000360","Chara_icon_loss/109000361","2021/06/08 7:00:00",True,"1", -109000370,9,"ユウキ","おすわりリトル",5,9,1,0,109000370,208000000,-1,109000370,1440,145150,288,28740,65,6530,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"幼さの残る少女は、悪戯っぽく
笑いかける。年相応の愛らしい姿
と裏腹に、ふとした瞬間に見せる
大人びた笑みが印象的。",1600,20000,100,10,20,"hlog_109000370_yuu_5_01_xxx","hlog_109000371_yuu_5_01_xxx",False,"Chara_comment/109000370","Chara_comment/109000371","109000370_cha","109000371_cha","Chara_icon/109000370","Chara_icon/109000371","Chara_icon_loss/109000370","Chara_icon_loss/109000371","2021/07/01 7:00:00",True,"1", -101000390,1,"キリト","飛翔の新天地",5,6,1,0,101000390,205000000,-1,101000390,1530,154220,282,28302,75,7710,200,19960,105600,0,0,1,0,0,2,0,0,2,0,0,"妖精の国、その遥か上空に浮かぶ
新たな冒険の地。妖精たちは胸を
躍らせて、新天地へと羽ばたいて
ゆく。",1600,20000,100,10,20,"hlog_101000390_kir_5_01_xxx","hlog_101000391_kir_5_01_xxx",False,"Chara_comment/101000390","Chara_comment/101000391","101000390_cha","101000391_cha","Chara_icon/101000390","Chara_icon/101000391","Chara_icon_loss/101000390","Chara_icon_loss/101000391","2022/01/01 7:00:00",True,"1", -103000390,3,"リーファ","風舞う妖精",5,6,1,0,103000390,205000000,-1,103000390,1530,154220,282,28302,60,6170,240,23950,105600,0,0,1,0,0,2,0,0,2,0,0,"新たな門出を祝うように、妖精た
ちの世界に一陣の風が吹く。誰よ
りも空を愛した風妖精は、仲間と
共に楽し気に舞い踊る。",1600,20000,100,10,20,"hlog_103000390_lea_5_01_xxx","hlog_103000391_lea_5_01_xxx",False,"Chara_comment/103000390","Chara_comment/103000391","103000390_cha","103000391_cha","Chara_icon/103000390","Chara_icon/103000391","Chara_icon_loss/103000390","Chara_icon_loss/103000391","2022/01/01 7:00:00",True,"1", -109000380,9,"ユウキ","十字の加護",5,3,1,0,109000380,202000000,-1,109000380,1440,145150,288,28740,65,6530,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"苛烈な11連撃に秘められた想い
は、大切な家族や仲間との絆。
皆の愛が詰まったこの技なら、
大事な人を守ってくれるはず。
",1600,20000,100,10,20,"hlog_109000380_yuu_5_01_xxx","hlog_109000381_yuu_5_01_xxx",False,"Chara_comment/109000380","Chara_comment/109000381","109000380_cha","109000381_cha","Chara_icon/109000380","Chara_icon/109000381","Chara_icon_loss/109000380","Chara_icon_loss/109000381","2021/07/13 7:00:00",True,"1", -111000270,11,"アリス","世界を愛した少女",5,1,1,0,111000270,201000000,-1,111000270,1660,166920,270,27432,85,8345,170,17240,1,0,0,1,0,0,2,0,0,2,0,0,"整合騎士は、自身が整合騎士とな
る前の記憶を保持していない。そ
れでも、胸が疼くほどの懐かしさ
を感じるのはどうしてだろう。",1600,20000,100,10,20,"hlog_111000270_alc_5_01_xxx","hlog_111000271_alc_5_01_xxx",False,"Chara_comment/111000270","Chara_comment/111000271","111000270_cha","111000271_cha","Chara_icon/111000270","Chara_icon/111000271","Chara_icon_loss/111000270","Chara_icon_loss/111000271","2021/07/13 7:00:00",True,"1", -112000180,12,"ユージオ","寄り添う心",5,1,1,0,112000180,201000000,-1,112000180,1440,145150,258,26130,70,7260,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"囚われた大切な人を救うため。
彼の望みはただそれだけ。だから
こそ、今はそっと寄り添う。いつ
か共に笑い合える日を願って。",1600,20000,100,10,20,"hlog_112000180_ego_5_01_xxx","hlog_112000181_ego_5_01_xxx",False,"Chara_comment/112000180","Chara_comment/112000181","112000180_cha","112000181_cha","Chara_icon/112000180","Chara_icon/112000181","Chara_icon_loss/112000180","Chara_icon_loss/112000181","2021/07/13 7:00:00",True,"1", -102000480,2,"アスナ","犀薫る浜辺",5,1,1,1,102000480,201000000,-1,102000480,1380,138800,282,28302,70,6940,240,23950,104000,10,0,1,0,0,2,0,0,2,0,0,"涼やかな青が浜辺に映える。眩し
いばかりの白い肌に、煌めく金木
犀が甘い香りを運んでくるよう。
華やかな姿に誰もが釘付けだ。",1600,20000,100,10,20,"hlog_102000480_asu_5_01_xxx","hlog_102000481_asu_5_01_xxx",False,"Chara_comment/102000480","Chara_comment/102000481","102000480_cha","102000481_cha","Chara_icon/102000480","Chara_icon/102000481","Chara_icon_loss/102000480","Chara_icon_loss/102000481","2021/08/10 7:00:00",True,"1", -111000280,11,"アリス","波打ち際の彩",5,3,1,1,111000280,202000000,-1,111000280,1760,177360,294,29718,90,8870,190,18960,104000,10,0,1,0,0,2,0,0,2,0,0,"波間で無邪気に足を遊ばせる様は
普段の真面目な騎士然とした彼女
には意外なほどに純粋無垢だ。白
い素肌に赤のラインが眩しい。",1600,20000,100,10,20,"hlog_111000280_alc_5_01_xxx","hlog_111000281_alc_5_01_xxx",False,"Chara_comment/111000280","Chara_comment/111000281","111000280_cha","111000281_cha","Chara_icon/111000280","Chara_icon/111000281","Chara_icon_loss/111000280","Chara_icon_loss/111000281","2021/08/10 7:00:00",True,"1", -118000170,18,"レイン","胸躍る一歩",5,3,1,0,118000170,202000000,-1,118000170,1300,130640,270,27432,70,7260,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"未来は分からないことばかりだけ
れど、今の彼女には沢山の仲間が
ついている。胸を張って歩む一歩
は、あなたと一緒に。",1600,20000,100,10,20,"hlog_118000170_rai_5_01_xxx","hlog_118000171_rai_5_01_xxx",False,"Chara_comment/118000170","Chara_comment/118000171","118000170_cha","118000171_cha","Chara_icon/118000170","Chara_icon/118000171","Chara_icon_loss/118000170","Chara_icon_loss/118000171","2021/08/01 7:00:00",True,"1", -106000330,6,"シリカ","一輪の幼花",5,3,3,0,106000330,202000000,-1,106000330,1300,130640,156,15678,65,6530,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"花々に囲まれて、微笑む彼女自身
も、愛らしい花のよう。隣に座っ
て眺める花畑は、いつにも増して
色づいて見えるだろう。",1600,20000,100,10,20,"hlog_106000330_sil_5_01_xxx","hlog_106000331_sil_5_01_xxx",False,"Chara_comment/106000330","Chara_comment/106000331","106000330_cha","106000331_cha","Chara_icon/106000330","Chara_icon/106000331","Chara_icon_loss/106000330","Chara_icon_loss/106000331","2021/08/01 7:00:00",True,"1", -103000400,3,"リーファ","微睡の砂浜",5,1,1,1,103000400,201000000,-1,103000400,1530,154220,282,28302,60,6170,240,23950,104000,10,0,1,0,0,2,0,0,2,0,0,"ビーチでまどろむ姿からは普段の
活発さは伺えず、いたいけな少女
の幸せそうな寝顔が日差しよりも
眩しく映えている。",1600,20000,100,10,20,"hlog_103000400_lea_5_01_xxx","hlog_103000401_lea_5_01_xxx",False,"Chara_comment/103000400","Chara_comment/103000401","103000400_cha","103000401_cha","Chara_icon/103000400","Chara_icon/103000401","Chara_icon_loss/103000400","Chara_icon_loss/103000401","2021/07/27 7:00:00",True,"1", -105000330,5,"リズベット","海辺の休息",5,10,3,1,105000330,209000000,-1,105000330,1710,172370,174,17634,105,10340,230,23590,105100,10,0,1,0,0,2,0,0,2,0,0,"煌めくビーチに大切な仲間たち、
そして、何よりお気に入りの水着
が彼女の笑顔を輝かせる。今年も
楽しい夏になりそうな予感。",1600,20000,100,10,20,"hlog_105000330_lis_5_01_xxx","hlog_105000331_lis_5_01_xxx",False,"Chara_comment/105000330","Chara_comment/105000331","105000330_cha","105000331_cha","Chara_icon/105000330","Chara_icon/105000331","Chara_icon_loss/105000330","Chara_icon_loss/105000331","2021/07/27 7:00:00",True,"1", -109000390,9,"ユウキ","夏の陽の夢",5,1,1,1,109000390,201000000,-1,109000390,1530,154220,306,31134,70,6940,200,19960,104000,10,0,1,0,0,2,0,0,2,0,0,"日差しに負けないほど眩い笑顔が
あどけなく、可愛らしい。
VR世界で叶えた夢は、データ以上
の感動を彼女にもたらした。",1600,20000,100,10,20,"hlog_109000390_yuu_5_01_xxx","hlog_109000391_yuu_5_01_xxx",False,"Chara_comment/109000390","Chara_comment/109000391","109000390_cha","109000391_cha","Chara_icon/109000390","Chara_icon/109000391","Chara_icon_loss/109000390","Chara_icon_loss/109000391","2021/07/27 7:00:00",True,"1", -110000250,10,"アルゴ","波際の司令塔",5,4,3,1,110000250,203000000,-1,110000250,1620,163750,186,18618,80,8185,280,28300,105100,10,0,1,0,0,2,0,0,2,0,0,"ビーチバレーの必勝法なら彼女に
お任せあれ!相手選手の好きな
食べ物から弱点まで、応じた報酬
を払えば優勝だって夢じゃない。",1600,20000,100,10,20,"hlog_110000250_arg_5_01_xxx","hlog_110000251_arg_5_01_xxx",False,"Chara_comment/110000250","Chara_comment/110000251","110000250_cha","110000251_cha","Chara_icon/110000250","Chara_icon/110000251","Chara_icon_loss/110000250","Chara_icon_loss/110000251","2021/07/27 7:00:00",True,"1", -118000180,18,"レイン","心躍る岸辺",5,2,1,1,118000180,201000000,201000000,118000180,1380,138800,294,29718,75,7710,200,19960,104000,10,0,1,0,0,2,0,0,2,0,0,"すらりと伸びる白い肌に太陽の光
がよく映える。いつもは内気な彼
女の心の内を開くキーアイテムは
水着なのかも。",1600,20000,100,10,20,"hlog_118000180_rai_5_01_xxx","hlog_118000181_rai_5_01_xxx",False,"Chara_comment/118000180","Chara_comment/118000181","118000180_cha","118000181_cha","Chara_icon/118000180","Chara_icon/118000181","Chara_icon_loss/118000180","Chara_icon_loss/118000181","2021/07/27 7:00:00",True,"1", -104000450,4,"シノン","砂辺の観戦者",5,14,1,1,104000450,215000000,-1,104000450,1150,116120,288,28740,60,5805,200,19960,1,0,0,1,0,0,2,0,0,2,0,0,"ビーチの戦いは勝っても負けても
恨みっこなしの真剣勝負。
参戦するも良し、観戦するも良し
あなたはどっちを選ぶ?",1600,20000,100,10,20,"hlog_104000450_sin_5_01_xxx","hlog_104000451_sin_5_01_xxx",False,"Chara_comment/104000450","Chara_comment/104000451","104000450_cha","104000451_cha","Chara_icon/104000450","Chara_icon/104000451","Chara_icon_loss/104000450","Chara_icon_loss/104000451","2021/07/27 7:00:00",True,"1", -106000340,6,"シリカ","大海の中の絆",5,4,2,0,106000340,203000000,-1,106000340,1580,159670,156,15678,80,7985,170,17420,1,0,0,1,0,0,2,0,0,2,0,0,"大好きな友達と遊ぶ夏は何度でも
楽しいもの。
どんな些細な思い出も、彼女には
すべて大切な出来事。",1600,20000,100,10,20,"hlog_106000340_sil_5_01_xxx","hlog_106000341_sil_5_01_xxx",False,"Chara_comment/106000340","Chara_comment/106000341","106000340_cha","106000341_cha","Chara_icon/106000340","Chara_icon/106000341","Chara_icon_loss/106000340","Chara_icon_loss/106000341","2021/07/27 7:00:00",True,"1", -104000460,4,"シノン","騎士の心得",5,1,1,0,104000460,201000000,-1,104000460,1220,123380,306,31134,60,6170,220,21950,104000,10,0,1,0,0,2,0,0,2,0,0,"騎士のように凛とした佇まいから
彼女の芯の強さが見て取れる。
誰であれ守りたい人のために力を
振るうのは勇気のいる選択だ。",1600,20000,100,10,20,"hlog_104000460_sin_5_01_xxx","hlog_104000461_sin_5_01_xxx",False,"Chara_comment/104000460","Chara_comment/104000461","104000460_cha","104000461_cha","Chara_icon/104000460","Chara_icon/104000461","Chara_icon_loss/104000460","Chara_icon_loss/104000461","2021/09/07 7:00:00",True,"1", -111000290,11,"アリス","射撃手の心得",5,14,1,0,111000290,215000000,-1,111000290,1760,177360,294,29718,90,8870,190,18960,104000,10,0,1,0,0,2,0,0,2,0,0,"今は堅い騎士服を脱いで、身軽な
ガンナーに転身。普段と違う装備
に落ち着かない様子だが、引き金
を引く指に迷いはない。",1600,20000,100,10,20,"hlog_111000290_alc_5_01_xxx","hlog_111000291_alc_5_01_xxx",False,"Chara_comment/111000290","Chara_comment/111000291","111000290_cha","111000291_cha","Chara_icon/111000290","Chara_icon/111000291","Chara_icon_loss/111000290","Chara_icon_loss/111000291","2021/09/07 7:00:00",True,"1", -103000410,3,"リーファ","休息の女神",5,1,3,0,103000410,201000000,-1,103000410,1620,163300,174,17418,65,6530,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"地母神テラリアのリーファの姿。
リラックスした雰囲気をまとった
彼女は、まるで西洋絵画に描かれ
た女神のよう。",1600,20000,100,10,20,"hlog_103000410_lea_5_01_xxx","hlog_103000411_lea_5_01_xxx",False,"Chara_comment/103000410","Chara_comment/103000411","103000410_cha","103000411_cha","Chara_icon/103000410","Chara_icon/103000411","Chara_icon_loss/103000410","Chara_icon_loss/103000411","2021/07/27 7:00:00",True,"1", -102000490,2,"アスナ","憩いの女神",5,3,4,0,102000490,202000000,-1,102000490,1620,163300,216,21774,80,8165,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"創世神ステイシアのアスナの姿。
女神の神々しい衣装にはアンバラ
ンスなあどけない表情が、彼女の
人間味を思わせる。",1600,20000,100,10,20,"hlog_102000490_asu_5_01_xxx","hlog_102000491_asu_5_01_xxx",False,"Chara_comment/102000490","Chara_comment/102000491","102000490_cha","102000491_cha","Chara_icon/102000490","Chara_icon/102000491","Chara_icon_loss/102000490","Chara_icon_loss/102000491","2021/09/21 7:00:00",True,"1", -106000350,6,"シリカ","仔猫の交情",5,4,3,0,106000350,203000000,-1,106000350,1300,130640,156,15678,65,6530,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"動物と仲良くなりたかったら、
まずは歩み寄る姿勢から。
だからといって、むやみに構って
嫌われないよう注意すること。",1600,20000,100,10,20,"hlog_106000350_sil_5_01_xxx","hlog_106000351_sil_5_01_xxx",False,"Chara_comment/106000350","Chara_comment/106000351","106000350_cha","106000351_cha","Chara_icon/106000350","Chara_icon/106000351","Chara_icon_loss/106000350","Chara_icon_loss/106000351","2021/09/21 7:00:00",True,"1", -118000190,18,"レイン","開襟への架け橋",5,2,1,0,118000190,201000000,201000000,118000190,1300,130640,270,27432,70,7260,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"本当の自分を隠したままでは人と
繋がるのは難しい。だからといっ
て全てをさらけ出すのも難しいもの。けれどその日は必ず訪れる。",1600,20000,100,10,20,"hlog_118000190_rai_5_01_xxx","hlog_118000191_rai_5_01_xxx",False,"Chara_comment/118000190","Chara_comment/118000191","118000190_cha","118000191_cha","Chara_icon/118000190","Chara_icon/118000191","Chara_icon_loss/118000190","Chara_icon_loss/118000191","2021/09/21 7:00:00",True,"1", -102000500,2,"アスナ","水妖精の導き",5,10,1,0,102000500,209000000,-1,102000500,1300,130640,258,26130,65,6530,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"空飛ぶ感覚はALOならでは。
もしあなたが妖精になったとき、
手こずったって大丈夫。
彼女が手ほどきをしてくれる。",1600,20000,100,10,20,"hlog_102000500_asu_5_01_xxx","hlog_102000501_asu_5_01_xxx",False,"Chara_comment/102000500","Chara_comment/102000501","102000500_cha","102000501_cha","Chara_icon/102000500","Chara_icon/102000501","Chara_icon_loss/102000500","Chara_icon_loss/102000501","2021/09/01 7:00:00",True,"1", -104000470,4,"シノン","同朋の待望",5,4,1,0,104000470,203000000,-1,104000470,1150,116120,288,28740,60,5805,200,19960,1,0,0,1,0,0,2,0,0,2,0,0,"誰かを待つようになって初めて
見えてくるものがある。
一人じゃないことがどれほどの
心強さか彼女はもう知っている。",1600,20000,100,10,20,"hlog_104000470_sin_5_01_xxx","hlog_104000471_sin_5_01_xxx",False,"Chara_comment/104000470","Chara_comment/104000471","104000470_cha","104000471_cha","Chara_icon/104000470","Chara_icon/104000471","Chara_icon_loss/104000470","Chara_icon_loss/104000471","2021/10/01 7:00:00",True,"1", -103000420,3,"リーファ","寛ぎの我が家",5,3,3,0,103000420,202000000,-1,103000420,1620,163300,174,17418,65,6530,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"《ALO》のアインクラッドにある
ログハウスは、彼女にとっても
大切な場所。これからも仲間と
共に思い出を積み重ねていく。",1600,20000,100,10,20,"hlog_103000420_lea_5_01_xxx","hlog_103000421_lea_5_01_xxx",False,"Chara_comment/103000420","Chara_comment/103000421","103000420_cha","103000421_cha","Chara_icon/103000420","Chara_icon/103000421","Chara_icon_loss/103000420","Chara_icon_loss/103000421","2021/10/01 7:00:00",True,"1", -102000510,2,"アスナ","真夏の眩耀・彩",5,3,4,1,102000510,202000000,-1,102000510,1700,171460,240,23952,85,8575,240,23950,1,0,0,1,0,0,2,0,0,2,0,0,"海辺を歩く彼女の姿は、誰が見て
も眩く輝いて見えるだろう。彼女
の瞳が何を捉えているのか、それ
を知る術はどこにも無い。",1600,20000,100,10,20,"hlog_102000170_asu_5_01_rap","hlog_102000171_asu_5_01_rap",False,"Chara_comment/102000170","Chara_comment/102000171","102000170_cha","102000171_cha","Chara_icon/102000170","Chara_icon/102000171","Chara_icon_loss/102000170","Chara_icon_loss/102000171","2021/09/28 7:00:00",False,"1", -102000520,2,"アスナ","潮風の悪戯・彩",5,3,3,1,102000520,202000000,-1,102000520,1540,155130,192,19596,75,7755,280,28300,1,0,0,1,0,0,2,0,0,2,0,0,"磯の香りに誘われて、波打ち際へ
と歩いてみれば、悪戯な潮風が頬
を撫でる。穏やかで美しい、大切
な海の思い出。",1600,20000,100,10,20,"hlog_102000310_asu_5_01_xxx","hlog_102000311_asu_5_01_xxx",False,"Chara_comment/102000310","Chara_comment/102000311","102000310_cha","102000311_cha","Chara_icon/102000310","Chara_icon/102000311","Chara_icon_loss/102000310","Chara_icon_loss/102000311","2021/09/28 7:00:00",False,"1", -103000430,3,"リーファ","魅惑の夏日・彩",5,1,4,1,103000430,201000000,-1,103000430,1890,190510,240,23952,75,7620,240,23950,1,0,0,1,0,0,2,0,0,2,0,0,"身体能力に長けた彼女だが、唯一
水泳だけは苦手意識が強い。抜か
りない準備運動の折、ふと目が
合ったのは偶然か必然か。",1600,20000,100,10,20,"hlog_103000150_lea_5_01_oha","hlog_103000151_lea_5_01_oha",False,"Chara_comment/103000150","Chara_comment/103000151","103000150_cha","103000151_cha","Chara_icon/103000150","Chara_icon/103000151","Chara_icon_loss/103000150","Chara_icon_loss/103000151","2021/09/28 7:00:00",False,"1", -103000440,3,"リーファ","彩!アタックガール",5,6,4,1,103000440,205000000,-1,103000440,1890,190510,240,23952,75,7620,240,23950,1,0,0,1,0,0,2,0,0,2,0,0,"Unknownで行われることになった
ビーチバレー大会。競技は違えど
体育会系の血が騒ぐ!!この夏、
誰よりも高く、少女は跳ぶ!",1600,20000,100,10,20,"hlog_103000270_lea_5_01_xxx","hlog_103000271_lea_5_01_xxx",False,"Chara_comment/103000270","Chara_comment/103000271","103000270_cha","103000271_cha","Chara_icon/103000270","Chara_icon/103000271","Chara_icon_loss/103000270","Chara_icon_loss/103000271","2021/09/28 7:00:00",False,"1", -104000480,4,"シノン","砂浜の女神・彩",5,14,4,1,104000480,215000000,-1,104000480,1510,152410,264,26346,75,7620,220,21950,1,0,0,1,0,0,2,0,0,2,0,0,"通り名さながらにビーチの視線を
集める彼女。目立つことには慣れ
ていない様子だが、彼女なりに海
を楽しんでいるようだ。",1600,20000,100,10,20,"hlog_104000160_sin_5_01_rif","hlog_104000161_sin_5_01_rif",False,"Chara_comment/104000160","Chara_comment/104000161","104000160_cha","104000161_cha","Chara_icon/104000160","Chara_icon/104000161","Chara_icon_loss/104000160","Chara_icon_loss/104000161","2021/09/28 7:00:00",False,"1", -105000340,5,"リズベット","目映い太陽・彩",5,10,4,1,105000340,209000000,-1,105000340,1890,190510,216,21558,115,11430,200,19960,1,0,0,1,0,0,2,0,0,2,0,0,"日差しを受けて煌めく、弾けるよ
うなとびきりの笑顔。真夏が良く
似合う太陽のような彼女となら、
最高の思い出が作れるはず。",1600,20000,100,10,20,"hlog_105000230_lis_5_01_xxx","hlog_105000231_lis_5_01_xxx",False,"Chara_comment/105000230","Chara_comment/105000231","105000230_cha","105000231_cha","Chara_icon/105000230","Chara_icon/105000231","Chara_icon_loss/105000230","Chara_icon_loss/105000231","2021/09/28 7:00:00",False,"1", -106000360,6,"シリカ","白砂の少女・彩",5,4,4,1,106000360,203000000,-1,106000360,1510,152410,216,21558,75,7620,240,23950,1,0,0,1,0,0,2,0,0,2,0,0,"ちょこんと座り、少女はこちらを
見上げる。その姿は、日差しに輝
く白い砂浜よりも眩しく愛らしく
見る者を魅了することだろう。",1600,20000,100,10,20,"hlog_106000250_sil_5_01_xxx","hlog_106000251_sil_5_01_xxx",False,"Chara_comment/106000250","Chara_comment/106000251","106000250_cha","106000251_cha","Chara_icon/106000250","Chara_icon/106000251","Chara_icon_loss/106000250","Chara_icon_loss/106000251","2021/09/28 7:00:00",False,"1", -111000300,11,"アリス","透き通る青・彩",5,1,4,1,111000300,201000000,-1,111000300,2170,219090,252,25146,110,10955,190,18960,1,0,0,1,0,0,2,0,0,2,0,0,"普段は鎧に隠された透き通るよう
な肌に、金糸のような髪が風にな
びく。鮮やかな青い水着が良く似
合う、海に相応しい出で立ちだ。",1600,20000,100,10,20,"hlog_111000200_alc_5_01_xxx","hlog_111000201_alc_5_01_xxx",False,"Chara_comment/111000200","Chara_comment/111000201","111000200_cha","111000201_cha","Chara_icon/111000200","Chara_icon/111000201","Chara_icon_loss/111000200","Chara_icon_loss/111000201","2021/09/28 7:00:00",False,"1", -102000530,2,"アスナ","夢の逸楽",5,3,1,0,102000530,202000000,-1,102000530,810,81650,432,43548,40,4080,250,25040,1,0,0,1,0,0,2,0,0,2,0,0,"一晩限りのカジノへようこそ!
記憶のカジノより立派ではないけ
れど、おもてなしの心は負けてい
ない。今日だけの夢をあなたに。",1600,20000,100,10,20,"hlog_102000530_asu_5_01_xxx","hlog_102000531_asu_5_01_xxx",False,"Chara_comment/102000530","Chara_comment/102000531","102000530_cha","102000531_cha","Chara_icon/102000530","Chara_icon/102000531","Chara_icon_loss/102000530","Chara_icon_loss/102000531","2021/09/28 7:00:00",True,"1", -103000450,3,"リーファ","賽の悦楽",5,1,1,0,103000450,201000000,-1,103000450,990,99790,444,44634,40,3990,260,26130,104000,10,0,1,0,0,2,0,0,2,0,0,"一晩限りのカジノへようこそ!
サイコロのように、気付けば
いつの間にか彼女の手の中で
転がされていた?!",1600,20000,100,10,20,"hlog_103000450_lea_5_01_xxx","hlog_103000451_lea_5_01_xxx",False,"Chara_comment/103000450","Chara_comment/103000451","103000450_cha","103000451_cha","Chara_icon/103000450","Chara_icon/103000451","Chara_icon_loss/103000450","Chara_icon_loss/103000451","2021/09/28 7:00:00",True,"1", -104000490,4,"シノン","廻る享楽",5,14,1,0,104000490,215000000,-1,104000490,790,79830,486,49098,40,3990,240,23950,104000,10,0,1,0,0,2,0,0,2,0,0,"一晩限りのカジノへようこそ!
ルーレットが止まっても、彼女の
一挙手一投足に目が離せない。
このドキドキは負けたせい?",1600,20000,100,10,20,"hlog_104000490_sin_5_01_xxx","hlog_104000491_sin_5_01_xxx",False,"Chara_comment/104000490","Chara_comment/104000491","104000490_cha","104000491_cha","Chara_icon/104000490","Chara_icon/104000491","Chara_icon_loss/104000490","Chara_icon_loss/104000491","2021/09/28 7:00:00",True,"1", -106000370,6,"シリカ","二色の歓楽",5,4,1,0,106000370,203000000,-1,106000370,790,79830,396,40170,40,3990,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"一晩限りのカジノへようこそ!
赤か黒か、どちらか悩んでいると
彼女の耳がぴこっと動く。その耳
が示した方にチップを賭けた。",1600,20000,100,10,20,"hlog_106000370_sil_5_01_xxx","hlog_106000371_sil_5_01_xxx",False,"Chara_comment/106000370","Chara_comment/106000371","106000370_cha","106000371_cha","Chara_icon/106000370","Chara_icon/106000371","Chara_icon_loss/106000370","Chara_icon_loss/106000371","2021/09/28 7:00:00",True,"1", -111000310,11,"アリス","兎の務め",5,1,1,0,111000310,201000000,-1,111000310,1140,114760,462,46866,55,5740,210,20680,104000,10,0,1,0,0,2,0,0,2,0,0,"一晩限りのカジノへようこそ!
おもてなしを受けるだけは性に合
わなかったよう。着慣れない制服
に、慌ててヒールをひっかけた。",1600,20000,100,10,20,"hlog_111000310_alc_5_01_xxx","hlog_111000311_alc_5_01_xxx",False,"Chara_comment/111000310","Chara_comment/111000311","111000310_cha","111000311_cha","Chara_icon/111000310","Chara_icon/111000311","Chara_icon_loss/111000310","Chara_icon_loss/111000311","2021/09/28 7:00:00",True,"1", -105000350,5,"リズベット","ぬくもりの黒制服",5,2,3,0,105000350,201000000,201000000,105000350,1710,172370,174,17634,105,10340,230,23590,105100,10,0,1,0,0,2,0,0,2,0,0,"彼女にとって黒は、健気で切ない
春のように暖かな色。思い出は自
分だけのものだから、風の音に耳
を澄ませた。",1600,20000,100,10,20,"hlog_105000350_lis_5_01_xxx","hlog_105000351_lis_5_01_xxx",False,"Chara_comment/105000350","Chara_comment/105000351","105000350_cha","105000351_cha","Chara_icon/105000350","Chara_icon/105000351","Chara_icon_loss/105000350","Chara_icon_loss/105000351","2021/11/02 7:00:00",True,"1", -106000380,6,"シリカ","闇色の心頼",5,2,3,0,106000380,201000000,201000000,106000380,1370,137890,174,17634,70,6895,280,28300,105100,10,0,1,0,0,2,0,0,2,0,0,"孤独で寂しい幼い心を照らすのは
深い夜空の光だった。本当は妹の
ように隣を歩きたかったけれど、
次の約束が胸にあれば。",1600,20000,100,10,20,"hlog_106000380_sil_5_01_xxx","hlog_106000381_sil_5_01_xxx",False,"Chara_comment/106000380","Chara_comment/106000381","106000380_cha","106000381_cha","Chara_icon/106000380","Chara_icon/106000381","Chara_icon_loss/106000380","Chara_icon_loss/106000381","2021/11/02 7:00:00",True,"1", -110000260,10,"アルゴ","黒尽くめの密か",5,2,3,0,110000260,201000000,201000000,110000260,1620,163750,186,18618,80,8185,280,28300,105100,10,0,1,0,0,2,0,0,2,0,0,"情報屋として、ときには友として
誰よりも長く剣士の生き様を見て
いたからこそ、漆黒に隠された悲
哀だって知っているのだ。",1600,20000,100,10,20,"hlog_110000260_arg_5_01_xxx","hlog_110000261_arg_5_01_xxx",False,"Chara_comment/110000260","Chara_comment/110000261","110000260_cha","110000261_cha","Chara_icon/110000260","Chara_icon/110000261","Chara_icon_loss/110000260","Chara_icon_loss/110000261","2021/11/02 7:00:00",True,"1", -101000400,1,"キリト","純白の誓い",5,3,1,0,101000400,202000000,-1,101000400,1530,154220,282,28302,75,7710,200,19960,104000,10,0,1,0,0,2,0,0,2,0,0,"その誓いは、白く清廉で、血の
ように紅い決意となって体を巡
る。決して違えることのない、
命よりも尊く大切な約束だ。
",1600,20000,100,10,20,"hlog_101000400_kir_5_01_xxx","hlog_101000401_kir_5_01_xxx",False,"Chara_comment/101000400","Chara_comment/101000401","101000400_cha","101000401_cha","Chara_icon/101000400","Chara_icon/101000401","Chara_icon_loss/101000400","Chara_icon_loss/101000401","2021/11/16 7:00:00",True,"1", -102000540,2,"アスナ","純黒の誓い",5,2,1,0,102000540,201000000,201000000,102000540,1380,138800,282,28302,70,6940,240,23950,104000,10,0,1,0,0,2,0,0,2,0,0,"何にも染まらず、けれど全てを受
け入れる、強くて優しいヒーロー
の黒。誰かを守ってばかりの彼だ
から、自分が守ると決めたのだ。",1600,20000,100,10,20,"hlog_102000540_asu_5_01_xxx","hlog_102000541_asu_5_01_xxx",False,"Chara_comment/102000540","Chara_comment/102000541","102000540_cha","102000541_cha","Chara_icon/102000540","Chara_icon/102000541","Chara_icon_loss/102000540","Chara_icon_loss/102000541","2021/11/16 7:00:00",True,"1", -107000180,7,"クライン","跳躍の托生",5,11,1,0,107000180,211000000,-1,107000180,1580,159670,288,28740,70,7260,140,14520,1,0,0,1,0,0,2,0,0,2,0,0,"博打じみた作戦も、仲間を信頼
すればこそ。背中を預けるべき
瞬間を見極めれば、小さな攻撃
だって次に繋げることができる。",1600,20000,100,10,20,"hlog_107000180_kle_5_01_xxx","hlog_107000181_kle_5_01_xxx",False,"Chara_comment/107000180","Chara_comment/107000181","107000180_cha","107000181_cha","Chara_icon/107000180","Chara_icon/107000181","Chara_icon_loss/107000180","Chara_icon_loss/107000181","2021/11/01 7:00:00",True,"1", -109000400,9,"ユウキ","優姿の眼差し",5,1,3,0,109000400,201000000,-1,109000400,1620,163300,192,19158,75,7350,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"その優しい視線が映すのは、フレ
ームに収めるまでもない、ありき
たりで些細な日常。そんな瞬間こ
そが、一番の宝物だった。",1600,20000,100,10,20,"hlog_109000400_yuu_5_01_xxx","hlog_109000401_yuu_5_01_xxx",False,"Chara_comment/109000400","Chara_comment/109000401","109000400_cha","109000401_cha","Chara_icon/109000400","Chara_icon/109000401","Chara_icon_loss/109000400","Chara_icon_loss/109000401","2021/09/28 7:00:00",True,"1", -110000270,10,"アルゴ","忍ぶ外套",5,8,1,0,110000270,207000000,-1,110000270,1370,137890,246,24822,70,6895,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"フード付きのマントは時折、彼女
のチャームポイントを隠してしま
う。おヒゲの理由を知る日はまだ
遠そうだ。",1600,20000,100,10,20,"hlog_110000270_arg_5_01_xxx","hlog_110000271_arg_5_01_xxx",False,"Chara_comment/110000270","Chara_comment/110000271","110000270_cha","110000271_cha","Chara_icon/110000270","Chara_icon/110000271","Chara_icon_loss/110000270","Chara_icon_loss/110000271","2021/11/02 7:00:00",True,"1", -102000550,2,"アスナ","小径の花環",5,3,2,0,102000550,202000000,-1,102000550,1780,179630,174,17418,90,8980,170,17420,1,0,0,1,0,0,2,0,0,2,0,0,"二人で選んだ花を束ねて、柔らか
な髪の上にそっと載せる。
世界に一つだけの冠は、まるで
ティアラのように輝いて。",1600,20000,100,10,20,"hlog_102000550_asu_5_01_xxx","hlog_102000551_asu_5_01_xxx",False,"Chara_comment/102000550","Chara_comment/102000551","102000550_cha","102000551_cha","Chara_icon/102000550","Chara_icon/102000551","Chara_icon_loss/102000550","Chara_icon_loss/102000551","2021/12/01 7:00:00",True,"1", -104000500,4,"シノン","潜む強弩",5,11,1,0,104000500,211000000,-1,104000500,1150,116120,288,28740,60,5805,200,19960,1,0,0,1,0,0,2,0,0,2,0,0,"戦う敵は状況によって選ぶべし。
耳をそばだて、敵の不意を狙って
飛び出そう。ただし逆立つ尻尾が
気づかれないよう注意すること。",1600,20000,100,10,20,"hlog_104000500_sin_5_01_xxx","hlog_104000501_sin_5_01_xxx",False,"Chara_comment/104000500","Chara_comment/104000501","104000500_cha","104000501_cha","Chara_icon/104000500","Chara_icon/104000501","Chara_icon_loss/104000500","Chara_icon_loss/104000501","2021/12/21 7:00:00",True,"1", -105000360,5,"リズベット","工匠の一驚",5,10,1,0,105000360,209000000,-1,105000360,1440,145150,234,23514,85,8710,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"フィールドでは、一瞬の油断が命
とり。物陰から出た音を聞き逃さ
ず身構えたものの、飛び出してき
たのは予想外の相手だった。",1600,20000,100,10,20,"hlog_105000360_lis_5_01_xxx","hlog_105000361_lis_5_01_xxx",False,"Chara_comment/105000360","Chara_comment/105000361","105000360_cha","105000361_cha","Chara_icon/105000360","Chara_icon/105000361","Chara_icon_loss/105000360","Chara_icon_loss/105000361","2021/12/21 7:00:00",True,"1", -112000190,12,"ユージオ","剣峰の決意",5,1,3,0,112000190,201000000,-1,112000190,1620,163300,174,17418,80,8165,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"知らない未来の自分でも青薔薇の
剣を振るう理由は、今の自分にも
分かる気がする。今よりもっと
別れや出会いがあるはずだから。",1600,20000,100,10,20,"hlog_112000190_ego_5_01_xxx","hlog_112000191_ego_5_01_xxx",False,"Chara_comment/112000190","Chara_comment/112000191","112000190_cha","112000191_cha","Chara_icon/112000190","Chara_icon/112000191","Chara_icon_loss/112000190","Chara_icon_loss/112000191","2021/12/21 7:00:00",True,"1", -101000410,1,"キリト","赦しの一振り",5,2,4,0,101000410,201000000,201000000,101000410,1890,190510,240,23952,95,9525,200,19960,102200,10,0,1,0,0,2,0,0,2,0,0,"その剣の先にどんな悪人がいよう
とも、彼が憎悪をのせることはな
い。なぜなら、守りたいもののた
めに剣を振るうからだ。",1600,20000,100,10,20,"hlog_101000410_kir_5_01_xxx","hlog_101000411_kir_5_01_xxx",False,"Chara_comment/101000410","Chara_comment/101000411","101000410_cha","101000411_cha","Chara_icon/101000410","Chara_icon/101000411","Chara_icon_loss/101000410","Chara_icon_loss/101000411","2021/10/05 7:00:00",True,"1", -102000560,2,"アスナ","正しき一突き",5,3,4,0,102000560,202000000,-1,102000560,1700,171460,240,23952,85,8575,240,23950,102200,10,0,1,0,0,2,0,0,2,0,0,"剣を持つ理由は、剣を持つ人の数
だけある。彼女の細剣の切っ先が
正確に狙いを捉えるのは、揺るが
ない自分がいるからだ。",1600,20000,100,10,20,"hlog_102000560_asu_5_01_xxx","hlog_102000561_asu_5_01_xxx",False,"Chara_comment/102000560","Chara_comment/102000561","102000560_cha","102000561_cha","Chara_icon/102000560","Chara_icon/102000561","Chara_icon_loss/102000560","Chara_icon_loss/102000561","2021/10/05 7:00:00",True,"1", -101000420,1,"キリト","糾明の快刀",5,2,1,0,101000420,201000000,201000000,101000420,1440,145150,258,26130,70,7260,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"夢のような仮想世界で起きた、
数値では説明のつかない不思議な
体験を思い出す。
それは、きっと愛剣にも。",1600,20000,100,10,20,"hlog_101000420_kir_5_01_xxx","hlog_101000421_kir_5_01_xxx",False,"Chara_comment/101000420","Chara_comment/101000421","101000420_cha","101000421_cha","Chara_icon/101000420","Chara_icon/101000421","Chara_icon_loss/101000420","Chara_icon_loss/101000421","2021/12/14 7:00:00",True,"1", -103000460,3,"リーファ","贈る一番星",5,1,1,3,103000460,201000000,-1,103000460,1440,145150,258,26130,60,5805,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"子どものころに読み返した絵本の
内容を夢に見る。鈴の音色を忘れ
ても、素敵な聖夜が等しく訪れる
よう木のてっぺんに星を載せた。",1600,20000,100,10,20,"hlog_103000460_lea_5_01_xxx","hlog_103000461_lea_5_01_xxx",False,"Chara_comment/103000460","Chara_comment/103000461","103000460_cha","103000461_cha","Chara_icon/103000460","Chara_icon/103000461","Chara_icon_loss/103000460","Chara_icon_loss/103000461","2021/12/14 7:00:00",True,"1", -106000390,6,"シリカ","聖しあの夜",5,4,1,3,106000390,203000000,-1,106000390,1220,123380,252,25476,60,6170,240,23950,104000,10,0,1,0,0,2,0,0,2,0,0,"サンタさんに一目会うため被った
毛布の暖かさと戦った幾年の夜。
あの日の胸躍る幸せな微睡を、
あなたにも感じて欲しくて。",1600,20000,100,10,20,"hlog_106000390_sil_5_01_xxx","hlog_106000391_sil_5_01_xxx",False,"Chara_comment/106000390","Chara_comment/106000391","106000390_cha","106000391_cha","Chara_icon/106000390","Chara_icon/106000391","Chara_icon_loss/106000390","Chara_icon_loss/106000391","2021/12/14 7:00:00",True,"1", -110000290,10,"アルゴ","慶びの枕元",5,8,3,3,110000290,207000000,-1,110000290,1620,163750,186,18618,80,8185,280,28300,105100,10,0,1,0,0,2,0,0,2,0,0,"情報屋に死角なし、クリスマス
カタログも網羅していた。今夜だ
けは特別価格。子供の夢が叶う
なら、お代はいらないんだそう。",1600,20000,100,10,20,"hlog_110000290_arg_5_01_xxx","hlog_110000291_arg_5_01_xxx",False,"Chara_comment/110000290","Chara_comment/110000291","110000290_cha","110000291_cha","Chara_icon/110000290","Chara_icon/110000291","Chara_icon_loss/110000290","Chara_icon_loss/110000291","2021/12/14 7:00:00",True,"1", -111000320,11,"アリス","駈走る雪原",5,1,4,3,111000320,201000000,-1,111000320,2070,208660,228,22860,105,10435,170,17240,1,0,0,1,0,0,2,0,0,2,0,0,"空を往くそりを子供と共に追いか
けた。騎士はそりに乗れずとも
隣を歩くことが出来るから、誇ら
しげに麻袋を背負って走った。",1600,20000,100,10,20,"hlog_111000320_alc_5_01_xxx","hlog_111000321_alc_5_01_xxx",False,"Chara_comment/111000320","Chara_comment/111000321","111000320_cha","111000321_cha","Chara_icon/111000320","Chara_icon/111000321","Chara_icon_loss/111000320","Chara_icon_loss/111000321","2021/12/14 7:00:00",True,"1", -118000200,18,"レイン","届ける聖歌",5,2,1,3,118000200,201000000,201000000,118000200,1380,138800,294,29718,75,7710,200,19960,104000,10,0,1,0,0,2,0,0,2,0,0,"夢を届けるサンタさんはアイドル
の彼女にとっては大先輩。いつか
世界中の人に笑顔を届けるため、
今日も歌って踊るから!",1600,20000,100,10,20,"hlog_118000200_rai_5_01_xxx","hlog_118000201_rai_5_01_xxx",False,"Chara_comment/118000200","Chara_comment/118000201","118000200_cha","118000201_cha","Chara_icon/118000200","Chara_icon/118000201","Chara_icon_loss/118000200","Chara_icon_loss/118000201","2021/12/14 7:00:00",True,"1", -110000280,10,"アルゴ","忍ぶ外套・冠",5,8,1,0,110000280,207000000,-1,110000280,1450,146510,264,26892,75,7325,240,23950,1,0,0,1,0,0,2,0,0,2,0,0,"フード付きのマントは時折、彼女
のチャームポイントを隠してしま
う。おヒゲの理由を知る日はまだ
遠そうだ。",1600,20000,100,10,20,"hlog_110000270_arg_5_01_xxx","hlog_110000271_arg_5_01_xxx",False,"Chara_comment/110000270","Chara_comment/110000271","110000270_cha","110000271_cha","Chara_icon/110000270","Chara_icon/110000271","Chara_icon_loss/110000270","Chara_icon_loss/110000271","2021/12/14 7:00:00",False,"1", -102000570,2,"アスナ","相愛は甘やかに",5,3,3,6,102000570,202000000,-1,102000570,1540,155130,192,19596,75,7755,280,28300,105100,10,0,1,0,0,2,0,0,2,0,0,"大好きなあなたに渡す特別な
チョコは、想い想われるほど
甘さと豪華さを増していく。今年
もとびきりのチョコを渡せそう。",1600,20000,100,10,20,"hlog_102000570_asu_5_01_xxx","hlog_102000571_asu_5_01_xxx",False,"Chara_comment/102000570","Chara_comment/102000571","102000570_cha","102000571_cha","Chara_icon/102000570","Chara_icon/102000571","Chara_icon_loss/102000570","Chara_icon_loss/102000571","2022/01/25 7:00:00",True,"1", -104000510,4,"シノン","親愛はとろけて",5,14,1,6,104000510,215000000,-1,104000510,1220,123380,306,31134,60,6170,220,21950,104000,10,0,1,0,0,2,0,0,2,0,0,"本命チョコはバレンタイン当日に
渡したいのが乙女心というもの。
果たして彼女が作った特別なチョ
コレートは一体誰の手に…?",1600,20000,100,10,20,"hlog_104000510_sin_5_01_xxx","hlog_104000511_sin_5_01_xxx",False,"Chara_comment/104000510","Chara_comment/104000511","104000510_cha","104000511_cha","Chara_icon/104000510","Chara_icon/104000511","Chara_icon_loss/104000510","Chara_icon_loss/104000511","2022/01/25 7:00:00",True,"1", -105000370,5,"リズベット","友愛はほろ苦く",5,10,3,6,105000370,209000000,-1,105000370,1620,163300,156,15678,95,9800,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"魔法をかけたチョコレートには、
甘い気持ちが込められている。
けれど、親友のことを想うと、
たまにほろ苦く感じて。",1600,20000,100,10,20,"hlog_105000370_lis_5_01_xxx","hlog_105000371_lis_5_01_xxx",False,"Chara_comment/105000370","Chara_comment/105000371","105000370_cha","105000371_cha","Chara_icon/105000370","Chara_icon/105000371","Chara_icon_loss/105000370","Chara_icon_loss/105000371","2022/01/25 7:00:00",True,"1", -111000330,11,"アリス","愛慕は未だ青く",5,1,4,6,111000330,201000000,-1,111000330,2170,219090,252,25146,110,10955,190,18960,102200,10,0,1,0,0,2,0,0,2,0,0,"バレンタインチョコレートが持つ
意味は一つとは限らない。特別に
大切な相手に渡す本命チョコを、
彼女が渡す日はくるのだろうか。",1600,20000,100,10,20,"hlog_111000330_alc_5_01_xxx","hlog_111000331_alc_5_01_xxx",False,"Chara_comment/111000330","Chara_comment/111000331","111000330_cha","111000331_cha","Chara_icon/111000330","Chara_icon/111000331","Chara_icon_loss/111000330","Chara_icon_loss/111000331","2022/01/25 7:00:00",True,"1", -109000410,9,"ユウキ","狙い撃つ軌道",5,14,1,0,109000410,215000000,-1,109000410,1530,154220,306,31134,70,6940,200,19960,104000,10,0,1,0,0,2,0,0,2,0,0,"友人の狙撃銃の扱いに四苦八苦し
ながらお揃いを喜んでいるよう。
反動をその身に受けつつ、武器を
モノにせんと励んでいる。",1600,20000,100,10,20,"hlog_109000410_yuu_5_01_xxx","hlog_109000411_yuu_5_01_xxx",False,"Chara_comment/109000410","Chara_comment/109000411","109000410_cha","109000411_cha","Chara_icon/109000410","Chara_icon/109000411","Chara_icon_loss/109000410","Chara_icon_loss/109000411","2022/02/15 7:00:00",True,"1", -112000200,12,"ユージオ","魔剣稽古",5,1,1,0,112000200,201000000,-1,112000200,1530,154220,282,28302,75,7710,200,19960,104000,10,0,1,0,0,2,0,0,2,0,0,"急に消えたと思ったら現れる刀身
に不慣れな様子。稽古中に誤って
消してしまうこともあるそう。
まずは慣れるために奮闘中だ。",1600,20000,100,10,20,"hlog_112000200_ego_5_01_xxx","hlog_112000201_ego_5_01_xxx",False,"Chara_comment/112000200","Chara_comment/112000201","112000200_cha","112000201_cha","Chara_icon/112000200","Chara_icon/112000201","Chara_icon_loss/112000200","Chara_icon_loss/112000201","2022/02/15 7:00:00",True,"1", -118000210,18,"レイン","虹架かる団らん",5,10,1,0,118000210,209000000,-1,118000210,1300,130640,270,27432,70,7260,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"招かれた家は優しくて温かくて、
幸せな雰囲気で満たされていた。
美味しいお茶に舌鼓を打っている
と窓の外には虹が広がって…。",1600,20000,100,10,20,"hlog_118000210_rai_5_01_xxx","hlog_118000211_rai_5_01_xxx",False,"Chara_comment/118000210","Chara_comment/118000211","118000210_cha","118000211_cha","Chara_icon/118000210","Chara_icon/118000211","Chara_icon_loss/118000210","Chara_icon_loss/118000211","2022/02/01 7:00:00",True,"1", -103000470,3,"リーファ","序数の続きを",5,1,1,0,103000470,201000000,-1,103000470,1440,145150,258,26130,60,5805,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"剣道部で活躍する彼女にとって
体を動かすことはお手の物。
拡張現実の戦闘も仮想世界の如く
軽やかな身のこなしを披露する。",1600,20000,100,10,20,"hlog_103000470_lea_5_01_xxx","hlog_103000471_lea_5_01_xxx",False,"Chara_comment/103000470","Chara_comment/103000471","103000470_cha","103000471_cha","Chara_icon/103000470","Chara_icon/103000471","Chara_icon_loss/103000470","Chara_icon_loss/103000471","2022/03/29 7:00:00",True,"1", -109000420,9,"ユウキ","並び立つ基数",5,1,1,0,109000420,201000000,-1,109000420,1440,145150,288,28740,65,6530,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"《オーディナル・スケール》の
装備は彼女にとって特別なもの。
いつか仮想現実を飛び出して、肩
を並べられたらと、願をかけた。",1600,20000,100,10,20,"hlog_109000420_yuu_5_01_xxx","hlog_109000421_yuu_5_01_xxx",False,"Chara_comment/109000420","Chara_comment/109000421","109000420_cha","109000421_cha","Chara_icon/109000420","Chara_icon/109000421","Chara_icon_loss/109000420","Chara_icon_loss/109000421","2022/03/29 7:00:00",True,"1", -111000340,11,"アリス","陽炎の傾聴",5,1,1,0,111000340,201000000,-1,111000340,1660,166920,270,27432,85,8345,170,17240,1,0,0,1,0,0,2,0,0,2,0,0,"遠くにいるのに近い声、本当は
ないものをあるように映す…。
たとえ陽炎のような存在でも、
あなたともう一度出会いたい。",1600,20000,100,10,20,"hlog_111000340_alc_5_01_xxx","hlog_111000341_alc_5_01_xxx",False,"Chara_comment/111000340","Chara_comment/111000341","111000340_cha","111000341_cha","Chara_icon/111000340","Chara_icon/111000341","Chara_icon_loss/111000340","Chara_icon_loss/111000341","2022/03/29 7:00:00",True,"1", -101000430,1,"キリト","受け止める軌跡",5,2,4,0,101000430,201000000,201000000,101000430,1800,181440,216,21774,90,9070,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"あなたと歩んだ3年間。それは、
彼が仲間の想いを受け止めた
軌跡。誰の想いも零さないよう、
あなたと同じ景色を見ていたい。",1600,20000,100,10,20,"hlog_101000430_kir_5_01_xxx","hlog_101000431_kir_5_01_xxx",False,"Chara_comment/101000430","Chara_comment/101000431","101000430_cha","101000431_cha","Chara_icon/101000430","Chara_icon/101000431","Chara_icon_loss/101000430","Chara_icon_loss/101000431","2022/03/08 7:00:00",True,"1", -102000580,2,"アスナ","見つめる軌跡",5,3,4,0,102000580,202000000,-1,102000580,1620,163300,216,21774,80,8165,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"あなたと歩んだ3年間。それは、
彼女が自らの正しさを見つめた
軌跡。どんな姿であろうとも、
その正しさは損なわれない。",1600,20000,100,10,20,"hlog_102000580_asu_5_01_xxx","hlog_102000581_asu_5_01_xxx",False,"Chara_comment/102000580","Chara_comment/102000581","102000580_cha","102000581_cha","Chara_icon/102000580","Chara_icon/102000581","Chara_icon_loss/102000580","Chara_icon_loss/102000581","2022/03/08 7:00:00",True,"1", -103000480,3,"リーファ","信じきる軌跡",5,1,4,0,103000480,201000000,-1,103000480,1800,181440,216,21774,70,7260,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"あなたと歩んだ3年間。それは、
彼女が隣人を信じ続けた軌跡。
それがどれだけ難しくても、
あなたといれば勇気を貰えて。",1600,20000,100,10,20,"hlog_103000480_lea_5_01_xxx","hlog_103000481_lea_5_01_xxx",False,"Chara_comment/103000480","Chara_comment/103000481","103000480_cha","103000481_cha","Chara_icon/103000480","Chara_icon/103000481","Chara_icon_loss/103000480","Chara_icon_loss/103000481","2022/03/08 7:00:00",True,"1", -104000520,4,"シノン","向き合う軌跡",5,14,4,0,104000520,215000000,-1,104000520,1440,145150,240,23952,70,7260,200,19960,1,0,0,1,0,0,2,0,0,2,0,0,"あなたと歩んだ3年間。それは、
彼女が自身の弱さと向き合った
軌跡。あなたと共に乗り越えた
記憶が、彼女を強くしていった。",1600,20000,100,10,20,"hlog_104000520_sin_5_01_xxx","hlog_104000521_sin_5_01_xxx",False,"Chara_comment/104000520","Chara_comment/104000521","104000520_cha","104000521_cha","Chara_icon/104000520","Chara_icon/104000521","Chara_icon_loss/104000520","Chara_icon_loss/104000521","2022/03/08 7:00:00",True,"1", -109000430,9,"ユウキ","確かめる軌跡",5,1,4,0,109000430,201000000,-1,109000430,1800,181440,240,23952,80,8165,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"あなたと歩んだ3年間。それは、
彼女が生きる喜びを確かめた
軌跡。あなたと過ごす刺激的な
日々が、ずっと続きますように。",1600,20000,100,10,20,"hlog_109000430_yuu_5_01_xxx","hlog_109000431_yuu_5_01_xxx",False,"Chara_comment/109000430","Chara_comment/109000431","109000430_cha","109000431_cha","Chara_icon/109000430","Chara_icon/109000431","Chara_icon_loss/109000430","Chara_icon_loss/109000431","2022/03/08 7:00:00",True,"1", -111000350,11,"アリス","思い返す軌跡",5,1,4,0,111000350,201000000,-1,111000350,2070,208660,228,22860,105,10435,170,17240,1,0,0,1,0,0,2,0,0,2,0,0,"あなたと歩んだ3年間。それは、
彼女が使命を思い返した軌跡。
己を見失わなかったのは、
あなたという道標がいたからだ。",1600,20000,100,10,20,"hlog_111000350_alc_5_01_xxx","hlog_111000351_alc_5_01_xxx",False,"Chara_comment/111000350","Chara_comment/111000351","111000350_cha","111000351_cha","Chara_icon/111000350","Chara_icon/111000351","Chara_icon_loss/111000350","Chara_icon_loss/111000351","2022/03/08 7:00:00",True,"1", -104000530,4,"シノン","天翔る女神",5,11,1,0,104000530,211000000,-1,104000530,1150,116120,288,28740,60,5805,200,19960,1,0,0,1,0,0,2,0,0,2,0,0,"太陽を司る女神を模したという
アバターの神々しい飛翔姿。
この姿でいるときは、
いつもより背筋が伸びるよう。",1600,20000,100,10,20,"hlog_104000530_sin_5_01_xxx","hlog_104000531_sin_5_01_xxx",False,"Chara_comment/104000530","Chara_comment/104000531","104000530_cha","104000531_cha","Chara_icon/104000530","Chara_icon/104000531","Chara_icon_loss/104000530","Chara_icon_loss/104000531","2022/03/08 7:00:00",True,"1", -104000540,4,"シノン","厳格手ほどき",5,11,4,5,104000540,211000000,-1,104000540,1510,152410,264,26346,75,7620,220,21950,111500,0,0,1,0,0,2,0,0,2,0,0,"いつも頼もしい仲間たちが、教卓
から見渡すと子供っぽく見える。
知らない一面に出会えた気がして
気付かず尻尾が揺れた。",1600,20000,100,10,20,"hlog_104000540_sin_5_01_xxx","hlog_104000541_sin_5_01_xxx",False,"Chara_comment/104000540","Chara_comment/104000541","104000540_cha","104000541_cha","Chara_icon/104000540","Chara_icon/104000541","Chara_icon_loss/104000540","Chara_icon_loss/104000541","2022/03/08 7:00:00",True,"1", -105000380,5,"リズベット","ほどけぬ胸飾り",5,10,4,5,105000380,209000000,-1,105000380,1890,190510,216,21558,115,11430,200,19960,111500,0,0,1,0,0,2,0,0,2,0,0,"たとえ着ている服が同じでも、
誰よりも可愛い自分でいたい。
赤いリボンを結んだ先に、
大切なあなたが待っているから。",1600,20000,100,10,20,"hlog_105000380_lis_5_01_xxx","hlog_105000381_lis_5_01_xxx",False,"Chara_comment/105000380","Chara_comment/105000381","105000380_cha","105000381_cha","Chara_icon/105000380","Chara_icon/105000381","Chara_icon_loss/105000380","Chara_icon_loss/105000381","2022/03/08 7:00:00",True,"1", -106000400,6,"シリカ","夢想の放課後",5,4,4,5,106000400,203000000,-1,106000400,1510,152410,216,21558,75,7620,240,23950,111500,0,0,1,0,0,2,0,0,2,0,0,"放課後は女子高校生らしくカフェ
やゲームセンターを楽しむ彼女。
もしあなたと過ごせたら、
巡りたい場所がたくさんあるの。",1600,20000,100,10,20,"hlog_106000400_sil_5_01_xxx","hlog_106000401_sil_5_01_xxx",False,"Chara_comment/106000400","Chara_comment/106000401","106000400_cha","106000401_cha","Chara_icon/106000400","Chara_icon/106000401","Chara_icon_loss/106000400","Chara_icon_loss/106000401","2022/03/08 7:00:00",True,"1", -102000590,2,"アスナ","熊の招宴",5,8,3,0,102000590,207000000,-1,102000590,1540,155130,192,19596,75,7755,280,28300,105100,10,0,1,0,0,2,0,0,2,0,0,"テディベアとお揃いのパジャマで
パジャマパーティーに向かう。
お互いのことを語り合った夜、
縮まった距離が嬉しくて。",1600,20000,100,10,20,"hlog_102000590_asu_5_01_xxx","hlog_102000591_asu_5_01_xxx",False,"Chara_comment/102000590","Chara_comment/102000591","102000590_cha","102000591_cha","Chara_icon/102000590","Chara_icon/102000591","Chara_icon_loss/102000590","Chara_icon_loss/102000591","2022/03/29 7:00:00",True,"1", -111000360,11,"アリス","兎の密会",5,8,1,0,111000360,207000000,-1,111000360,1760,177360,294,29718,90,8870,190,18960,104000,10,0,1,0,0,2,0,0,2,0,0,"その秘密の会合は、
夜の帳が降りてから開かれる。
ドレスコードと渡された装いは、
想像以上にファンシーで?!",1600,20000,100,10,20,"hlog_111000360_alc_5_01_xxx","hlog_111000361_alc_5_01_xxx",False,"Chara_comment/111000360","Chara_comment/111000361","111000360_cha","111000361_cha","Chara_icon/111000360","Chara_icon/111000361","Chara_icon_loss/111000360","Chara_icon_loss/111000361","2022/03/29 7:00:00",True,"1", -105000390,5,"リズベット","約束の花束",5,10,1,0,105000390,209000000,-1,105000390,1440,145150,234,23514,85,8710,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"花々の甘い香りに包まれながら、
あなたと穏やかな時間を過ごす。
一輪ずつ集めるたびに、知らない
あなたを知れた気がした。",1600,20000,100,10,20,"hlog_105000390_lis_5_01_xxx","hlog_105000391_lis_5_01_xxx",False,"Chara_comment/105000390","Chara_comment/105000391","105000390_cha","105000391_cha","Chara_icon/105000390","Chara_icon/105000391","Chara_icon_loss/105000390","Chara_icon_loss/105000391","2022/04/01 7:00:00",True,"1", -110000300,10,"アルゴ","炯々たる瞳",5,8,1,0,110000300,207000000,-1,110000300,1370,137890,246,24822,70,6895,220,21770,1,0,0,1,0,0,2,0,0,2,0,0,"視線の先にはご馳走と、まだ見ぬ
レシピの新情報!美味しいご飯と
情報屋の明るい未来を想像して、
瞳を鋭く光らせた。",1600,20000,100,10,20,"hlog_110000300_arg_5_01_xxx","hlog_110000301_arg_5_01_xxx",False,"Chara_comment/110000300","Chara_comment/110000301","110000300_cha","110000301_cha","Chara_icon/110000300","Chara_icon/110000301","Chara_icon_loss/110000300","Chara_icon_loss/110000301","2022/03/29 7:00:00",True,"1", -106000410,6,"シリカ","一杯の息抜き",5,4,3,6,106000410,203000000,-1,106000410,1300,130640,156,15678,65,6530,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"メイド心得其の一、紅茶は熱々で
淹れる。大切なご主人様には最高
の紅茶を出したいのがメイド心。
あとは、慎重に運ぶだけだが…。",1600,20000,100,10,20,"hlog_106000410_sil_5_01_xxx","hlog_106000411_sil_5_01_xxx",False,"Chara_comment/106000410","Chara_comment/106000411","106000410_cha","106000411_cha","Chara_icon/106000410","Chara_icon/106000411","Chara_icon_loss/106000410","Chara_icon_loss/106000411","2022/04/26 7:00:00",True,"1", -103000490,3,"リーファ","零れる活力",5,6,1,6,103000490,205000000,-1,103000490,1530,154220,282,28302,60,6170,240,23950,104000,10,0,1,0,0,2,0,0,2,0,0,"メイド心得其の二、埃は一つも見
逃さない。ご主人様が暮らす場所
は綺麗にしたい。そう思って、
バケツになみなみと水を入れた。",1600,20000,100,10,20,"hlog_103000490_lea_5_01_xxx","hlog_103000491_lea_5_01_xxx",False,"Chara_comment/103000490","Chara_comment/103000491","103000490_cha","103000491_cha","Chara_icon/103000490","Chara_icon/103000491","Chara_icon_loss/103000490","Chara_icon_loss/103000491","2022/04/26 7:00:00",True,"1", -109000440,9,"ユウキ","瞬きの宙",5,1,4,6,109000440,201000000,-1,109000440,1890,190510,264,26346,85,8575,200,19960,102200,10,0,1,0,0,2,0,0,2,0,0,"メイド心得其の三、如何なる時も
楽しんで。広いお屋敷のお掃除は
大変だけど、楽しんでしまえば
あっという間だ。",1600,20000,100,10,20,"hlog_109000440_yuu_5_01_xxx","hlog_109000441_yuu_5_01_xxx",False,"Chara_comment/109000440","Chara_comment/109000441","109000440_cha","109000441_cha","Chara_icon/109000440","Chara_icon/109000441","Chara_icon_loss/109000440","Chara_icon_loss/109000441","2022/04/26 7:00:00",True,"1", -118000220,18,"レイン","就床の一揖",5,2,1,6,118000220,201000000,201000000,118000220,1380,138800,294,29718,75,7710,200,19960,104000,10,0,1,0,0,2,0,0,2,0,0,"メイド心得其の四、ふかふかベッ
ドを用意。ご主人様の疲れを癒す
ため晴れた日は天日干しが必須。
お日様の香りでおやすみなさい。",1600,20000,100,10,20,"hlog_118000220_rai_5_01_xxx","hlog_118000221_rai_5_01_xxx",False,"Chara_comment/118000220","Chara_comment/118000221","118000220_cha","118000221_cha","Chara_icon/118000220","Chara_icon/118000221","Chara_icon_loss/118000220","Chara_icon_loss/118000221","2022/04/26 7:00:00",True,"1", -105000400,5,"リズベット","不屈の鍛冶屋魂",5,10,3,0,105000400,209000000,-1,105000400,1710,172370,174,17634,105,10340,230,23590,105100,10,0,1,0,0,2,0,0,2,0,0,"鍛冶師リズベットとしての人生を
右手のハンマー一本で切り開いて
きた。その経験が、彼女の鍛冶師
としての矜持を裏打ちしている。",1600,20000,100,10,20,"hlog_105000400_lis_5_01_xxx","hlog_105000401_lis_5_01_xxx",False,"Chara_comment/105000400","Chara_comment/105000401","105000400_cha","105000401_cha","Chara_icon/105000400","Chara_icon/105000401","Chara_icon_loss/105000400","Chara_icon_loss/105000401","2022/04/26 7:00:00",True,"1", -106000420,6,"シリカ","絆が生む力",5,4,3,0,106000420,203000000,-1,106000420,1370,137890,174,17634,70,6895,280,28300,105100,10,0,1,0,0,2,0,0,2,0,0,"彼女と相棒のピナはいつも一緒。
一人と一匹の間に隠し事は
ひとつもない。これからも共に
様々な冒険をしていくだろう。",1600,20000,100,10,20,"hlog_106000420_sil_5_01_xxx","hlog_106000421_sil_5_01_xxx",False,"Chara_comment/106000420","Chara_comment/106000421","106000420_cha","106000421_cha","Chara_icon/106000420","Chara_icon/106000421","Chara_icon_loss/106000420","Chara_icon_loss/106000421","2022/04/26 7:00:00",True,"1", -101000440,1,"キリト","頬張り黒猫",5,8,1,4,101000440,207000000,-1,101000440,1530,154220,282,28302,75,7710,200,19960,104000,10,0,1,0,0,2,0,0,2,0,0,"鼠のおもちゃにひとっとび!
猫の本能に抗えず獲物をくわえる
その姿は、クールな黒の剣士の
イメージからは程遠い。",1600,20000,100,10,20,"hlog_101000440_kir_5_01_xxx","hlog_101000441_kir_5_01_xxx",False,"Chara_comment/101000440","Chara_comment/101000441","101000440_cha","101000441_cha","Chara_icon/101000440","Chara_icon/101000441","Chara_icon_loss/101000440","Chara_icon_loss/101000441","2022/04/26 7:00:00",True,"1", -102000600,2,"アスナ","身支度愛猫",5,8,1,4,102000600,207000000,-1,102000600,1380,138800,282,28302,70,6940,240,23950,104000,10,0,1,0,0,2,0,0,2,0,0,"身だしなみは足元ではなく尻尾
から。熱心な毛繕いは身だしなみ
のためと言うが、耳を澄ませば
ゴロゴロと音が聞こえきて…?",1600,20000,100,10,20,"hlog_102000600_asu_5_01_xxx","hlog_102000601_asu_5_01_xxx",False,"Chara_comment/102000600","Chara_comment/102000601","102000600_cha","102000601_cha","Chara_icon/102000600","Chara_icon/102000601","Chara_icon_loss/102000600","Chara_icon_loss/102000601","2022/04/26 7:00:00",True,"1", -103000500,3,"リーファ","養う英気",5,6,4,5,103000500,205000000,-1,103000500,1890,190510,240,23952,75,7620,240,23950,111500,0,0,1,0,0,2,0,0,2,0,0,"学校生活は恋に勉強に部活に、
とにかく毎日が大忙し。
何事も全力で取り組めるよう、
お弁当を食べて力をつけよう。",1600,20000,100,10,20,"hlog_103000500_lea_5_01_xxx","hlog_103000501_lea_5_01_xxx",False,"Chara_comment/103000500","Chara_comment/103000501","103000500_cha","103000501_cha","Chara_icon/103000500","Chara_icon/103000501","Chara_icon_loss/103000500","Chara_icon_loss/103000501","2022/04/26 7:00:00",True,"1", -110000310,10,"アルゴ","秘めたる日誌",5,8,4,0,110000310,207000000,-1,110000310,1800,180990,228,22752,90,9050,240,23950,111500,0,0,1,0,0,2,0,0,2,0,0,"彼女の日直日誌はマル秘情報が
満載。あの人の意外な一面を
知りたいなら、一見の価値あり。
……モチロン、お代は頂くヨ。",1600,20000,100,10,20,"hlog_110000310_arg_5_01_xxx","hlog_110000311_arg_5_01_xxx",False,"Chara_comment/110000310","Chara_comment/110000311","110000310_cha","110000311_cha","Chara_icon/110000310","Chara_icon/110000311","Chara_icon_loss/110000310","Chara_icon_loss/110000311","2022/04/26 7:00:00",True,"1", -118000230,18,"レイン","即席軽音楽団",5,2,4,5,118000230,201000000,201000000,118000230,1700,171460,252,25146,95,9525,200,19960,111500,0,0,1,0,0,2,0,0,2,0,0,"帰りのホームルームが終わったら
皆でお掃除の時間……、のはずが
教室がライブ会場に大変身!
ボーカルは、もちろん彼女!",1600,20000,100,10,20,"hlog_118000230_rai_5_01_xxx","hlog_118000231_rai_5_01_xxx",False,"Chara_comment/118000230","Chara_comment/118000231","118000230_cha","118000231_cha","Chara_icon/118000230","Chara_icon/118000231","Chara_icon_loss/118000230","Chara_icon_loss/118000231","2022/04/26 7:00:00",True,"1", -101000450,1,"キリト","明日への一葉",5,2,4,0,101000450,201000000,201000000,101000450,1800,181440,216,21774,90,9070,180,18140,1,0,0,1,0,0,2,0,0,2,0,0,"あなたと共に見渡すこの世界が
仮想現実の未来を明るく照らす。
信頼するあなたと共に、明日へ
向かって歩き出したい。",1600,20000,100,10,20,"hlog_101000450_kir_5_01_xxx","hlog_101000451_kir_5_01_xxx",False,"Chara_comment/101000450","Chara_comment/101000451","101000450_cha","101000451_cha","Chara_icon/101000450","Chara_icon/101000451","Chara_icon_loss/101000450","Chara_icon_loss/101000451","2022/03/08 7:00:00",True,"1", -102000610,2,"アスナ","流露の一葉",5,3,3,0,102000610,202000000,-1,102000610,1460,146970,174,17418,75,7350,260,26130,1,0,0,1,0,0,2,0,0,2,0,0,"あなたと巡り合ったこの世界は
微睡のように心地よく、記憶を
想い起こせば、自然と頬がゆる
んでしまって。",1600,20000,100,10,20,"hlog_102000610_asu_5_01_xxx","hlog_102000611_asu_5_01_xxx",False,"Chara_comment/102000610","Chara_comment/102000611","102000610_cha","102000611_cha","Chara_icon/102000610","Chara_icon/102000611","Chara_icon_loss/102000610","Chara_icon_loss/102000611","2022/03/08 7:00:00",True,"1", -103000510,3,"リーファ","篤実の一葉",5,1,2,0,103000510,201000000,-1,103000510,1980,199580,174,17418,80,7985,170,17420,1,0,0,1,0,0,2,0,0,2,0,0,"明るくて素直な彼女が引き寄せた
あなたとの繋がりが、この世界の
思い出をより輝かせる。
今も昔も、きっと、これからも。",1600,20000,100,10,20,"hlog_103000510_lea_5_01_xxx","hlog_103000511_lea_5_01_xxx",False,"Chara_comment/103000510","Chara_comment/103000511","103000510_cha","103000511_cha","Chara_icon/103000510","Chara_icon/103000511","Chara_icon_loss/103000510","Chara_icon_loss/103000511","2022/03/08 7:00:00",True,"1", -104000550,4,"シノン","有縁の一葉",5,14,1,0,104000550,215000000,-1,104000550,1150,116120,288,28740,60,5805,200,19960,1,0,0,1,0,0,2,0,0,2,0,0,"あなたと出会った過去があり、
あなたとこの世界を駆ける今が
ある。今日が昔に変わっても、
繋いだ縁は永久に続いていく。",1600,20000,100,10,20,"hlog_104000550_sin_5_01_xxx","hlog_104000551_sin_5_01_xxx",False,"Chara_comment/104000550","Chara_comment/104000551","104000550_cha","104000551_cha","Chara_icon/104000550","Chara_icon/104000551","Chara_icon_loss/104000550","Chara_icon_loss/104000551","2022/03/08 7:00:00",True,"1", diff --git a/titles/sao/data/Item.csv b/titles/sao/data/Item.csv deleted file mode 100644 index fc780af..0000000 --- a/titles/sao/data/Item.csv +++ /dev/null @@ -1,207 +0,0 @@ -ItemId,ItemTypeId,Name,Rarity,Value,PropertyId,PropertyValue1Min,PropertyValue1Max,PropertyValue2Min,PropertyValue2Max,FlavorText,SalePrice,ItemIcon, -110020,1,"ヒーローコード:HP上昇",1,0,100200,0,20,0,0,"ヒーローログに能力合成を行うと
HP+200~800の
プロパティを付与できる。",100,"ITEM_110020", -110021,1,"ヒーローコード:HP上昇",2,0,100200,20,40,0,0,"ヒーローログに能力合成を行うと
HP+800~1400の
プロパティを付与できる。",100,"ITEM_110020", -110022,1,"ヒーローコード:HP上昇",3,0,100200,40,60,0,0,"ヒーローログに能力合成を行うと
HP+1400~2000の
プロパティを付与できる。",100,"ITEM_110020", -110023,1,"ヒーローコード:HP上昇",4,0,100200,60,80,0,0,"ヒーローログに能力合成を行うと
HP+2000~2600の
プロパティを付与できる。",100,"ITEM_110020", -110024,1,"ヒーローコード:HP上昇",5,0,100200,80,100,0,0,"ヒーローログに能力合成を行うと
HP+2600~3200の
プロパティを付与できる。",100,"ITEM_110020", -110030,1,"ヒーローコード:STR上昇",1,0,100300,0,20,0,0,"ヒーローログに能力合成を行うと
STR+20~80の
プロパティを付与できる。",100,"ITEM_110030", -110031,1,"ヒーローコード:STR上昇",2,0,100300,20,40,0,0,"ヒーローログに能力合成を行うと
STR+80~140の
プロパティを付与できる。",100,"ITEM_110030", -110032,1,"ヒーローコード:STR上昇",3,0,100300,40,60,0,0,"ヒーローログに能力合成を行うと
STR+140~200の
プロパティを付与できる。",100,"ITEM_110030", -110033,1,"ヒーローコード:STR上昇",4,0,100300,60,80,0,0,"ヒーローログに能力合成を行うと
STR+200~260の
プロパティを付与できる。",100,"ITEM_110030", -110034,1,"ヒーローコード:STR上昇",5,0,100300,80,100,0,0,"ヒーローログに能力合成を行うと
STR+260~320の
プロパティを付与できる。",100,"ITEM_110030", -110040,1,"ヒーローコード:VIT上昇",1,0,100400,0,20,0,0,"ヒーローログに能力合成を行うと
VIT+20~80の
プロパティを付与できる。",100,"ITEM_110040", -110041,1,"ヒーローコード:VIT上昇",2,0,100400,20,40,0,0,"ヒーローログに能力合成を行うと
VIT+80~140の
プロパティを付与できる。",100,"ITEM_110040", -110042,1,"ヒーローコード:VIT上昇",3,0,100400,40,60,0,0,"ヒーローログに能力合成を行うと
VIT+140~200の
プロパティを付与できる。",100,"ITEM_110040", -110043,1,"ヒーローコード:VIT上昇",4,0,100400,60,80,0,0,"ヒーローログに能力合成を行うと
VIT+200~260の
プロパティを付与できる。",100,"ITEM_110040", -110044,1,"ヒーローコード:VIT上昇",5,0,100400,80,100,0,0,"ヒーローログに能力合成を行うと
VIT+260~320の
プロパティを付与できる。",100,"ITEM_110040", -110050,1,"ヒーローコード:INT上昇",1,0,100500,0,20,0,0,"ヒーローログに能力合成を行うと
INT+20~80の
プロパティを付与できる。",100,"ITEM_110050", -110051,1,"ヒーローコード:INT上昇",2,0,100500,20,40,0,0,"ヒーローログに能力合成を行うと
INT+80~140の
プロパティを付与できる。",100,"ITEM_110050", -110052,1,"ヒーローコード:INT上昇",3,0,100500,40,60,0,0,"ヒーローログに能力合成を行うと
INT+140~200の
プロパティを付与できる。",100,"ITEM_110050", -110053,1,"ヒーローコード:INT上昇",4,0,100500,60,80,0,0,"ヒーローログに能力合成を行うと
INT+200~260の
プロパティを付与できる。",100,"ITEM_110050", -110054,1,"ヒーローコード:INT上昇",5,0,100500,80,100,0,0,"ヒーローログに能力合成を行うと
INT+260~320の
プロパティを付与できる。",100,"ITEM_110050", -110055,1,"ヒーローコード:INT上昇",5,0,100510,50,100,0,0,"ヒーローログに能力合成を行うと
INT+340~640の
プロパティを付与できる。",100,"ITEM_110051", -110060,1,"ヒーローコード:斬耐性",3,0,100600,1,20,0,0,"ヒーローログに能力合成を行うと
斬属性の被ダメージを
1%~20%減少する
プロパティを付与できる。",100,"ITEM_110060", -110070,1,"ヒーローコード:打耐性",3,0,100700,1,20,0,0,"ヒーローログに能力合成を行うと
打属性の被ダメージを
1%~20%減少する
プロパティを付与できる。",100,"ITEM_110070", -110080,1,"ヒーローコード:突耐性",3,0,100800,1,20,0,0,"ヒーローログに能力合成を行うと
突属性の被ダメージを
1%~20%減少する
プロパティを付与できる。",100,"ITEM_110080", -110090,1,"ヒーローコード:火耐性",3,0,100900,20,50,0,0,"ヒーローログに能力合成を行うと
火属性の被ダメージを
20%~50%減少する
プロパティを付与できる。",100,"ITEM_110090", -110100,1,"ヒーローコード:水耐性",3,0,101000,20,50,0,0,"ヒーローログに能力合成を行うと
水属性の被ダメージを
20%~50%減少する
プロパティを付与できる。",100,"ITEM_110100", -110110,1,"ヒーローコード:風耐性",3,0,101100,20,50,0,0,"ヒーローログに能力合成を行うと
風属性の被ダメージを
20%~50%減少する
プロパティを付与できる。",100,"ITEM_110110", -110120,1,"ヒーローコード:土耐性",3,0,101200,20,50,0,0,"ヒーローログに能力合成を行うと
土属性の被ダメージを
20%~50%減少する
プロパティを付与できる。",100,"ITEM_110120", -110130,1,"ヒーローコード:聖耐性",3,0,101300,20,50,0,0,"ヒーローログに能力合成を行うと
聖属性の被ダメージを
20%~50%減少する
プロパティを付与できる。",100,"ITEM_110130", -110140,1,"ヒーローコード:闇耐性",3,0,101400,20,50,0,0,"ヒーローログに能力合成を行うと
闇属性の被ダメージを
20%~50%減少する
プロパティを付与できる。",100,"ITEM_110140", -110150,1,"ヒーローコード:毒無効",3,0,101500,0,0,0,0,"ヒーローログに能力合成を行うと
毒無効の
プロパティを付与できる。",100,"ITEM_110150", -110160,1,"ヒーローコード:混乱無効",3,0,101600,0,0,0,0,"ヒーローログに能力合成を行うと
混乱無効の
プロパティを付与できる。",100,"ITEM_110160", -110170,1,"ヒーローコード:封印無効",3,0,101700,0,0,0,0,"ヒーローログに能力合成を行うと
封印無効の
プロパティを付与できる。",100,"ITEM_110170", -110180,1,"ヒーローコード:地震無効",3,0,101800,0,0,0,0,"ヒーローログに能力合成を行うと
地震無効の
プロパティを付与できる。",100,"ITEM_110180", -110190,1,"ヒーローコード:Col増加",3,0,101900,10,50,0,0,"ヒーローログに能力合成を行うと
クエストでの獲得Colを
10%~50%増加させる
プロパティを付与できる。",100,"ITEM_110190", -110191,1,"ヒーローコード:Col増加",4,0,101900,40,50,0,0,"ヒーローログに能力合成を行うと
クエストでの獲得Colを
40%~50%増加させる
プロパティを付与できる。",100,"ITEM_110190", -110200,1,"ヒーローコード:EXP増加",3,0,102000,5,25,0,0,"ヒーローログに能力合成を行うと
クエストでの獲得EXPを
5%~25%増加する
プロパティを付与できる。",100,"ITEM_110200", -110210,1,"ヒーローコード:探索効率上昇",3,0,102100,2,20,0,0,"ヒーローログに能力合成を行うと
探索効率を
2%~20%上昇させる
プロパティを付与できる。",100,"ITEM_110211", -110220,1,"ヒーローコード:CT短縮",3,0,102200,5,5,0,0,"ヒーローログに能力合成を行うと
全スキルのクールタイムを
5%短縮する
プロパティを付与できる。",100,"ITEM_110220", -110221,1,"ヒーローコード:CT短縮",5,0,102200,5,10,0,0,"ヒーローログに能力合成を行うと
全スキルのクールタイムを
5%~10%短縮する
プロパティを付与できる。",100,"ITEM_110220", -110230,1,"ヒーローコード:修復時間短縮",3,0,102300,20,40,0,0,"ヒーローログに能力合成を行うと
全損状態の修復時間を
20%~40%短縮する
プロパティを付与できる。",100,"ITEM_110230", -110240,1,"ヒーローコード:ボスキラー",3,0,102400,10,30,0,0,"ヒーローログに能力合成を行うと
ボスへのダメージを
10%~30%上昇する
プロパティを付与できる。",100,"ITEM_110240", -110241,1,"ヒーローコード:ボスキラー",5,0,102400,50,100,0,0,"ヒーローログに能力合成を行うと
ボスへのダメージを
50%~100%上昇する
プロパティを付与できる。",100,"ITEM_110240", -110250,1,"ヒーローコード:Mobキラー",3,0,102500,10,30,0,0,"ヒーローログに能力合成を行うと
Mobへのダメージを
10%~30%上昇する
プロパティを付与できる。",100,"ITEM_110250", -110251,1,"ヒーローコード:Mobキラー",5,0,102500,50,100,0,0,"ヒーローログに能力合成を行うと
Mobへのダメージを
50%~100%上昇する
プロパティを付与できる。",100,"ITEM_110250", -110260,1,"ヒーローコード:HP最大時強化",3,0,102600,10,30,0,0,"ヒーローログに能力合成を行うと
自分のHPがMAXの時に
与ダメージ+10%~30%の
プロパティを付与できる。",100,"ITEM_110260", -110261,1,"ヒーローコード:HP最大時強化",5,0,102600,10,50,0,0,"ヒーローログに能力合成を行うと
自分のHPがMAXの時に
与ダメージ+10%~50%の
プロパティを付与できる。",100,"ITEM_110260", -110270,1,"ヒーローコード:HP低下時強化",3,0,102700,20,40,0,0,"ヒーローログに能力合成を行うと
自分のHPが25%未満の時に
与ダメージ+20%~40%の
プロパティを付与できる。",100,"ITEM_110270", -110271,1,"ヒーローコード:HP低下時強化",5,0,102700,50,100,0,0,"ヒーローログに能力合成を行うと
自分のHPが25%未満の時に
与ダメージ+50%~100%の
プロパティを付与できる。",100,"ITEM_110270", -110540,1,"ヒーローコード:デバフ無効",3,0,105400,0,0,0,0,"ヒーローログに能力合成を行うと
デバフを無効化する
プロパティを付与できる。",100,"ITEM_110540", -110620,1,"ヒーローコード:魔法防御",3,0,106200,50,100,0,0,"ヒーローログに能力合成を行うと
魔法属性の被ダメージを
VITの50~100%減らす
プロパティを付与できる。",100,"ITEM_110620", -110670,1,"ヒーローコード:詠唱時アーマー",3,0,106700,0,0,0,0,"ヒーローログに能力合成を行うと
魔法詠唱中によろけにくくなる
プロパティを付与できる。",100,"ITEM_110670", -110680,1,"ヒーローコード:衰弱無効",3,0,106800,0,0,0,0,"ヒーローログに能力合成を行うと
衰弱無効の
プロパティを付与できる。",100,"ITEM_110680", -110690,1,"ヒーローコード:状態異常強化",3,0,106900,1,10,0,0,"ヒーローログに能力合成を行うと
状態異常付与率を
1%~10%上昇させる
プロパティを付与できる。",100,"ITEM_110690", -110700,1,"ヒーローコード:全損回避",3,0,107000,0,0,0,0,"ヒーローログに能力合成を行うと
全損時に一度だけ
HP1で持ちこたえる
プロパティを付与できる。",100,"ITEM_110700", -110730,1,"ヒーローコード:弱点攻撃強化",5,0,107300,10,50,0,0,"ヒーローログに能力合成を行うと
弱点を突いた時に
与ダメージ+10%~50%の
プロパティを付与できる。",100,"ITEM_110730", -110790,1,"ヒーローコード:炎上無効",3,0,107900,0,0,0,0,"ヒーローログに能力合成を行うと
炎上無効の
プロパティを付与できる。",100,"ITEM_110790", -110800,1,"ヒーローコード:バフ保護",3,0,108000,0,0,0,0,"ヒーローログに能力合成を行うと
バフキャンセルを無効化する
プロパティを付与できる。",100,"ITEM_110800", -111020,1,"ヒーローコード:魔法攻撃強化",5,0,110200,10,50,0,0,"ヒーローログに能力合成を行うと
魔法攻撃の与ダメージを
10%~50%上昇する
プロパティを付与できる。",100,"ITEM_111020", -111100,1,"ヒーローコード:通常攻撃強化",5,0,111000,50,100,0,0,"ヒーローログに能力合成を行うと
通常攻撃の与ダメージを
50%~100%上昇する
プロパティを付与できる。",100,"ITEM_111100", -111160,1,"ヒーローコード:スキル攻撃強化",5,0,111600,10,50,0,0,"ヒーローログに能力合成を行うと
攻撃スキルの与ダメージを
10%~50%上昇する
プロパティを付与できる。",100,"ITEM_111160", -120020,2,"ウェポンコード:HP上昇",1,0,200200,0,20,0,0,"武器に能力合成を行うと
HP+200~800の
プロパティを付与できる。",100,"ITEM_120020", -120021,2,"ウェポンコード:HP上昇",2,0,200200,20,40,0,0,"武器に能力合成を行うと
HP+800~1400の
プロパティを付与できる。",100,"ITEM_120020", -120022,2,"ウェポンコード:HP上昇",3,0,200200,40,60,0,0,"武器に能力合成を行うと
HP+1400~2000の
プロパティを付与できる。",100,"ITEM_120020", -120023,2,"ウェポンコード:HP上昇",4,0,200200,60,80,0,0,"武器に能力合成を行うと
HP+2000~2600の
プロパティを付与できる。",100,"ITEM_120020", -120024,2,"ウェポンコード:HP上昇",5,0,200200,80,100,0,0,"武器に能力合成を行うと
HP+2600~3200の
プロパティを付与できる。",100,"ITEM_120020", -120030,2,"ウェポンコード:STR上昇",1,0,200300,0,20,0,0,"武器に能力合成を行うと
STR+20~80の
プロパティを付与できる。",100,"ITEM_120030", -120031,2,"ウェポンコード:STR上昇",2,0,200300,20,40,0,0,"武器に能力合成を行うと
STR+80~140の
プロパティを付与できる。",100,"ITEM_120030", -120032,2,"ウェポンコード:STR上昇",3,0,200300,40,60,0,0,"武器に能力合成を行うと
STR+140~200の
プロパティを付与できる。",100,"ITEM_120030", -120033,2,"ウェポンコード:STR上昇",4,0,200300,60,80,0,0,"武器に能力合成を行うと
STR+200~260の
プロパティを付与できる。",100,"ITEM_120030", -120034,2,"ウェポンコード:STR上昇",5,0,200300,80,100,0,0,"武器に能力合成を行うと
STR+260~320の
プロパティを付与できる。",100,"ITEM_120030", -120040,2,"ウェポンコード:VIT上昇",1,0,200400,0,20,0,0,"武器に能力合成を行うと
VIT+20~80の
プロパティを付与できる。",100,"ITEM_120040", -120041,2,"ウェポンコード:VIT上昇",2,0,200400,20,40,0,0,"武器に能力合成を行うと
VIT+80~140の
プロパティを付与できる。",100,"ITEM_120040", -120042,2,"ウェポンコード:VIT上昇",3,0,200400,40,60,0,0,"武器に能力合成を行うと
VIT+140~200の
プロパティを付与できる。",100,"ITEM_120040", -120043,2,"ウェポンコード:VIT上昇",4,0,200400,60,80,0,0,"武器に能力合成を行うと
VIT+200~260の
プロパティを付与できる。",100,"ITEM_120040", -120044,2,"ウェポンコード:VIT上昇",5,0,200400,80,100,0,0,"武器に能力合成を行うと
VIT+260~320の
プロパティを付与できる。",100,"ITEM_120040", -120050,2,"ウェポンコード:INT上昇",1,0,200500,0,20,0,0,"武器に能力合成を行うと
INT+20~80の
プロパティを付与できる。",100,"ITEM_120050", -120051,2,"ウェポンコード:INT上昇",2,0,200500,20,40,0,0,"武器に能力合成を行うと
INT+80~140の
プロパティを付与できる。",100,"ITEM_120050", -120052,2,"ウェポンコード:INT上昇",3,0,200500,40,60,0,0,"武器に能力合成を行うと
INT+140~200の
プロパティを付与できる。",100,"ITEM_120050", -120053,2,"ウェポンコード:INT上昇",4,0,200500,60,80,0,0,"武器に能力合成を行うと
INT+200~260の
プロパティを付与できる。",100,"ITEM_120050", -120054,2,"ウェポンコード:INT上昇",5,0,200500,80,100,0,0,"武器に能力合成を行うと
INT+260~320の
プロパティを付与できる。",100,"ITEM_120050", -120240,2,"ウェポンコード:ボスキラー",3,0,202400,10,30,0,0,"武器に能力合成を行うと
ボスへのダメージを
10%~30%上昇する
プロパティを付与できる。",100,"ITEM_120241", -120241,2,"ウェポンコード:ボスキラー",5,0,202400,50,100,0,0,"武器に能力合成を行うと
ボスへのダメージを
50%~100%上昇する
プロパティを付与できる。",100,"ITEM_120241", -120250,2,"ウェポンコード:Mobキラー",3,0,202500,10,30,0,0,"武器に能力合成を行うと
Mobへのダメージを
10%~30%上昇する
プロパティを付与できる。",100,"ITEM_120250", -120251,2,"ウェポンコード:Mobキラー",5,0,202500,50,100,0,0,"武器に能力合成を行うと
Mobへのダメージを
50%~100%上昇する
プロパティを付与できる。",100,"ITEM_120250", -120260,2,"ウェポンコード:HP最大時強化",3,0,202600,10,30,0,0,"武器に能力合成を行うと
自分のHPがMAXの時に
与ダメージ+10%~30%の
プロパティを付与できる。",100,"ITEM_120260", -120261,2,"ウェポンコード:HP最大時強化",5,0,202600,50,100,0,0,"武器に能力合成を行うと
自分のHPがMAXの時に
与ダメージ+50%~100%の
プロパティを付与できる。",100,"ITEM_120260", -120262,2,"ウェポンコード:HP最大時強化",5,0,202600,80,100,0,0,"武器に能力合成を行うと
自分のHPがMAXの時に
与ダメージ+80%~100%の
プロパティを付与できる。",100,"ITEM_120262", -120270,2,"ウェポンコード:HP低下時強化",3,0,202700,20,40,0,0,"武器に能力合成を行うと
自分のHPが25%未満の時に
与ダメージ+20%~40%の
プロパティを付与できる。",100,"ITEM_120270", -120271,2,"ウェポンコード:HP低下時強化",5,0,202700,80,100,0,0,"武器に能力合成を行うと
自分のHPが25%未満の時に
与ダメージ+80%~100%の
プロパティを付与できる。",100,"ITEM_120270", -120300,2,"ウェポンコード:火属性",3,0,203000,0,0,0,0,"武器に能力合成を行うと
攻撃が火属性になる
プロパティを付与できる。",100,"ITEM_120300", -120310,2,"ウェポンコード:水属性",3,0,203100,0,0,0,0,"武器に能力合成を行うと
攻撃が水属性になる
プロパティを付与できる。",100,"ITEM_120310", -120320,2,"ウェポンコード:風属性",3,0,203200,0,0,0,0,"武器に能力合成を行うと
攻撃が風属性になる
プロパティを付与できる。",100,"ITEM_120320", -120330,2,"ウェポンコード:土属性",3,0,203300,0,0,0,0,"武器に能力合成を行うと
攻撃が土属性になる
プロパティを付与できる。",100,"ITEM_120330", -120340,2,"ウェポンコード:聖属性",3,0,203400,0,0,0,0,"武器に能力合成を行うと
攻撃が聖属性になる
プロパティを付与できる。",100,"ITEM_120340", -120350,2,"ウェポンコード:闇属性",3,0,203500,0,0,0,0,"武器に能力合成を行うと
攻撃が闇属性になる
プロパティを付与できる。",100,"ITEM_120350", -120360,2,"ウェポンコード:毒",3,0,203600,1,10,0,0,"武器に能力合成を行うと
攻撃時に1%~10%の確率で
相手を毒状態にする
プロパティを付与できる。",100,"ITEM_120360", -120370,2,"ウェポンコード:麻痺",3,0,203700,1,10,0,0,"武器に能力合成を行うと
攻撃時に1%~10%の確率で
相手を麻痺状態にする
プロパティを付与できる。",100,"ITEM_120370", -120380,2,"ウェポンコード:混乱",3,0,203800,1,10,0,0,"武器に能力合成を行うと
攻撃時に1%~10%の確率で
相手を混乱状態にする
プロパティを付与できる。",100,"ITEM_120380", -120390,2,"ウェポンコード:封印",3,0,203900,1,10,0,0,"武器に能力合成を行うと
攻撃時に1%~10%の確率で
相手を封印状態にする
プロパティを付与できる。",100,"ITEM_120390", -120400,2,"ウェポンコード:CT短縮",3,0,204000,5,5,0,0,"武器に能力合成を行うと
攻撃スキルのクールタイムを
5%短縮する
プロパティを付与できる。",100,"ITEM_120400", -120410,2,"ウェポンコード:コンボ強化",3,0,204100,100,100,0,0,"武器に能力合成を行うと
コンボによるダメージ上昇率を
アップする
プロパティを付与できる。",100,"ITEM_120410", -120420,2,"ウェポンコード:ドレイン",3,0,204200,5,5,0,0,"武器に能力合成を行うと
攻撃時に与ダメージの5%
自分のHPを回復する
プロパティを付与できる。",100,"ITEM_120420", -120430,2,"ウェポンコード:VIT無視",3,0,204300,20,50,0,0,"武器に能力合成を行うと
敵のVITを
20%~50%無視する
プロパティを付与できる。",100,"ITEM_120430", -120440,2,"ウェポンコード:物理無属性",3,0,204400,0,0,0,0,"武器に能力合成を行うと
武器の物理属性を無しにする
プロパティを付与できる。",100,"ITEM_120440", -120450,2,"ウェポンコード:STRデバフ",3,0,204500,1,10,0,0,"武器に能力合成を行うと
攻撃時に1%~10%の確率で
敵のSTRを25%ダウンさせる
プロパティを付与できる。",100,"ITEM_120450", -120460,2,"ウェポンコード:VITデバフ",3,0,204600,1,10,0,0,"武器に能力合成を行うと
攻撃時に1%~10%の確率で
敵のVITを25%ダウンさせる
プロパティを付与できる。",100,"ITEM_120460", -120550,2,"ウェポンコード:追加攻撃",3,0,205500,10,30,0,0,"武器に能力合成を行うと
攻撃時に10%~30%の確率で
追加の攻撃を発生させる
プロパティを付与できる。",100,"ITEM_120550", -120551,2,"ウェポンコード:追加攻撃",5,0,205500,30,60,0,0,"武器に能力合成を行うと
攻撃時に30%~60%の確率で
追加の攻撃を発生させる
プロパティを付与できる。",100,"ITEM_120550", -120560,2,"ウェポンコード:アクセラ強化",3,0,205600,0,0,0,0,"武器に能力合成を行うと
アクセラレーションによる
ダメージ上昇率をアップする
プロパティを付与できる。",100,"ITEM_120560", -120570,2,"ウェポンコード:速度低下付与",3,0,205700,1,10,0,0,"武器に能力合成を行うと
攻撃時に1%~10%の確率で
敵の移動速度を低下させる
プロパティを付与できる。",100,"ITEM_120570", -120710,2,"ウェポンコード:属性残存",3,0,207100,0,0,0,0,"武器に能力合成を行うと
付与された魔法属性を
武器に残存させる
プロパティを付与できる。",100,"ITEM_120710", -120730,2,"ウェポンコード:弱点攻撃強化",5,0,207300,50,100,0,0,"武器に能力合成を行うと
弱点を突いた時に
与ダメージ+50%~100%の
プロパティを付与できる。",100,"ITEM_120730", -120731,2,"ウェポンコード:弱点攻撃強化",5,0,207300,80,100,0,0,"武器に能力合成を行うと
弱点を突いた時に
与ダメージ+80%~100%の
プロパティを付与できる。",100,"ITEM_120731", -120740,2,"ウェポンコード:バリア破壊",3,0,207400,0,0,0,0,"武器に能力合成を行うと
その武器での攻撃時に
バリアを破壊する
プロパティを付与できる。",100,"ITEM_120740", -120990,2,"ウェポンコード:斬属性攻撃強化",3,0,209900,10,30,0,0,"武器に能力合成を行うと
斬属性攻撃の与ダメージを
10%~30%上昇する
プロパティを付与できる。",100,"ITEM_120990", -121000,2,"ウェポンコード:打属性攻撃強化",3,0,210000,10,30,0,0,"武器に能力合成を行うと
打属性攻撃の与ダメージを
10%~30%上昇する
プロパティを付与できる。",100,"ITEM_121000", -121010,2,"ウェポンコード:突属性攻撃強化",3,0,210100,10,30,0,0,"武器に能力合成を行うと
突属性攻撃の与ダメージを
10%~30%上昇する
プロパティを付与できる。",100,"ITEM_121010", -121020,2,"ウェポンコード:魔法攻撃強化",3,0,210200,10,50,0,0,"武器に能力合成を行うと
魔法攻撃の与ダメージを
10%~50%上昇する
プロパティを付与できる。",100,"ITEM_121020", -121100,2,"ウェポンコード:通常攻撃強化",3,0,211000,50,100,0,0,"武器に能力合成を行うと
通常攻撃の与ダメージを
50%~100%上昇する
プロパティを付与できる。",100,"ITEM_121100", -121140,2,"ウェポンコード:PF貫通",3,0,211400,10,50,0,0,"武器に能力合成を行うと
パニッシャーフィールドを
10%~50%貫通する
プロパティを付与できる。",100,"ITEM_121140", -121160,2,"ウェポンコード:スキル攻撃強化",5,0,211600,50,100,0,0,"武器に能力合成を行うと
攻撃スキルの与ダメージを
50%~100%上昇する
プロパティを付与できる。",100,"ITEM_121160", -130020,3,"アクセサリコード:HP上昇",1,0,300200,0,20,0,0,"防具に能力合成を行うと
HP+200~800の
プロパティを付与できる。",100,"ITEM_130020", -130021,3,"アクセサリコード:HP上昇",2,0,300200,20,40,0,0,"防具に能力合成を行うと
HP+800~1400の
プロパティを付与できる。",100,"ITEM_130020", -130022,3,"アクセサリコード:HP上昇",3,0,300200,40,60,0,0,"防具に能力合成を行うと
HP+1400~2000の
プロパティを付与できる。",100,"ITEM_130020", -130023,3,"アクセサリコード:HP上昇",4,0,300200,60,80,0,0,"防具に能力合成を行うと
HP+2000~2600の
プロパティを付与できる。",100,"ITEM_130020", -130024,3,"アクセサリコード:HP上昇",5,0,300200,80,100,0,0,"防具に能力合成を行うと
HP+2600~3200の
プロパティを付与できる。",100,"ITEM_130020", -130030,3,"アクセサリコード:STR上昇",1,0,300300,0,20,0,0,"防具に能力合成を行うと
STR+20~80の
プロパティを付与できる。",100,"ITEM_130030", -130031,3,"アクセサリコード:STR上昇",2,0,300300,20,40,0,0,"防具に能力合成を行うと
STR+80~140の
プロパティを付与できる。",100,"ITEM_130030", -130032,3,"アクセサリコード:STR上昇",3,0,300300,40,60,0,0,"防具に能力合成を行うと
STR+140~200の
プロパティを付与できる。",100,"ITEM_130030", -130033,3,"アクセサリコード:STR上昇",4,0,300300,60,80,0,0,"防具に能力合成を行うと
STR+200~260の
プロパティを付与できる。",100,"ITEM_130030", -130034,3,"アクセサリコード:STR上昇",5,0,300300,80,100,0,0,"防具に能力合成を行うと
STR+260~320の
プロパティを付与できる。",100,"ITEM_130030", -130040,3,"アクセサリコード:VIT上昇",1,0,300400,0,20,0,0,"防具に能力合成を行うと
VIT+20~80の
プロパティを付与できる。",100,"ITEM_130040", -130041,3,"アクセサリコード:VIT上昇",2,0,300400,20,40,0,0,"防具に能力合成を行うと
VIT+80~140の
プロパティを付与できる。",100,"ITEM_130040", -130042,3,"アクセサリコード:VIT上昇",3,0,300400,40,60,0,0,"防具に能力合成を行うと
VIT+140~200の
プロパティを付与できる。",100,"ITEM_130040", -130043,3,"アクセサリコード:VIT上昇",4,0,300400,60,80,0,0,"防具に能力合成を行うと
VIT+200~260の
プロパティを付与できる。",100,"ITEM_130040", -130044,3,"アクセサリコード:VIT上昇",5,0,300400,80,100,0,0,"防具に能力合成を行うと
VIT+260~320の
プロパティを付与できる。",100,"ITEM_130040", -130050,3,"アクセサリコード:INT上昇",1,0,300500,0,20,0,0,"防具に能力合成を行うと
INT+20~80の
プロパティを付与できる。",100,"ITEM_130050", -130051,3,"アクセサリコード:INT上昇",2,0,300500,20,40,0,0,"防具に能力合成を行うと
INT+80~140の
プロパティを付与できる。",100,"ITEM_130050", -130052,3,"アクセサリコード:INT上昇",3,0,300500,40,60,0,0,"防具に能力合成を行うと
INT+140~200の
プロパティを付与できる。",100,"ITEM_130050", -130053,3,"アクセサリコード:INT上昇",4,0,300500,60,80,0,0,"防具に能力合成を行うと
INT+200~260の
プロパティを付与できる。",100,"ITEM_130050", -130054,3,"アクセサリコード:INT上昇",5,0,300500,80,100,0,0,"防具に能力合成を行うと
INT+260~320の
プロパティを付与できる。",100,"ITEM_130050", -130060,3,"アクセサリコード:斬耐性",3,0,300600,1,20,0,0,"防具に能力合成を行うと
斬属性の被ダメージを
1%~20%減少する
プロパティを付与できる。",100,"ITEM_130060", -130070,3,"アクセサリコード:打耐性",3,0,300700,1,20,0,0,"防具に能力合成を行うと
打属性の被ダメージを
1%~20%減少する
プロパティを付与できる。",100,"ITEM_130070", -130080,3,"アクセサリコード:突耐性",3,0,300800,1,20,0,0,"防具に能力合成を行うと
突属性の被ダメージを
1%~20%減少する
プロパティを付与できる。",100,"ITEM_130080", -130090,3,"アクセサリコード:火耐性",3,0,300900,20,50,0,0,"防具に能力合成を行うと
火属性の被ダメージを
20%~50%減少する
プロパティを付与できる。",100,"ITEM_130090", -130100,3,"アクセサリコード:水耐性",3,0,301000,20,50,0,0,"防具に能力合成を行うと
水属性の被ダメージを
20%~50%減少する
プロパティを付与できる。",100,"ITEM_130100", -130110,3,"アクセサリコード:風耐性",3,0,301100,20,50,0,0,"防具に能力合成を行うと
風属性の被ダメージを
20%~50%減少する
プロパティを付与できる。",100,"ITEM_130110", -130120,3,"アクセサリコード:土耐性",3,0,301200,20,50,0,0,"防具に能力合成を行うと
土属性の被ダメージを
20%~50%減少する
プロパティを付与できる。",100,"ITEM_130120", -130130,3,"アクセサリコード:聖耐性",3,0,301300,20,50,0,0,"防具に能力合成を行うと
聖属性の被ダメージを
20%~50%減少する
プロパティを付与できる。",100,"ITEM_130130", -130140,3,"アクセサリコード:闇耐性",3,0,301400,20,50,0,0,"防具に能力合成を行うと
闇属性の被ダメージを
20%~50%減少する
プロパティを付与できる。",100,"ITEM_130140", -130150,3,"アクセサリコード:毒無効",3,0,301500,0,0,0,0,"防具に能力合成を行うと
毒無効の
プロパティを付与できる。",100,"ITEM_130150", -130160,3,"アクセサリコード:混乱無効",3,0,301600,0,0,0,0,"防具に能力合成を行うと
混乱無効の
プロパティを付与できる。",100,"ITEM_130160", -130170,3,"アクセサリコード:封印無効",3,0,301700,0,0,0,0,"防具に能力合成を行うと
封印無効の
プロパティを付与できる。",100,"ITEM_130170", -130180,3,"アクセサリコード:地震無効",3,0,301800,0,0,0,0,"防具に能力合成を行うと
地震無効の
プロパティを付与できる。",100,"ITEM_130180", -130190,3,"アクセサリコード:Col増加",3,0,301900,10,50,0,0,"防具に能力合成を行うと
クエストでの獲得Colを
10%~50%増加させる
プロパティを付与できる。",100,"ITEM_130190", -130200,3,"アクセサリコード:EXP増加",3,0,302000,5,25,0,0,"防具に能力合成を行うと
クエストでの獲得EXPを
5%~25%増加する
プロパティを付与できる。",100,"ITEM_130200", -130420,3,"アクセサリコード:ドレイン",3,0,304200,5,5,0,0,"防具に能力合成を行うと
攻撃時に与ダメージの5%
自分のHPを回復する
プロパティを付与できる。",100,"ITEM_130211", -130510,3,"アクセサリコード:CT短縮",3,0,305100,5,5,0,0,"防具に能力合成を行うと
補助スキルのクールタイムを
5%短縮する
プロパティを付与できる。",100,"ITEM_130510", -130520,3,"アクセサリコード:状態異常ガード",3,0,305200,0,0,0,0,"防具に能力合成を行うと
ガード中は
状態異常にならなくなる
プロパティを付与できる。",100,"ITEM_130520", -130530,3,"アクセサリコード:自動回復",3,0,305300,5,5,0,0,"防具に能力合成を行うと
10秒毎にHPを5%回復する
プロパティを付与できる。",100,"ITEM_130530", -130531,3,"アクセサリコード:自動回復",5,0,305300,10,50,0,0,"防具に能力合成を行うと
10秒毎にHPを
10~50%回復する
プロパティを付与できる。",100,"ITEM_130530", -130540,3,"アクセサリコード:デバフ無効",3,0,305400,0,0,0,0,"防具に能力合成を行うと
デバフを無効化する
プロパティを付与できる。",100,"ITEM_130540", -130660,3,"アクセサリコード:Col吸引",3,0,306600,0,0,0,0,"防具に能力合成を行うと
ドロップしたColを吸引する
プロパティを付与できる。",100,"ITEM_130660", -130680,3,"アクセサリコード:衰弱無効",3,0,306800,0,0,0,0,"防具に能力合成を行うと
衰弱無効の
プロパティを付与できる。",100,"ITEM_130680", -130700,3,"アクセサリコード:全損回避",3,0,307000,0,0,0,0,"防具に能力合成を行うと
全損時に一度だけ
HP1で持ちこたえる
プロパティを付与できる。",100,"ITEM_130700", -130720,3,"アクセサリコード:耐性記憶",3,0,307200,20,50,0,0,"防具に能力合成を行うと
最後に被弾した魔法属性に
20~50%の耐性を得る
プロパティを付与できる。",100,"ITEM_130720", -130790,3,"アクセサリコード:炎上無効",3,0,307900,0,0,0,0,"防具に能力合成を行うと
炎上無効の
プロパティを付与できる。",100,"ITEM_130790", -130800,3,"アクセサリコード:バフ保護",3,0,308000,0,0,0,0,"防具に能力合成を行うと
バフキャンセルを無効化する
プロパティを付与できる。",100,"ITEM_130800", -131130,3,"アクセサリコード:属性保護",3,0,311300,0,0,0,0,"防具に能力合成を行うと
自パーティ以外からの
魔法属性付与を無視する
プロパティを付与できる。",100,"ITEM_131130", -140000,4,"プロパティアペンド",3,0,1,0,0,0,0,"プロパティを拡張するキット。
そのリソースが保持可能な
プロパティの数をひとつ増やす。",100,"ITEM_140000", -150010,5,"コードイレイサー:1",3,1,1,0,0,0,0,"リソースに記録された
1枠目のプロパティを
消去することができるアイテム。",100,"ITEM_150010", -150020,5,"コードイレイサー:2",3,2,1,0,0,0,0,"リソースに記録された
2枠目のプロパティを
消去することができるアイテム。",100,"ITEM_150020", -150030,5,"コードイレイサー:3",3,3,1,0,0,0,0,"リソースに記録された
3枠目のプロパティを
消去することができるアイテム。",100,"ITEM_150030", -150040,5,"コードイレイサー:4",3,4,1,0,0,0,0,"リソースに記録された
4枠目のプロパティを
消去することができるアイテム。",100,"ITEM_150040", -160000,6,"スキルアペンド",3,0,1,0,0,0,0,"ヒーローログのスキルスロットを
拡張するキット。セットできる
スキルの数をひとつ増やす。",100,"ITEM_160000", -170000,7,"EXPチップ",1,2000,1,0,0,0,0,"高密度な情報チップ。
リソースに合成することで
わずかに成長させられる。",100,"ITEM_170000", -170001,7,"EXPチップ《キロ》",2,7000,1,0,0,0,0,"高密度な情報チップ。
リソースに合成することで
少し成長させられる。",100,"ITEM_170001", -170002,7,"EXPチップ《メガ》",3,12000,1,0,0,0,0,"高密度な情報チップ。
リソースに合成することで
成長させられる。",100,"ITEM_170002", -170003,7,"EXPチップ《ギガ》",3,17000,1,0,0,0,0,"超高密度な情報チップ。
リソースに合成することで
かなり成長させられる。",100,"ITEM_170003", -170004,7,"EXPチップ《テラ》",3,22000,1,0,0,0,0,"超高密度な情報チップ。
リソースに合成することで
非常に成長させられる。",100,"ITEM_170004", -170005,7,"EXPメモリ",4,40000,1,0,0,0,0,"超高密度な情報ストレージ。
リソースに合成することで
きわめて多く成長させられる。",100,"ITEM_170005", -170006,7,"EXPメモリ《ミディ》",4,80000,1,0,0,0,0,"超高密度な情報ストレージ。
リソースに合成することで
絶大に成長させられる。",100,"ITEM_170006", -170007,7,"EXPメモリ《グラン》",4,120000,1,0,0,0,0,"超高密度な情報ストレージ。
リソースに合成することで
極大に成長させられる。",100,"ITEM_170007", -170100,7,"BET:アスナ",5,7000,1,0,0,0,0,"自分にも他人にも正しくあろうと
する彼女に、一夜だけでも夢を賭
けたいと願った。カウンター越し
なら同じ目線に立てる気がして。",10000,"ITEM_170100", -170101,7,"BET:リーファ",5,7000,1,0,0,0,0,"誰であろうと公平に優しさを注ぐ
彼女に、一夜だけでも夢を賭けた
いと願った。平等な想いなんてな
いと、気付いてほしくて。",10000,"ITEM_170101", -170102,7,"BET:シノン",5,7000,1,0,0,0,0,"痛みを乗り越えて強くあろうとす
る彼女に、一夜だけでも夢を賭け
たいと願った。チップを掴む指先
のたおやかさにはっとしたから。",10000,"ITEM_170102", -170103,7,"BET:シリカ",5,7000,1,0,0,0,0,"どんな思いも分かち合ってくれる
彼女に、一夜だけでも夢を賭けた
いと願った。きょとんとした顔を
とびきりの笑顔に変えたくて。",10000,"ITEM_170103", -170104,7,"BET:アリス",5,7000,1,0,0,0,0,"世界のため前を見据える彼女に、
一夜だけでも夢を賭けたいと願っ
た。いつかの別れの日が来ても、
同じ世界を見た証が欲しくて。",10000,"ITEM_170104", -180000,8,"ゴールド",1,0,1,0,0,0,0,"ゲーム内通貨に換金できる金塊。
売却することでわずかにColを
入手することができる。",5000,"ITEM_180000", -180001,8,"ゴールド《キロ》",2,0,1,0,0,0,0,"ゲーム内通貨に換金できる金塊。
売却することで少しColを
入手することができる。",10000,"ITEM_180001", -180002,8,"ゴールド《メガ》",3,0,1,0,0,0,0,"ゲーム内通貨に換金できる金塊。
売却することでColを
入手することができる。",15000,"ITEM_180002", -180003,8,"ゴールド《ギガ》",3,0,1,0,0,0,0,"ゲーム内通貨に換金できる金塊。
売却することで大量のColを
入手することができる。",20000,"ITEM_180003", -180004,8,"ゴールド《テラ》",3,0,1,0,0,0,0,"ゲーム内通貨に換金できる金塊。
売却することで非常に大量の
Colを入手することができる。",30000,"ITEM_180004", -180005,8,"プラチナ",4,0,1,0,0,0,0,"ゲーム内通貨に換金できる
希少金属。売却することで
きわめて大量のColを入手する
ことができる。",60000,"ITEM_180005", -180006,8,"プラチナ《ミディ》",4,0,1,0,0,0,0,"ゲーム内通貨に換金できる
希少金属。売却することで膨大な
Colを入手することができる。",100000,"ITEM_180006", -180007,8,"プラチナ《グラン》",4,0,1,0,0,0,0,"ゲーム内通貨に換金できる
希少金属。売却することで莫大な
Colを入手することができる。",150000,"ITEM_180007", -190000,9,"ヒーローレベルアペンド《1》",4,1,1,0,0,0,0,"ヒーローログのレベル上限を
1拡張するキット。
※拡張上限を超えて
拡張することは出来ない",10000,"ITEM_190000", -190001,9,"ヒーローレベルアペンド《2》",5,2,1,0,0,0,0,"ヒーローログのレベル上限を
2拡張するキット。
※拡張上限を超えて
拡張することは出来ない",20000,"ITEM_190001", -200000,10,"ウェポンレベルアペンド《1》",4,1,1,0,0,0,0,"武器のレベル上限を
1拡張するキット。
※拡張上限を超えて
拡張することは出来ない",10000,"ITEM_200000", -200001,10,"ウェポンレベルアペンド《2》",5,2,1,0,0,0,0,"武器のレベル上限を
2拡張するキット。
※拡張上限を超えて
拡張することは出来ない",20000,"ITEM_200001", -210000,11,"アクセサリレベルアペンド《1》",4,1,1,0,0,0,0,"防具のレベル上限を
1拡張するキット。
※拡張上限を超えて
拡張することは出来ない",10000,"ITEM_210000", -210001,11,"アクセサリレベルアペンド《2》",5,2,1,0,0,0,0,"防具のレベル上限を
2拡張するキット。
※拡張上限を超えて
拡張することは出来ない",20000,"ITEM_210001", -220000,12,"ホワイトデーギフト",5,100,1,0,0,0,0,"ホワイトデーのお返しの
プレゼント。
リソースの覚醒レベルを
1上昇させる。",100,"ITEM_220000", -220001,12,"イベントギフト",5,100,1,0,0,0,0,"共に楽しみを分かち合った
思い出のプレゼント。
リソースの覚醒レベルを
1上昇させる。",100,"ITEM_220001", -220002,12,"サンキューギフト",5,100,1,0,0,0,0,"日頃の感謝が込められた
特別なプレゼント。
リソースの覚醒レベルを
1上昇させる。",100,"ITEM_220002", diff --git a/titles/sao/data/QuestScene.csv b/titles/sao/data/QuestScene.csv deleted file mode 100644 index 3e6b402..0000000 --- a/titles/sao/data/QuestScene.csv +++ /dev/null @@ -1,243 +0,0 @@ -QuestSceneId,SortNo,Name,MapData,UnitData,DemoMap,BgmBasic,BgmBoss,Tutorial,CharaCommentId0,CharaCommentId1,CharaCommentId2,CharaCommentId3,CharaCommentId4,ColRate,LimitDefault,LimitTimeDec,LimitCharaDec,LimitResurrection,MissionTableSubId,MissionEnemyLvLimit,RewardTableSubId,PlayerTraceTableSubId,SuccessPlayerExp,FailedPlayerExp,GreedSpawnWaitCount,HoneySpawnWaitCount,MenuDisplayEnemySetId,StageFilepath,RarityUpChanceRate,PairMobHpRate,PairMobAtkRate,PairLeaderHpRate,PairLeaderAtkRate,PairBossHpRate,PairBossAtkRate,PairExpRate,TrioMobHpRate,TrioMobAtkRate,TrioLeaderHpRate,TrioLeaderAtkRate,TrioBossHpRate,TrioBossAtkRate,TrioExpRate,SingleRewardVp,PairRewardVp,TrioRewardVp, -1001,14,"sao_wf_day_009","sao_wf_day_009_mapData","sao_wf_day_009_unitData","sao_wf_day","BGM_F_WEST_DAYTIME_BASIC","BGM_F_WEST_BOSS",True,0,0,0,0,0,1,500,1,0,200,99,4,100001,1000,100,50,100,0,1001,"Stage_WF_day",0,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -1002,15,"sao_wf_day_010","sao_wf_day_010_mapData","sao_wf_day_010_unitData","sao_wf_day","BGM_F_WEST_DAYTIME_BASIC","BGM_F_WEST_BOSS",False,10004,10021,0,20000,20001,0.6,450,0,0,200,1,5,110002,1000,100,50,100,0,1002,"Stage_WF_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -1003,35,"sao_fm_74_003","sao_fm_74_003_mapData","sao_fm_74_003_unitData","sao_fm_47","BGM_F_LABYRINTH_BASIC","BGM_F_LABYRINTH_BOSS",False,10005,10024,0,20000,20001,1,450,0,0,200,1,6,110002,1000,100,50,100,0,1003,"Stage_FM",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -1004,16,"sao_wf_day_011","sao_wf_day_011_mapData","sao_wf_day_011_unitData","sao_dk_day","BGM_F_DANAK_DAYTIME_BASIC","BGM_F_DANAK_BOSS",False,10009,10021,10002,20000,20001,1.1,400,0,0,200,1,7,110002,1000,100,50,100,0,1004,"Stage_DNK_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -1005,24,"sao_wf_dusk_008","sao_wf_dusk_008_mapData","sao_wf_dusk_008_unitData","sao_wf_dusk","BGM_F_WEST_EVENING_BASIC","BGM_F_WEST_BOSS",False,10004,10022,10002,20000,20001,1.3,400,0,0,200,1,9,110002,1000,100,50,100,0,1005,"Stage_WF_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -1006,50,"alo_st_day_008","alo_st_day_008_mapData","alo_st_day_008_unitData","alo_st_day","BGM_F_WORLDTREE_DAYTIME_BASIC","BGM_F_WORLDTREE_BOSS",False,10006,10021,10002,20000,20001,1.5,360,0,0,180,1,10,110002,1000,100,50,100,0,1006,"Stage_ST_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -1007,32,"sao_wf_night_007","sao_wf_night_007_mapData","sao_wf_night_007_unitData","sao_wf_night","BGM_F_WEST_NIGHT_BASIC","BGM_F_WEST_BOSS",False,10004,10023,10002,20000,20001,1.7,330,0,0,165,1,11,110002,1000,100,50,100,0,1007,"Stage_WF_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -1008,76,"ggo_at_dusk_007","ggo_at_dusk_007_mapData","ggo_at_dusk_007_unitData","ggo_at_dusk","BGM_F_RUIN_EVENING_BASIC","BGM_F_RUIN_BOSS",False,10008,10022,10002,20000,20001,1.98,330,0,0,165,1,12,110002,1000,100,50,100,0,1008,"Stage_GGO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -1009,82,"ggo_at_night_005","ggo_at_night_005_mapData","ggo_at_night_005_unitData","ggo_at_night","BGM_F_RUIN_NIGHT_BASIC","BGM_F_RUIN_BOSS",False,10008,10023,10002,20000,20001,2.178,360,0,0,180,1,13,110002,1000,100,50,100,0,1009,"Stage_GGO_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -1010,51,"alo_st_day_009","alo_st_day_009_mapData","alo_st_day_009_unitData","alo_st_day","BGM_F_WORLDTREE_DAYTIME_BASIC","BGM_F_WORLDTREE_BOSS",False,10006,10021,10002,20000,20001,2.64,360,0,0,180,1,15,110002,1000,100,50,100,0,1010,"Stage_ST_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -1011,33,"sao_wf_night_008","sao_wf_night_008_mapData","sao_wf_night_008_unitData","sao_dk_night","BGM_F_DANAK_NIGHT_BASIC","BGM_F_DANAK_BOSS",False,10023,10002,0,20000,20001,2.838,330,0,0,165,1,16,110011,1000,100,50,100,0,1011,"Stage_DNK_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -1012,66,"alo_ip_basic_007","alo_ip_basic_007_mapData","alo_ip_basic_007_unitData","alo_id_srymheimr","BGM_F_SRYMHEIMR_BASIC","BGM_F_SRYMHEIMR_BOSS",False,10007,10024,10002,20000,20001,3.036,360,0,0,180,1,17,110011,1000,100,50,100,0,1012,"Stage_SF",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -1013,37,"sao_fm_74_005","sao_fm_74_005_mapData","sao_fm_74_005_unitData","sao_fm_47","BGM_F_LABYRINTH_BASIC","BGM_F_LABYRINTH_BOSS",False,10005,10024,10002,20000,20001,3.3,300,0,0,150,1,18,110011,1000,100,50,100,0,1013,"Stage_FM",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -1014,67,"alo_ip_basic_008","alo_ip_basic_008_mapData","alo_ip_basic_008_unitData","alo_id_srymheimr","BGM_F_SRYMHEIMR_BASIC","BGM_F_SRYMHEIMR_BOSS",False,10007,10024,10002,20000,20001,3.498,340,0,0,170,1,19,110011,1000,100,50,100,0,1014,"Stage_SF",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -1015,58,"alo_st_night_007","alo_st_night_007_mapData","alo_st_night_007_unitData","alo_st_night","BGM_F_WORLDTREE_NIGHT_BASIC","BGM_F_WORLDTREE_BOSS",False,10006,10023,10003,20000,20001,4.356,340,0,0,170,1,23,110011,1000,100,50,100,0,1015,"Stage_ST_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -1016,25,"sao_wf_dusk_009","sao_wf_dusk_009_mapData","sao_wf_dusk_009_unitData","sao_dk_dusk","BGM_F_DANAK_EVENING_BASIC","BGM_F_DANAK_BOSS",False,10022,10003,0,20000,20001,4.62,320,0,0,160,1,24,110011,1000,100,50,100,0,1016,"Stage_DNK_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -1017,68,"alo_ip_basic_009","alo_ip_basic_009_mapData","alo_ip_basic_009_unitData","alo_id_srymheimr","BGM_F_SRYMHEIMR_BASIC","BGM_F_SRYMHEIMR_BOSS",False,10007,10024,10003,20000,20001,4.818,340,0,0,170,1,25,110011,1000,100,50,100,0,1017,"Stage_SF",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -1018,38,"sao_fm_74_006","sao_fm_74_006_mapData","sao_fm_74_006_unitData","sao_fm_47","BGM_F_LABYRINTH_BASIC","BGM_F_LABYRINTH_BOSS",False,10005,10024,10003,20000,20001,5.478,300,0,0,150,1,28,110011,1000,100,50,100,0,1018,"Stage_FM",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -1019,2001,"sao_ff_day_001","sao_ff_day_001_mapData","sao_ff_day_001_unitData","sao_ff_day","BGM_F_FLORIA_DAYTIME_BASIC","BGM_F_FLORIA_BOSS",False,10010,10021,10003,20000,20001,5.5,380,0,0,190,1,26,110019,1000,100,50,100,0,1019,"Stage_FLO_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -1020,2002,"sao_wf_day_014","sao_wf_day_014_mapData","sao_wf_day_014_unitData","sao_wf_day","BGM_F_WEST_DAYTIME_BASIC","BGM_F_WEST_BOSS",False,10004,10021,10003,20000,20001,5.6,340,0,0,170,1,27,110020,1000,100,50,100,0,1020,"Stage_WF_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -1021,2003,"ggo_at_night_012","ggo_at_night_012_mapData","ggo_at_night_012_unitData","ggo_at_night","BGM_F_RUIN_NIGHT_BASIC","BGM_F_RUIN_BOSS",False,10008,10031,10003,20000,20001,5.7,360,0,0,180,1,28,110020,1000,100,50,100,0,1021,"Stage_GGO_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -1022,2004,"alo_st_day_011","alo_st_day_011_mapData","alo_st_day_011_unitData","alo_st_day","BGM_F_WORLDTREE_DAYTIME_BASIC","BGM_F_WORLDTREE_BOSS",False,10006,10026,10003,20000,20001,5.8,350,0,0,175,1,29,110020,1000,100,50,100,0,1022,"Stage_ST_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -1023,2005,"alo_ip_basic_012","alo_ip_basic_012_mapData","alo_ip_basic_012_unitData","alo_id_srymheimr","BGM_F_SRYMHEIMR_BASIC","BGM_F_SRYMHEIMR_BOSS",False,10007,10029,10003,20000,20001,5.9,350,0,0,175,1,30,110020,1000,100,50,100,0,1023,"Stage_SF",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -1024,2006,"sao_wf_night_011","sao_wf_night_011_mapData","sao_wf_night_011_unitData","sao_dk_night","BGM_F_DANAK_NIGHT_BASIC","BGM_F_DANAK_BOSS",False,10023,10003,0,20000,20001,6,330,0,0,165,1,31,110024,1000,100,50,100,0,1024,"Stage_DNK_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -1025,2007,"sao_ff_dusk_001","sao_ff_dusk_001_mapData","sao_ff_dusk_001_unitData","sao_ff_dusk","BGM_F_FLORIA_EVENING_BASIC","BGM_F_FLORIA_BOSS",False,10010,10022,10003,20000,20001,6.1,340,0,0,170,1,32,110024,1000,100,50,100,0,1025,"Stage_FLO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -1026,2008,"ggo_gt_day_001","ggo_gt_day_001_mapData","ggo_gt_day_001_unitData","ggo_gt_day","BGM_F_GLOCKEN_DAYTIME_BASIC","BGM_F_GLOCKEN_BOSS",False,10017,10003,0,20000,20001,6.2,340,0,0,170,1,33,110024,1000,100,50,100,0,1026,"Stage_GLO_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -1027,2009,"sao_fm_74_013","sao_fm_74_013_mapData","sao_fm_74_013_unitData","sao_fm_47","BGM_F_LABYRINTH_BASIC","BGM_F_LABYRINTH_BOSS",False,10005,10024,10003,20000,20001,6.3,330,0,0,165,1,34,110024,1000,100,50,100,0,1027,"Stage_FM",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -1028,2010,"alo_st_night_003","alo_st_night_003_mapData","alo_st_night_003_unitData","alo_st_night","BGM_F_WORLDTREE_NIGHT_BASIC","BGM_F_WORLDTREE_BOSS",False,10006,10023,10003,20000,20001,6.4,330,0,0,165,1,35,110024,1000,100,50,100,0,1028,"Stage_ST_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -1029,2011,"sao_wf_day_015","sao_wf_day_015_mapData","sao_wf_day_015_unitData","sao_wf_day","BGM_F_WEST_DAYTIME_BASIC","BGM_F_WEST_BOSS",False,10004,10021,10003,20000,20001,6.5,320,0,0,160,1,36,110024,1000,100,50,100,0,1029,"Stage_WF_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -1030,2012,"ggo_at_dusk_013","ggo_at_dusk_013_mapData","ggo_at_dusk_013_unitData","ggo_at_dusk","BGM_F_RUIN_EVENING_BASIC","BGM_F_RUIN_BOSS",False,10008,10022,10003,20000,20001,6.6,340,0,0,170,1,37,110024,1000,100,50,100,0,1030,"Stage_GGO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -1031,2013,"sao_ff_day_002","sao_ff_day_002_mapData","sao_ff_day_002_unitData","sao_ff_day","BGM_F_FLORIA_DAYTIME_BASIC","BGM_F_FLORIA_BOSS",False,10010,10021,10003,20000,20001,6.7,350,0,0,175,1,38,110024,1000,100,50,100,0,1031,"Stage_FLO_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -1032,2014,"ggo_gt_night_001","ggo_gt_night_001_mapData","ggo_gt_night_001_unitData","ggo_gt_night","BGM_F_GLOCKEN_NIGHT_BASIC","BGM_F_GLOCKEN_BOSS",False,10017,10003,0,20000,20001,6.8,360,0,0,180,1,39,110024,1000,100,50,100,0,1032,"Stage_GLO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -2001,43,"sao_fm_74_011","sao_fm_74_011_mapData","sao_fm_74_011_unitData","sao_fm_47","BGM_F_LABYRINTH_BASIC","BGM_F_LABYRINTH_BOSS",False,10005,10024,0,20000,20001,6.996,300,0,0,150,1,35,190100,1000,100,50,100,0,2001,"Stage_FM",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -2002,59,"alo_st_night_008","alo_st_night_008_mapData","alo_st_night_008_unitData","alo_st_night","BGM_F_WORLDTREE_NIGHT_BASIC","BGM_F_WORLDTREE_BOSS",False,10006,10023,10026,20000,20001,5.478,340,0,0,170,1,35,190200,1000,100,50,100,0,2002,"Stage_ST_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -2003,52,"alo_st_day_010","alo_st_day_010_mapData","alo_st_day_010_unitData","alo_st_day","BGM_F_WORLDTREE_DAYTIME_BASIC","BGM_F_WORLDTREE_BOSS",False,10006,10021,10026,20000,20001,5.478,340,0,0,170,1,35,190300,1000,100,50,100,0,2003,"Stage_ST_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -2004,77,"ggo_at_dusk_008","ggo_at_dusk_008_mapData","ggo_at_dusk_008_unitData","ggo_at_dusk","BGM_F_RUIN_EVENING_BASIC","BGM_F_RUIN_BOSS",False,10008,10022,10031,20000,20001,5.478,340,0,0,170,1,35,190400,1000,100,50,100,0,2004,"Stage_GGO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -2005,26,"sao_wf_dusk_010","sao_wf_dusk_010_mapData","sao_wf_dusk_010_unitData","sao_wf_dusk","BGM_F_WEST_EVENING_BASIC","BGM_F_WEST_BOSS",False,10004,10022,10025,20000,20001,5.478,320,0,0,160,1,35,190500,1000,100,50,100,0,2005,"Stage_WF_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -2006,17,"sao_wf_day_012","sao_wf_day_012_mapData","sao_wf_day_012_unitData","sao_wf_day","BGM_F_WEST_DAYTIME_BASIC","BGM_F_WEST_BOSS",False,10004,10021,0,20000,20001,5.478,300,0,0,150,1,35,190600,1000,100,50,100,0,2006,"Stage_WF_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -2007,69,"alo_ip_basic_010","alo_ip_basic_010_mapData","alo_ip_basic_010_unitData","alo_id_srymheimr","BGM_F_SRYMHEIMR_BASIC","BGM_F_SRYMHEIMR_BOSS",False,10007,10024,10029,20000,20001,5.478,340,0,0,170,1,35,190700,1000,100,50,100,0,2007,"Stage_SF",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -2008,44,"sao_fm_74_012","sao_fm_74_012_mapData","sao_fm_74_012_unitData","sao_fm_47","BGM_F_LABYRINTH_BASIC","BGM_F_LABYRINTH_BOSS",False,10005,10024,0,20000,20001,5.478,300,0,0,150,1,35,190800,1000,100,50,100,0,2008,"Stage_FM",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -2009,3001,"sao_wf_day_018","sao_wf_day_018_mapData","sao_wf_day_018_unitData","sao_wf_day","BGM_F_WEST_DAYTIME_BASIC","BGM_F_WEST_BOSS",False,10019,10021,0,20000,20001,7.38,320,0,0,160,1,40,190300,1000,100,50,100,0,2009,"Stage_WF_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -2010,3002,"sao_wf_dusk_013","sao_wf_dusk_013_mapData","sao_wf_dusk_013_unitData","sao_dk_dusk","BGM_F_DANAK_EVENING_BASIC","BGM_F_DANAK_BOSS",False,10022,0,0,20000,20001,7.38,330,0,0,165,1,40,191000,1000,100,50,100,0,2010,"Stage_DNK_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -2011,3003,"sao_wf_day_023","sao_wf_day_023_mapData","sao_wf_day_023_unitData","sao_wf_day","BGM_F_WEST_DAYTIME_BASIC","BGM_F_WEST_BOSS",False,10021,10025,0,20000,20001,7.38,320,0,0,160,1,40,191100,1000,100,50,100,0,2011,"Stage_WF_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -2012,3004,"sao_wm_day_002","sao_wm_day_002_mapData","sao_wm_day_002_unitData","sao_wm_day","BGM_F_WEST_MOUNTAIN_BASIC","BGM_F_WEST_MOUNTAIN_BOSS",False,10013,10029,0,20000,20001,7.38,340,0,0,170,1,40,191200,1000,100,50,100,0,2012,"Stage_WM_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -2013,3005,"alo_st_day_015","alo_st_day_015_mapData","alo_st_day_015_unitData","alo_st_day","BGM_F_WORLDTREE_DAYTIME_BASIC","BGM_F_WORLDTREE_BOSS",False,10006,10021,10026,20000,20001,7.38,320,0,0,160,1,40,191300,1000,100,50,100,0,2013,"Stage_ST_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3001,7,"sao_wf_day_002","sao_wf_day_002_mapData","sao_wf_day_002_unitData","sao_dk_day","BGM_F_DANAK_DAYTIME_BASIC","BGM_F_DANAK_BOSS",False,10009,10021,0,20000,20001,5,460,0,0,230,1,8,200001,2000,100,50,100,0,3001,"Stage_DNK_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3002,18,"sao_wf_dusk_002","sao_wf_dusk_002_mapData","sao_wf_dusk_002_unitData","sao_wf_dusk","BGM_F_WEST_EVENING_BASIC","BGM_F_WEST_BOSS",False,10004,10022,0,20000,20001,6,460,0,0,230,1,9,200002,2000,100,50,100,0,3002,"Stage_WF_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3003,27,"sao_wf_night_002","sao_wf_night_002_mapData","sao_wf_night_002_unitData","sao_dk_night","BGM_F_DANAK_NIGHT_BASIC","BGM_F_DANAK_BOSS",False,10023,0,0,20000,20001,7,430,0,0,215,1,10,200003,2000,100,50,100,0,3003,"Stage_DNK_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3004,45,"alo_st_day_003","alo_st_day_003_mapData","alo_st_day_003_unitData","alo_st_day","BGM_F_WORLDTREE_DAYTIME_BASIC","BGM_F_WORLDTREE_BOSS",False,10006,10021,10026,20000,20001,8,460,0,0,230,1,11,200004,2000,100,50,100,0,3004,"Stage_ST_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3005,34,"sao_fm_74_002","sao_fm_74_002_mapData","sao_fm_74_002_unitData","sao_fm_47","BGM_F_LABYRINTH_BASIC","BGM_F_LABYRINTH_BOSS",False,10005,10024,0,20000,20001,9,460,0,0,230,1,13,200005,2000,100,50,100,0,3005,"Stage_FM",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3006,70,"ggo_at_dusk_001","ggo_at_dusk_001_mapData","ggo_at_dusk_001_unitData","ggo_at_dusk","BGM_F_RUIN_EVENING_BASIC","BGM_F_RUIN_BOSS",False,10008,10022,10031,20000,20001,10,460,0,0,230,1,14,200006,2000,100,50,100,0,3006,"Stage_GGO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3007,60,"alo_ip_basic_001","alo_ip_basic_001_mapData","alo_ip_basic_001_unitData","alo_id_srymheimr","BGM_F_SRYMHEIMR_BASIC","BGM_F_SRYMHEIMR_BOSS",False,10007,10024,10029,20000,20001,11,460,0,0,230,1,15,200007,2000,100,50,100,0,3007,"Stage_SF",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3008,8,"sao_wf_day_003","sao_wf_day_003_mapData","sao_wf_day_003_unitData","sao_wf_day","BGM_F_WEST_DAYTIME_BASIC","BGM_F_WEST_BOSS",False,10004,10021,10025,20000,20001,12,430,0,0,215,1,16,200008,2000,100,50,100,0,3008,"Stage_WF_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3009,53,"alo_st_night_002","alo_st_night_002_mapData","alo_st_night_002_unitData","alo_st_night","BGM_F_WORLDTREE_NIGHT_BASIC","BGM_F_WORLDTREE_BOSS",False,10006,10023,10026,20000,20001,13,460,0,0,230,1,17,200009,2000,100,50,100,0,3009,"Stage_ST_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -10001,991,"sao_wf_day_013","sao_wf_day_013_mapData","sao_wf_day_013_unitData","sao_wf_day","BGM_F_WEST_DAYTIME_BASIC","BGM_F_WEST_BOSS",False,10004,10021,0,20000,20001,19,400,0,0,200,1,20,200010,2000,100,50,100,0,3010,"Stage_WF_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3011,46,"alo_st_day_004","alo_st_day_004_mapData","alo_st_day_004_unitData","alo_st_day","BGM_F_WORLDTREE_DAYTIME_BASIC","BGM_F_WORLDTREE_BOSS",False,10006,10021,10026,20000,20001,20,400,0,0,200,1,21,200011,2000,100,50,100,0,3011,"Stage_ST_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3012,71,"ggo_at_dusk_002","ggo_at_dusk_002_mapData","ggo_at_dusk_002_unitData","ggo_at_dusk","BGM_F_RUIN_EVENING_BASIC","BGM_F_RUIN_BOSS",False,10008,10022,10031,20000,20001,21,380,0,0,190,1,22,200012,2000,100,50,100,0,3012,"Stage_GGO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3013,36,"sao_fm_74_004","sao_fm_74_004_mapData","sao_fm_74_004_unitData","sao_fm_47","BGM_F_LABYRINTH_BASIC","BGM_F_LABYRINTH_BOSS",False,10005,10024,0,20000,20001,22,380,0,0,190,1,23,200013,2000,100,50,100,0,3013,"Stage_FM",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3014,20,"sao_wf_dusk_004","sao_wf_dusk_004_mapData","sao_wf_dusk_004_unitData","sao_dk_dusk","BGM_F_DANAK_EVENING_BASIC","BGM_F_DANAK_BOSS",False,10022,0,0,20000,20001,23,360,0,0,180,1,24,200014,2000,100,50,100,0,3014,"Stage_DNK_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3015,61,"alo_ip_basic_002","alo_ip_basic_002_mapData","alo_ip_basic_002_unitData","alo_id_srymheimr","BGM_F_SRYMHEIMR_BASIC","BGM_F_SRYMHEIMR_BOSS",False,10007,10024,10029,20000,20001,24,380,0,0,180,1,26,200015,2000,100,50,100,0,3015,"Stage_SF",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3016,78,"ggo_at_night_001","ggo_at_night_001_mapData","ggo_at_night_001_unitData","ggo_at_night","BGM_F_RUIN_NIGHT_BASIC","BGM_F_RUIN_BOSS",False,10008,10023,10031,20000,20001,25,380,0,0,190,1,27,200016,2000,100,50,100,0,3016,"Stage_GGO_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3017,9,"sao_wf_day_004","sao_wf_day_004_mapData","sao_wf_day_004_unitData","sao_wf_day","BGM_F_WEST_DAYTIME_BASIC","BGM_F_WEST_BOSS",False,10004,10021,10025,20000,20001,26,360,0,0,180,1,28,200017,2000,100,50,100,0,3017,"Stage_WF_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3018,28,"sao_wf_night_003","sao_wf_night_003_mapData","sao_wf_night_003_unitData","sao_wf_night","BGM_F_WEST_NIGHT_BASIC","BGM_F_WEST_BOSS",False,10004,10023,10025,20000,20001,27,380,0,0,190,1,29,200018,2000,100,50,100,0,3018,"Stage_WF_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3019,72,"ggo_at_dusk_003","ggo_at_dusk_003_mapData","ggo_at_dusk_003_unitData","ggo_at_dusk","BGM_F_RUIN_EVENING_BASIC","BGM_F_RUIN_BOSS",False,10008,10022,10031,20000,20001,28,380,0,0,190,1,30,200019,2000,100,50,100,0,3019,"Stage_GGO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -10002,992,"sao_wf_dusk_011","sao_wf_dusk_011_mapData","sao_wf_dusk_011_unitData","sao_wf_dusk","BGM_F_WEST_EVENING_BASIC","BGM_F_WEST_BOSS",False,10004,10022,10025,0,0,34,360,0,0,180,1,33,200020,2000,100,50,100,0,3020,"Stage_WF_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3021,47,"alo_st_day_005","alo_st_day_005_mapData","alo_st_day_005_unitData","alo_st_day","BGM_F_WORLDTREE_DAYTIME_BASIC","BGM_F_WORLDTREE_BOSS",False,10006,10021,10026,20000,20001,35,360,0,0,180,1,34,200021,2000,100,50,100,0,3021,"Stage_ST_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3022,10,"sao_wf_day_005","sao_wf_day_005_mapData","sao_wf_day_005_unitData","sao_wf_day","BGM_F_WEST_DAYTIME_BASIC","BGM_F_WEST_BOSS",False,10004,10021,10025,20000,20001,36,330,0,0,165,1,35,200022,2000,100,50,100,0,3022,"Stage_WF_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3023,62,"alo_ip_basic_003","alo_ip_basic_003_mapData","alo_ip_basic_003_unitData","alo_id_srymheimr","BGM_F_SRYMHEIMR_BASIC","BGM_F_SRYMHEIMR_BOSS",False,10007,10024,10029,20000,20001,37,360,0,0,180,1,36,200023,2000,100,50,100,0,3023,"Stage_SF",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3024,73,"ggo_at_dusk_004","ggo_at_dusk_004_mapData","ggo_at_dusk_004_unitData","ggo_at_dusk","BGM_F_RUIN_EVENING_BASIC","BGM_F_RUIN_BOSS",False,10008,10022,10031,20000,20001,38,330,0,0,165,1,37,200024,2000,100,50,100,0,3024,"Stage_GGO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3025,29,"sao_wf_night_004","sao_wf_night_004_mapData","sao_wf_night_004_unitData","sao_dk_night","BGM_F_DANAK_NIGHT_BASIC","BGM_F_DANAK_BOSS",False,10023,0,0,20000,20001,39,360,0,0,180,1,39,200025,2000,100,50,100,0,3025,"Stage_DNK_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3026,39,"sao_fm_74_007","sao_fm_74_007_mapData","sao_fm_74_007_unitData","sao_fm_47","BGM_F_LABYRINTH_BASIC","BGM_F_LABYRINTH_BOSS",False,10005,10024,0,20000,20001,40,300,0,0,150,1,40,200026,2010,100,50,100,0,3026,"Stage_FM",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3027,21,"sao_wf_dusk_005","sao_wf_dusk_005_mapData","sao_wf_dusk_005_unitData","sao_wf_dusk","BGM_F_WEST_EVENING_BASIC","BGM_F_WEST_BOSS",False,10004,10022,10025,20000,20001,41,300,0,0,150,1,41,200027,2010,100,50,100,0,3027,"Stage_WF_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3028,55,"alo_st_night_004","alo_st_night_004_mapData","alo_st_night_004_unitData","alo_st_night","BGM_F_WORLDTREE_NIGHT_BASIC","BGM_F_WORLDTREE_BOSS",False,10006,10023,10026,20000,20001,42,360,0,0,180,1,42,200028,2010,100,50,100,0,3028,"Stage_ST_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3029,11,"sao_wf_day_006","sao_wf_day_006_mapData","sao_wf_day_006_unitData","sao_dk_day","BGM_F_DANAK_DAYTIME_BASIC","BGM_F_DANAK_BOSS",False,10009,10021,0,20000,20001,43,330,0,0,165,1,43,200029,2010,100,50,100,0,3029,"Stage_DNK_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -10004,994,"ggo_at_dusk_009","ggo_at_dusk_009_mapData","ggo_at_dusk_009_unitData","ggo_at_dusk","BGM_F_RUIN_EVENING_BASIC","BGM_F_RUIN_BOSS",False,10008,10022,10031,0,0,49,360,0,0,180,1,46,200030,2010,100,50,100,0,3030,"Stage_GGO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3031,56,"alo_st_night_005","alo_st_night_005_mapData","alo_st_night_005_unitData","alo_st_night","BGM_F_WORLDTREE_NIGHT_BASIC","BGM_F_WORLDTREE_BOSS",False,10006,10023,10026,20000,20001,50,360,0,0,180,1,47,200031,2010,100,50,100,0,3031,"Stage_ST_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3032,79,"ggo_at_night_002","ggo_at_night_002_mapData","ggo_at_night_002_unitData","ggo_at_night","BGM_F_RUIN_NIGHT_BASIC","BGM_F_RUIN_BOSS",False,10008,10023,10031,20000,20001,51,330,0,0,165,1,48,200032,2010,100,50,100,0,3032,"Stage_GGO_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3033,22,"sao_wf_dusk_006","sao_wf_dusk_006_mapData","sao_wf_dusk_006_unitData","sao_wf_dusk","BGM_F_WEST_EVENING_BASIC","BGM_F_WEST_BOSS",False,10004,10022,10025,20000,20001,52,330,0,0,165,1,49,200033,2010,100,50,100,0,3033,"Stage_WF_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3034,63,"alo_ip_basic_004","alo_ip_basic_004_mapData","alo_ip_basic_004_unitData","alo_id_srymheimr","BGM_F_SRYMHEIMR_BASIC","BGM_F_SRYMHEIMR_BOSS",False,10007,10024,10029,20000,20001,53,360,0,0,180,1,50,200034,2010,100,50,100,0,3034,"Stage_SF",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3035,74,"ggo_at_dusk_005","ggo_at_dusk_005_mapData","ggo_at_dusk_005_unitData","ggo_at_dusk","BGM_F_RUIN_EVENING_BASIC","BGM_F_RUIN_BOSS",False,10008,10022,10031,20000,20001,54,360,0,0,180,1,52,200035,2010,100,50,100,0,3035,"Stage_GGO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3036,30,"sao_wf_night_005","sao_wf_night_005_mapData","sao_wf_night_005_unitData","sao_dk_night","BGM_F_DANAK_NIGHT_BASIC","BGM_F_DANAK_BOSS",False,10023,0,0,20000,20001,55,330,0,0,165,1,54,200036,2010,100,50,100,0,3036,"Stage_DNK_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3037,12,"sao_wf_day_007","sao_wf_day_007_mapData","sao_wf_day_007_unitData","sao_wf_day","BGM_F_WEST_DAYTIME_BASIC","BGM_F_WEST_BOSS",False,10004,10021,0,20000,20001,56,330,0,0,165,1,55,200037,2010,100,50,100,0,3037,"Stage_WF_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3038,41,"sao_fm_74_009","sao_fm_74_009_mapData","sao_fm_74_009_unitData","sao_fm_47","BGM_F_LABYRINTH_BASIC","BGM_F_LABYRINTH_BOSS",False,10005,10024,0,20000,20001,57,300,0,0,150,1,56,200038,2010,100,50,100,0,3038,"Stage_FM",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3039,48,"alo_st_day_006","alo_st_day_006_mapData","alo_st_day_006_unitData","alo_st_day","BGM_F_WORLDTREE_DAYTIME_BASIC","BGM_F_WORLDTREE_BOSS",False,10006,10021,10026,20000,20001,58,360,0,0,180,1,57,200039,2010,100,50,100,0,3039,"Stage_ST_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -10003,993,"sao_wf_night_009","sao_wf_night_009_mapData","sao_wf_night_009_unitData","sao_wf_night","BGM_F_WEST_NIGHT_BASIC","BGM_F_WEST_BOSS",False,10004,10023,10025,0,0,64,360,0,0,180,1,60,200040,2010,100,50,100,0,3040,"Stage_WF_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3041,42,"sao_fm_74_010","sao_fm_74_010_mapData","sao_fm_74_010_unitData","sao_fm_47","BGM_F_LABYRINTH_BASIC","BGM_F_LABYRINTH_BOSS",False,10005,10024,0,20000,20001,65,330,0,0,165,1,61,200041,2010,100,50,100,0,3041,"Stage_FM",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3042,13,"sao_wf_day_008","sao_wf_day_008_mapData","sao_wf_day_008_unitData","sao_dk_day","BGM_F_DANAK_DAYTIME_BASIC","BGM_F_DANAK_BOSS",False,10009,10021,0,20000,20001,66,330,0,0,165,1,62,200042,2010,100,50,100,0,3042,"Stage_DNK_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3043,64,"alo_ip_basic_005","alo_ip_basic_005_mapData","alo_ip_basic_005_unitData","alo_id_srymheimr","BGM_F_SRYMHEIMR_BASIC","BGM_F_SRYMHEIMR_BOSS",False,10007,10024,10029,20000,20001,67,360,0,0,180,1,63,200043,2010,100,50,100,0,3043,"Stage_SF",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3044,31,"sao_wf_night_006","sao_wf_night_006_mapData","sao_wf_night_006_unitData","sao_wf_night","BGM_F_WEST_NIGHT_BASIC","BGM_F_WEST_BOSS",False,10004,10023,0,20000,20001,68,360,0,0,180,1,64,200044,2010,100,50,100,0,3044,"Stage_WF_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3045,40,"sao_fm_74_008","sao_fm_74_008_mapData","sao_fm_74_008_unitData","sao_fm_47","BGM_F_LABYRINTH_BASIC","BGM_F_LABYRINTH_BOSS",False,10005,10024,0,20000,20001,69,360,0,0,180,1,66,200045,2010,100,50,100,0,3045,"Stage_FM",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3046,65,"alo_ip_basic_006","alo_ip_basic_006_mapData","alo_ip_basic_006_unitData","alo_id_srymheimr","BGM_F_SRYMHEIMR_BASIC","BGM_F_SRYMHEIMR_BOSS",False,10007,10024,10029,20000,20001,70,360,0,0,180,1,67,200046,2010,100,50,100,0,3046,"Stage_SF",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3047,75,"ggo_at_dusk_006","ggo_at_dusk_006_mapData","ggo_at_dusk_006_unitData","ggo_at_dusk","BGM_F_RUIN_EVENING_BASIC","BGM_F_RUIN_BOSS",False,10008,10022,10031,20000,20001,71,330,0,0,165,1,68,200047,2010,100,50,100,0,3047,"Stage_GGO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3048,57,"alo_st_night_006","alo_st_night_006_mapData","alo_st_night_006_unitData","alo_st_night","BGM_F_WORLDTREE_NIGHT_BASIC","BGM_F_WORLDTREE_BOSS",False,10006,10023,10026,20000,20001,72,360,0,0,180,1,69,200048,2010,100,50,100,0,3048,"Stage_ST_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3049,23,"sao_wf_dusk_007","sao_wf_dusk_007_mapData","sao_wf_dusk_007_unitData","sao_wf_dusk","BGM_F_WEST_EVENING_BASIC","BGM_F_WEST_BOSS",False,10004,10022,10025,20000,20001,73,330,0,0,165,1,70,200049,2010,100,50,100,0,3049,"Stage_WF_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -10005,995,"alo_ip_basic_011","alo_ip_basic_011_mapData","alo_ip_basic_011_unitData","alo_id_srymheimr","BGM_F_SRYMHEIMR_BASIC","BGM_F_SRYMHEIMR_BOSS",False,10007,10024,10029,0,0,79,460,0,0,230,1,73,200050,2010,100,50,100,0,3050,"Stage_SF",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3051,4001,"ggo_at_dusk_011","ggo_at_dusk_011_mapData","ggo_at_dusk_011_unitData","ggo_at_dusk","BGM_F_RUIN_EVENING_BASIC","BGM_F_RUIN_BOSS",False,10008,10022,10031,20000,20001,80,330,0,0,165,1,71,200051,2020,100,50,100,0,3051,"Stage_GGO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3052,4002,"sao_wf_dusk_014","sao_wf_dusk_014_mapData","sao_wf_dusk_014_unitData","sao_wf_dusk","BGM_F_WEST_EVENING_BASIC","BGM_F_WEST_BOSS",False,10004,10022,10025,20000,20001,81,310,0,0,155,1,72,200052,2020,100,50,100,0,3052,"Stage_WF_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3053,4003,"sao_ff_day_005","sao_ff_day_005_mapData","sao_ff_day_005_unitData","sao_ff_day","BGM_F_FLORIA_DAYTIME_BASIC","BGM_F_FLORIA_BOSS",False,10010,10021,0,20000,20001,82,360,0,0,180,1,73,200053,2020,100,50,100,0,3053,"Stage_FLO_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3054,4004,"ggo_gt_day_003","ggo_gt_day_003_mapData","ggo_gt_day_003_unitData","ggo_gt_day","BGM_F_GLOCKEN_DAYTIME_BASIC","BGM_F_GLOCKEN_BOSS",False,10017,0,0,20000,20001,83,340,0,0,170,1,74,200054,2020,100,50,100,0,3054,"Stage_GLO_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3055,4005,"sao_wf_day_022","sao_wf_day_022_mapData","sao_wf_day_022_unitData","sao_dk_day","BGM_F_DANAK_DAYTIME_BASIC","BGM_F_DANAK_BOSS",False,10009,10021,0,20000,20001,84,320,0,0,160,1,77,200055,2020,100,50,100,0,3055,"Stage_DNK_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3056,4006,"alo_ip_basic_013","alo_ip_basic_013_mapData","alo_ip_basic_013_unitData","alo_id_srymheimr","BGM_F_SRYMHEIMR_BASIC","BGM_F_SRYMHEIMR_BOSS",False,10007,10024,10029,20000,20001,85,340,0,0,170,1,78,200056,2020,100,50,100,0,3056,"Stage_SF",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3057,4007,"sao_wf_night_012","sao_wf_night_012_mapData","sao_wf_night_012_unitData","sao_wf_night","BGM_F_WEST_NIGHT_BASIC","BGM_F_WEST_BOSS",False,10004,10023,0,20000,20001,86,330,0,0,165,1,79,200057,2020,100,50,100,0,3057,"Stage_WF_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3058,4008,"alo_st_day_013","alo_st_day_013_mapData","alo_st_day_013_unitData","alo_st_day","BGM_F_WORLDTREE_DAYTIME_BASIC","BGM_F_WORLDTREE_BOSS",False,10006,10021,10026,20000,20001,87,360,0,0,180,1,80,200058,2020,100,50,100,0,3058,"Stage_ST_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3059,4009,"sao_ff_dusk_004","sao_ff_dusk_004_mapData","sao_ff_dusk_004_unitData","sao_ff_dusk","BGM_F_FLORIA_EVENING_BASIC","BGM_F_FLORIA_BOSS",False,10010,10022,0,20000,20001,88,320,0,0,160,1,81,200059,2020,100,50,100,0,3059,"Stage_FLO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3060,4010,"sao_fm_74_014","sao_fm_74_014_mapData","sao_fm_74_014_unitData","sao_fm_47","BGM_F_LABYRINTH_BASIC","BGM_F_LABYRINTH_BOSS",False,10005,10024,0,0,0,89,360,0,0,180,1,85,200060,2020,100,50,100,0,3060,"Stage_FM",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3061,4011,"sao_wf_day_026","sao_wf_day_026_mapData","sao_wf_day_026_unitData","sao_wf_day","BGM_F_WEST_DAYTIME_BASIC","BGM_F_WEST_BOSS",False,10004,10021,0,20000,20001,90,320,0,0,160,1,86,200061,2020,100,50,100,0,3061,"Stage_WF_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3062,4012,"ggo_at_night_007","ggo_at_night_007_mapData","ggo_at_night_007_unitData","ggo_at_night","BGM_F_RUIN_NIGHT_BASIC","BGM_F_RUIN_BOSS",False,10008,10023,10031,20000,20001,91,360,0,0,180,1,87,200062,2020,100,50,100,0,3062,"Stage_GGO_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3063,4013,"alo_st_night_010","alo_st_night_010_mapData","alo_st_night_010_unitData","alo_st_night","BGM_F_WORLDTREE_NIGHT_BASIC","BGM_F_WORLDTREE_BOSS",False,10006,10023,10026,20000,20001,92,320,0,0,160,1,88,200063,2020,100,50,100,0,3063,"Stage_ST_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3064,4014,"sao_wf_day_021","sao_wf_day_021_mapData","sao_wf_day_021_unitData","sao_wf_day","BGM_F_WEST_DAYTIME_BASIC","BGM_F_WEST_BOSS",False,10004,10021,0,20000,20001,93,320,0,0,160,1,89,200064,2020,100,50,100,0,3064,"Stage_WF_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3065,4015,"ggo_gt_night_005","ggo_gt_night_005_mapData","ggo_gt_night_005_unitData","ggo_gt_night","BGM_F_GLOCKEN_NIGHT_BASIC","BGM_F_GLOCKEN_BOSS",False,10017,0,0,20000,20001,94,360,0,0,180,1,92,200065,2020,100,50,100,0,3065,"Stage_GLO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3066,4016,"sao_ff_day_006","sao_ff_day_006_mapData","sao_ff_day_006_unitData","sao_ff_day","BGM_F_FLORIA_DAYTIME_BASIC","BGM_F_FLORIA_BOSS",False,10010,10021,0,20000,20001,95,330,0,0,165,1,93,200066,2020,100,50,100,0,3066,"Stage_FLO_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3067,4017,"sao_fm_74_015","sao_fm_74_015_mapData","sao_fm_74_015_unitData","sao_fm_47","BGM_F_LABYRINTH_BASIC","BGM_F_LABYRINTH_BOSS",False,10005,10024,0,20000,20001,96,320,0,0,160,1,94,200067,2020,100,50,100,0,3067,"Stage_FM",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3068,4018,"alo_ip_basic_014","alo_ip_basic_014_mapData","alo_ip_basic_014_unitData","alo_id_srymheimr","BGM_F_SRYMHEIMR_BASIC","BGM_F_SRYMHEIMR_BOSS",False,10007,10024,10029,20000,20001,97,350,0,0,175,1,95,200068,2020,100,50,100,0,3068,"Stage_SF",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3069,4019,"sao_wf_dusk_012","sao_wf_dusk_012_mapData","sao_wf_dusk_012_unitData","sao_wf_dusk","BGM_F_WEST_EVENING_BASIC","BGM_F_WEST_BOSS",False,10004,10022,10025,20000,20001,98,360,0,0,180,1,96,200069,2020,100,50,100,0,3069,"Stage_WF_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3070,4020,"ggo_at_dusk_012","ggo_at_dusk_012_mapData","ggo_at_dusk_012_unitData","ggo_at_dusk","BGM_F_RUIN_EVENING_BASIC","BGM_F_RUIN_BOSS",False,10008,10031,0,0,0,99,380,0,0,190,1,100,200070,2020,100,50,100,0,3070,"Stage_GGO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3071,4021,"sao_ff_dusk_005","sao_ff_dusk_005_mapData","sao_ff_dusk_005_unitData","sao_ff_dusk","BGM_F_FLORIA_EVENING_BASIC","BGM_F_FLORIA_BOSS",False,10010,10022,0,20000,20001,100,330,0,0,165,1,101,200071,2020,100,50,100,0,3071,"Stage_FLO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3072,4022,"sao_fm_74_016","sao_fm_74_016_mapData","sao_fm_74_016_unitData","sao_fm_47","BGM_F_LABYRINTH_BASIC","BGM_F_LABYRINTH_BOSS",False,10005,10024,0,20000,20001,101,380,0,0,190,1,102,200072,2020,100,50,100,0,3072,"Stage_FM",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3073,4023,"sao_wf_day_019","sao_wf_day_019_mapData","sao_wf_day_019_unitData","sao_dk_day","BGM_F_DANAK_DAYTIME_BASIC","BGM_F_DANAK_BOSS",False,10009,10021,0,20000,20001,102,320,0,0,160,1,103,200073,2020,100,50,100,0,3073,"Stage_DNK_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3074,4024,"ggo_at_dusk_010","ggo_at_dusk_010_mapData","ggo_at_dusk_010_unitData","ggo_at_dusk","BGM_F_RUIN_EVENING_BASIC","BGM_F_RUIN_BOSS",False,10008,10031,0,20000,20001,103,360,0,0,180,1,104,200074,2020,100,50,100,0,3074,"Stage_GGO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3075,4025,"alo_ti_day_006","alo_ti_day_006_mapData","alo_ti_day_006_unitData","alo_ti_day","BGM_F_COAST_BASIC","BGM_F_WEST_BOSS",False,20004,10025,0,20000,20001,104,380,0,0,190,1,105,200075,2020,100,50,100,0,3075,"Stage_TI_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3076,4026,"alo_st_night_009","alo_st_night_009_mapData","alo_st_night_009_unitData","alo_st_night","BGM_F_WORLDTREE_NIGHT_BASIC","BGM_F_WORLDTREE_BOSS",False,10006,10023,10026,20000,20001,105,320,0,0,160,1,106,200076,2030,100,50,100,0,3076,"Stage_ST_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3077,4027,"sao_wm_day_001","sao_wm_day_001_mapData","sao_wm_day_001_unitData","sao_wm_day","BGM_F_WEST_MOUNTAIN_BASIC","BGM_F_WEST_MOUNTAIN_BOSS",False,10013,10029,0,20000,20001,106,360,0,0,180,1,107,200077,2030,100,50,100,0,3077,"Stage_WM_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3078,4028,"sao_wf_night_014","sao_wf_night_014_mapData","sao_wf_night_014_unitData","sao_wf_night","BGM_F_WEST_NIGHT_BASIC","BGM_F_WEST_BOSS",False,10004,10023,0,20000,20001,107,380,0,0,190,1,108,200078,2030,100,50,100,0,3078,"Stage_WF_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3079,4029,"sao_wf_dusk_015","sao_wf_dusk_015_mapData","sao_wf_dusk_015_unitData","sao_dk_dusk","BGM_F_DANAK_EVENING_BASIC","BGM_F_DANAK_BOSS",False,10022,0,0,20000,20001,108,320,0,0,160,1,109,200079,2030,100,50,100,0,3079,"Stage_DNK_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3080,4030,"ggo_gt_day_004","ggo_gt_day_004_mapData","ggo_gt_day_004_unitData","ggo_gt_day","BGM_F_GLOCKEN_DAYTIME_BASIC","BGM_F_GLOCKEN_BOSS",False,10017,0,0,0,0,109,400,0,0,200,1,110,200080,2030,100,50,100,0,3080,"Stage_GLO_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3081,4031,"ggo_at_night_004","ggo_at_night_004_mapData","ggo_at_night_004_unitData","ggo_at_night","BGM_F_RUIN_NIGHT_BASIC","BGM_F_RUIN_BOSS",False,10008,10023,10031,20000,20001,110,360,0,0,180,1,110,200081,2030,100,50,100,0,3081,"Stage_GGO_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3082,4032,"sao_wf_day_020","sao_wf_day_020_mapData","sao_wf_day_020_unitData","sao_wf_day","BGM_F_WEST_DAYTIME_BASIC","BGM_F_WEST_BOSS",False,10004,10021,0,20000,20001,111,380,0,0,190,1,110,200082,2030,100,50,100,0,3082,"Stage_WF_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3083,4033,"alo_ti_day_007","alo_ti_day_007_mapData","alo_ti_day_007_unitData","alo_ti_day","BGM_F_COAST_BASIC","BGM_F_WEST_BOSS",False,20004,10025,0,20000,20001,112,360,0,0,180,1,110,200083,2030,100,50,100,0,3083,"Stage_TI_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3084,4034,"ggo_gt_night_002","ggo_gt_night_002_mapData","ggo_gt_night_002_unitData","ggo_gt_night","BGM_F_GLOCKEN_NIGHT_BASIC","BGM_F_GLOCKEN_BOSS",False,10017,0,0,20000,20001,113,360,0,0,180,1,110,200084,2030,100,50,100,0,3084,"Stage_GLO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3085,4035,"ggo_at_dusk_014","ggo_at_dusk_014_mapData","ggo_at_dusk_014_unitData","ggo_at_dusk","BGM_F_RUIN_EVENING_BASIC","BGM_F_RUIN_BOSS",False,10008,10031,0,20000,20001,114,380,0,0,190,1,110,200085,2030,100,50,100,0,3085,"Stage_GGO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3086,4036,"sao_ff_dusk_006","sao_ff_dusk_006_mapData","sao_ff_dusk_006_unitData","sao_ff_dusk","BGM_F_FLORIA_EVENING_BASIC","BGM_F_FLORIA_BOSS",False,10010,10021,0,20000,20001,115,340,0,0,170,1,110,200086,2030,100,50,100,0,3086,"Stage_FLO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3087,4037,"sao_wm_night_001","sao_wm_night_001_mapData","sao_wm_night_001_unitData","sao_wm_night","BGM_F_WEST_MOUNTAIN_BASIC","BGM_F_WEST_MOUNTAIN_BOSS",False,10013,10029,0,20000,20001,116,360,0,0,180,1,110,200087,2030,100,50,100,0,3087,"Stage_WM_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3088,4038,"sao_fm_74_017","sao_fm_74_017_mapData","sao_fm_74_017_unitData","sao_fm_47","BGM_F_LABYRINTH_BASIC","BGM_F_LABYRINTH_BOSS",False,10005,10024,0,20000,20001,117,380,0,0,190,1,110,200088,2030,100,50,100,0,3088,"Stage_FM",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3089,4039,"alo_st_night_011","alo_st_night_011_mapData","alo_st_night_011_unitData","alo_st_night","BGM_F_WORLDTREE_NIGHT_BASIC","BGM_F_WORLDTREE_BOSS",False,10006,10023,10026,20000,20001,118,340,0,0,170,1,110,200089,2030,100,50,100,0,3089,"Stage_ST_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3090,4040,"alo_ip_basic_015","alo_ip_basic_015_mapData","alo_ip_basic_015_unitData","alo_id_srymheimr","BGM_F_SRYMHEIMR_BASIC","BGM_F_SRYMHEIMR_BOSS",False,10007,10024,10029,0,0,119,400,0,0,200,1,110,200090,2030,100,50,100,0,3090,"Stage_SF",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3091,4041,"sao_wf_dusk_016","sao_wf_dusk_016_mapData","sao_wf_dusk_016_unitData","sao_dk_dusk","BGM_F_DANAK_EVENING_BASIC","BGM_F_DANAK_BOSS",False,10022,0,0,20000,20001,120,340,0,0,170,1,110,200091,2030,100,50,100,0,3091,"Stage_DNK_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3092,4042,"ggo_at_night_009","ggo_at_night_009_mapData","ggo_at_night_009_unitData","ggo_at_night","BGM_F_RUIN_NIGHT_BASIC","BGM_F_RUIN_BOSS",False,10008,10023,10031,20000,20001,121,380,0,0,190,1,110,200092,2030,100,50,100,0,3092,"Stage_GGO_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3093,4043,"alo_ti_day_008","alo_ti_day_008_mapData","alo_ti_day_008_unitData","alo_ti_day","BGM_F_COAST_BASIC","BGM_F_WEST_BOSS",False,20004,10025,0,20000,20001,122,360,0,0,180,1,110,200093,2030,100,50,100,0,3093,"Stage_TI_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3094,4044,"alo_st_day_012","alo_st_day_012_mapData","alo_st_day_012_unitData","alo_st_day","BGM_F_WORLDTREE_DAYTIME_BASIC","BGM_F_WORLDTREE_BOSS",False,10006,10021,10026,20000,20001,123,340,0,0,170,1,110,200094,2030,100,50,100,0,3094,"Stage_ST_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3095,4045,"sao_lw_night_001","sao_lw_night_001_mapData","sao_lw_night_001_unitData","sao_lw_night","BGM_F_LOST_FOREST_BASIC","BGM_F_LOST_FOREST_BOSS",False,10014,10029,0,20000,20001,124,400,0,0,200,1,110,200095,2030,100,50,100,0,3095,"Stage_LW_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3096,4046,"sao_ff_dusk_002","sao_ff_dusk_002_mapData","sao_ff_dusk_002_unitData","sao_ff_dusk","BGM_F_FLORIA_EVENING_BASIC","BGM_F_FLORIA_BOSS",False,10010,10022,0,20000,20001,125,340,0,0,170,1,110,200096,2030,100,50,100,0,3096,"Stage_FLO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3097,4047,"sao_wf_day_016","sao_wf_day_016_mapData","sao_wf_day_016_unitData","sao_dk_day","BGM_F_DANAK_DAYTIME_BASIC","BGM_F_DANAK_BOSS",False,10009,10021,0,20000,20001,126,340,0,0,170,1,110,200097,2030,100,50,100,0,3097,"Stage_DNK_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3098,4048,"ggo_gt_day_005","ggo_gt_day_005_mapData","ggo_gt_day_005_unitData","ggo_gt_day","BGM_F_GLOCKEN_DAYTIME_BASIC","BGM_F_GLOCKEN_BOSS",False,10017,0,0,20000,20001,127,380,0,0,190,1,110,200098,2030,100,50,100,0,3098,"Stage_GLO_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3099,4049,"alo_st_night_012","alo_st_night_012_mapData","alo_st_night_012_unitData","alo_st_night","BGM_F_WORLDTREE_NIGHT_BASIC","BGM_F_WORLDTREE_BOSS",False,10006,10023,10026,20000,20001,128,340,0,0,170,1,110,200099,2030,100,50,100,0,3099,"Stage_ST_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -3100,4050,"sao_wm_dawn_001","sao_wm_dawn_001_mapData","sao_wm_dawn_001_unitData","sao_wm_dawn","BGM_F_WEST_MOUNTAIN_BASIC","BGM_F_PUNISHER_BOSS",False,10013,10029,0,0,0,129,400,0,0,200,1,110,200100,2030,100,50,100,0,3100,"Stage_WM_dawn",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4011,6001,"alo_st_day_014","alo_st_day_014_mapData","alo_st_day_014_unitData","alo_st_day","BGM_F_WORLDTREE_DAYTIME_BASIC","BGM_F_WORLDTREE_BOSS",False,10006,10021,10026,20000,20001,20,340,0,0,170,1,110,201001,2030,100,50,100,0,4011,"Stage_ST_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4012,6002,"sao_ff_day_003","sao_ff_day_003_mapData","sao_ff_day_003_unitData","sao_ff_day","BGM_F_FLORIA_DAYTIME_BASIC","BGM_F_FLORIA_BOSS",False,10010,10021,0,20000,20001,22,360,0,0,180,1,110,201002,2030,100,50,100,0,4012,"Stage_FLO_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4013,6003,"ggo_at_dusk_015","ggo_at_dusk_015_mapData","ggo_at_dusk_015_unitData","ggo_at_dusk","BGM_F_RUIN_EVENING_BASIC","BGM_F_RUIN_BOSS",False,10008,10031,0,20000,20001,24,380,0,0,190,1,110,201003,2030,100,50,100,0,4013,"Stage_GGO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4014,6004,"sao_wm_night_002","sao_wm_night_002_mapData","sao_wm_night_002_unitData","sao_wm_night","BGM_F_WEST_MOUNTAIN_BASIC","BGM_F_WEST_MOUNTAIN_BOSS",False,10013,10029,0,20000,20001,26,400,0,0,200,1,120,201004,2030,100,50,100,0,4014,"Stage_WM_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4021,6005,"sao_fm_74_018","sao_fm_74_018_mapData","sao_fm_74_018_unitData","sao_fm_47","BGM_F_LABYRINTH_BASIC","BGM_F_LABYRINTH_BOSS",False,10005,10024,0,20000,20001,20,340,0,0,170,1,110,202001,2030,100,50,100,0,4021,"Stage_FM",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4022,6006,"alo_ip_basic_016","alo_ip_basic_016_mapData","alo_ip_basic_016_unitData","alo_id_srymheimr","BGM_F_SRYMHEIMR_BASIC","BGM_F_SRYMHEIMR_BOSS",False,10007,10024,10029,20000,20001,22,380,0,0,190,1,110,202002,2030,100,50,100,0,4022,"Stage_SF",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4023,6007,"sao_wf_night_015","sao_wf_night_015_mapData","sao_wf_night_015_unitData","sao_wf_night","BGM_F_WEST_NIGHT_BASIC","BGM_F_WEST_BOSS",False,10004,10023,0,20000,20001,24,380,0,0,190,1,110,202003,2030,100,50,100,0,4023,"Stage_WF_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4024,6008,"ggo_gt_day_006","ggo_gt_day_006_mapData","ggo_gt_day_006_unitData","ggo_gt_day","BGM_F_GLOCKEN_DAYTIME_BASIC","BGM_F_GLOCKEN_BOSS",False,10017,0,0,20000,20001,26,400,0,0,200,1,120,202004,2030,100,50,100,0,4024,"Stage_GLO_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4031,6009,"alo_ti_day_009","alo_ti_day_009_mapData","alo_ti_day_009_unitData","alo_ti_day","BGM_F_COAST_BASIC","BGM_F_WEST_BOSS",False,20004,10025,0,20000,20001,20,360,0,0,180,1,110,203001,2030,100,50,100,0,4031,"Stage_TI_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4032,6010,"alo_ti_day_010","alo_ti_day_010_mapData","alo_ti_day_010_unitData","alo_ti_day","BGM_F_COAST_BASIC","BGM_F_WEST_BOSS",False,20004,10025,0,20000,20001,22,360,0,0,180,1,110,203002,2030,100,50,100,0,4032,"Stage_TI_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4033,6011,"alo_ti_day_011","alo_ti_day_011_mapData","alo_ti_day_011_unitData","alo_ti_day","BGM_F_COAST_BASIC","BGM_F_WEST_BOSS",False,20004,10025,0,20000,20001,24,380,0,0,190,1,110,203003,2030,100,50,100,0,4033,"Stage_TI_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4034,6012,"alo_ti_day_012","alo_ti_day_012_mapData","alo_ti_day_012_unitData","alo_ti_day","BGM_F_COAST_BASIC","BGM_F_WEST_BOSS",False,20004,10025,0,20000,20001,26,400,0,0,200,1,120,203004,2030,100,50,100,0,4034,"Stage_TI_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4041,6013,"sao_wf_night_016","sao_wf_night_016_mapData","sao_wf_night_016_unitData","sao_wf_night","BGM_F_WEST_NIGHT_BASIC","BGM_F_WEST_BOSS",False,10004,10023,0,20000,20001,20,360,0,0,180,1,110,204001,2030,100,50,100,0,4041,"Stage_WF_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4042,6014,"sao_ff_day_007","sao_ff_day_007_mapData","sao_ff_day_007_unitData","sao_ff_day","BGM_F_FLORIA_DAYTIME_BASIC","BGM_F_FLORIA_BOSS",False,10010,10021,0,20000,20001,22,360,0,0,180,1,110,204002,2030,100,50,100,0,4042,"Stage_FLO_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4043,6015,"sao_wf_day_028","sao_wf_day_028_mapData","sao_wf_day_028_unitData","sao_wf_day","BGM_F_WEST_DAYTIME_BASIC","BGM_F_WEST_BOSS",False,10004,10021,0,20000,20001,24,380,0,0,190,1,110,204003,2030,100,50,100,0,4043,"Stage_WF_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4044,6016,"alo_st_day_016","alo_st_day_016_mapData","alo_st_day_016_unitData","alo_st_day","BGM_F_WORLDTREE_DAYTIME_BASIC","BGM_F_WORLDTREE_BOSS",False,10006,10021,10026,20000,20001,26,400,0,0,200,1,120,204004,2030,100,50,100,0,4044,"Stage_ST_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4051,6017,"sao_fm_74_019","sao_fm_74_019_mapData","sao_fm_74_019_unitData","sao_fm_47","BGM_F_LABYRINTH_BASIC","BGM_F_LABYRINTH_BOSS",False,10005,10024,0,20000,20001,20,300,0,0,170,1,110,205001,2030,100,50,100,0,4051,"Stage_FM",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4052,6018,"alo_st_night_013","alo_st_night_013_mapData","alo_st_night_013_unitData","alo_st_night","BGM_F_WORLDTREE_NIGHT_BASIC","BGM_F_WORLDTREE_BOSS",False,10006,10023,10026,20000,20001,16.6,320,0,0,170,1,110,205002,2030,100,50,100,0,4052,"Stage_ST_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4053,6019,"sao_ff_day_008","sao_ff_day_008_mapData","sao_ff_day_008_unitData","sao_ff_day","BGM_F_FLORIA_DAYTIME_BASIC","BGM_F_FLORIA_BOSS",False,10010,10021,0,20000,20001,22,330,0,0,180,1,110,205003,2030,100,50,100,0,4053,"Stage_FLO_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4054,6020,"sao_lw_night_002","sao_lw_night_002_mapData","sao_lw_night_002_unitData","sao_lw_night","BGM_F_LOST_FOREST_BASIC","BGM_F_LOST_FOREST_BOSS",False,10014,10029,0,20000,20001,16.2,340,0,0,200,1,110,205004,2030,100,50,100,0,4054,"Stage_LW_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4061,6021,"alo_ip_basic_017","alo_ip_basic_017_mapData","alo_ip_basic_017_unitData","alo_id_srymheimr","BGM_F_SRYMHEIMR_BASIC","BGM_F_SRYMHEIMR_BOSS",False,10007,10024,10029,20000,20001,20,300,0,0,170,1,110,206001,2030,100,50,100,0,4061,"Stage_SF",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4062,6022,"ggo_gt_night_006","ggo_gt_night_006_mapData","ggo_gt_night_006_unitData","ggo_gt_night","BGM_F_GLOCKEN_NIGHT_BASIC","BGM_F_GLOCKEN_BOSS",False,10017,0,0,20000,20001,22,320,0,0,170,1,110,206002,2030,100,50,100,0,4062,"Stage_GLO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4063,6023,"sao_wf_dusk_019","sao_wf_dusk_019_mapData","sao_wf_dusk_019_unitData","sao_wf_dusk","BGM_F_WEST_EVENING_BASIC","BGM_F_WEST_BOSS",False,10004,10022,10025,20000,20001,24,380,0,0,180,1,110,206003,2030,100,50,100,0,4063,"Stage_WF_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4064,6024,"ggo_at_dusk_018","ggo_at_dusk_018_mapData","ggo_at_dusk_018_unitData","ggo_at_dusk","BGM_F_RUIN_EVENING_BASIC","BGM_F_RUIN_BOSS",False,10008,10031,0,20000,20001,26,340,0,0,200,1,120,206004,2030,100,50,100,0,4064,"Stage_GGO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4071,6025,"sao_wf_night_017","sao_wf_night_017_mapData","sao_wf_night_017_unitData","sao_dk_night","BGM_F_DANAK_NIGHT_BASIC","BGM_F_DANAK_BOSS",False,10023,0,0,20000,20001,20,340,0,0,170,1,110,207001,2030,100,50,100,0,4071,"Stage_DNK_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4072,6026,"ggo_at_night_013","ggo_at_night_013_mapData","ggo_at_night_013_unitData","ggo_at_night","BGM_F_RUIN_NIGHT_BASIC","BGM_F_RUIN_BOSS",False,10008,10023,10031,20000,20001,22,340,0,0,170,1,110,207002,2030,100,50,100,0,4072,"Stage_GGO_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4073,6027,"sao_fm_74_021","sao_fm_74_021_mapData","sao_fm_74_021_unitData","sao_fm_47","BGM_F_LABYRINTH_BASIC","BGM_F_LABYRINTH_BOSS",False,10005,10024,0,20000,20001,24,380,0,0,190,1,110,207003,2030,100,50,100,0,4073,"Stage_FM",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4074,6028,"sao_wm_night_003","sao_wm_night_003_mapData","sao_wm_night_003_unitData","sao_lw_night","BGM_F_WEST_MOUNTAIN_BASIC","BGM_F_WEST_MOUNTAIN_BOSS",False,10013,10029,0,20000,20001,26,360,0,0,180,1,120,207004,2030,100,50,100,0,4074,"Stage_WM_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4081,6029,"ggo_at_dusk_015_re","ggo_at_dusk_015_mapData","ggo_at_dusk_015_unitData","ggo_at_dusk","BGM_F_RUIN_EVENING_BASIC","BGM_F_RUIN_BOSS",False,10008,10031,0,20000,20001,24,380,0,0,190,1,110,208001,2030,100,50,100,0,4081,"Stage_GGO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4082,6030,"ggo_at_dusk_019","ggo_at_dusk_019_mapData","ggo_at_dusk_019_unitData","ggo_at_dusk","BGM_F_RUIN_EVENING_BASIC","BGM_F_RUIN_BOSS",False,10008,10031,0,20000,20001,22,380,0,0,190,1,105,208002,2030,100,50,100,0,4082,"Stage_GGO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4083,6031,"alo_ti_day_013","alo_ti_day_013_mapData","alo_ti_day_013_unitData","alo_ti_day","BGM_F_COAST_BASIC","BGM_F_WEST_BOSS",False,20004,10025,0,20000,20001,24,380,0,0,190,1,110,208003,2030,100,50,100,0,4083,"Stage_TI_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4084,6032,"alo_ti_day_014","alo_ti_day_014_mapData","alo_ti_day_014_unitData","alo_ti_day","BGM_F_COAST_BASIC","BGM_F_WEST_BOSS",False,20004,10025,0,20000,20001,26,360,0,0,180,1,120,208004,2030,100,50,100,0,4084,"Stage_TI_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4091,6033,"sao_wf_night_015_re","sao_wf_night_015_mapData","sao_wf_night_015_unitData","sao_wf_night","BGM_F_WEST_NIGHT_BASIC","BGM_F_WEST_BOSS",False,10004,10023,0,20000,20001,24,380,0,0,190,1,110,209001,2030,100,50,100,0,4091,"Stage_WF_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4092,6034,"sao_ff_day_009","sao_ff_day_009_mapData","sao_ff_day_009_unitData","sao_ff_day","BGM_F_FLORIA_DAYTIME_BASIC","BGM_F_FLORIA_BOSS",False,10010,10021,0,20000,20001,22,360,0,0,180,1,105,209002,2030,100,50,100,0,4092,"Stage_FLO_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4093,6035,"ggo_gt_day_007","ggo_gt_day_007_mapData","ggo_gt_day_007_unitData","ggo_gt_day","BGM_F_GLOCKEN_DAYTIME_BASIC","BGM_F_GLOCKEN_BOSS",False,10017,0,0,20000,20001,24,380,0,0,190,1,110,209003,2030,100,50,100,0,4093,"Stage_GLO_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4094,6036,"sao_ff_dusk_007","sao_ff_dusk_007_mapData","sao_ff_dusk_007_unitData","sao_ff_dusk","BGM_F_FLORIA_EVENING_BASIC","BGM_F_FLORIA_BOSS",False,10010,10022,0,20000,20001,26,340,0,0,170,1,120,209004,2030,100,50,100,0,4094,"Stage_FLO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4101,6037,"alo_ti_day_011_re","alo_ti_day_011_mapData","alo_ti_day_011_unitData","alo_ti_day","BGM_F_COAST_BASIC","BGM_F_WEST_BOSS",False,20004,10025,0,20000,20001,24,380,0,0,190,1,110,210001,2030,100,50,100,0,4101,"Stage_TI_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4102,6038,"sao_lw_night_003","sao_lw_night_003_mapData","sao_lw_night_003_unitData","sao_lw_night","BGM_F_LOST_FOREST_BASIC","BGM_F_LOST_FOREST_BOSS",False,10014,10029,0,20000,20001,22,360,0,0,180,1,105,210002,2030,100,50,100,0,4102,"Stage_LW_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4103,6039,"sao_wm_day_005","sao_wm_day_005_mapData","sao_wm_day_005_unitData","sao_wm_day","BGM_F_WEST_MOUNTAIN_BASIC","BGM_F_WEST_MOUNTAIN_BOSS",False,10013,10029,0,20000,20001,24,380,0,0,190,1,110,210003,2030,100,50,100,0,4103,"Stage_WM_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4104,6040,"alo_ip_basic_018","alo_ip_basic_018_mapData","alo_ip_basic_018_unitData","alo_id_srymheimr","BGM_F_SRYMHEIMR_BASIC","BGM_F_SRYMHEIMR_BOSS",False,10007,10024,10029,20000,20001,26,340,0,0,170,1,120,210004,2030,100,50,100,0,4104,"Stage_SF",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4111,6041,"sao_wf_day_028_re","sao_wf_day_028_mapData","sao_wf_day_028_unitData","sao_wf_day","BGM_F_WEST_DAYTIME_BASIC","BGM_F_WEST_BOSS",False,10004,10021,0,20000,20001,24,380,0,0,190,1,110,211001,2030,100,50,100,0,4111,"Stage_WF_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4112,6042,"alo_ti_day_015","alo_ti_day_015_mapData","alo_ti_day_015_unitData","alo_ti_day","BGM_F_COAST_BASIC","BGM_F_WEST_BOSS",False,20004,10025,0,20000,20001,22,380,0,0,190,1,110,211002,2030,100,50,100,0,4112,"Stage_TI_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4113,6043,"sao_ff_day_010","sao_ff_day_010_mapData","sao_ff_day_010_unitData","sao_ff_day","BGM_F_FLORIA_DAYTIME_BASIC","BGM_F_FLORIA_BOSS",False,10010,10021,0,20000,20001,24,380,0,0,190,1,110,211003,2030,100,50,100,0,4113,"Stage_FLO_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4114,6044,"alo_st_day_018","alo_st_day_018_mapData","alo_st_day_018_unitData","alo_st_day","BGM_F_WORLDTREE_DAYTIME_BASIC","BGM_F_WORLDTREE_BOSS",False,10006,10021,10026,20000,20001,26,380,0,0,190,1,120,211004,2030,100,50,100,0,4114,"Stage_ST_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4901,6045,"alo_st_day_014_ex","alo_st_day_014_mapData","alo_st_day_014_unitData","alo_st_day","BGM_F_WORLDTREE_DAYTIME_BASIC","BGM_F_WORLDTREE_BOSS",False,10006,10021,10026,20000,20001,130,340,0,0,170,1,110,201001,2030,100,50,100,0,4011,"Stage_ST_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4902,6046,"sao_ff_day_003_ex","sao_ff_day_003_mapData","sao_ff_day_003_unitData","sao_ff_day","BGM_F_FLORIA_DAYTIME_BASIC","BGM_F_FLORIA_BOSS",False,10010,10021,0,20000,20001,130,360,0,0,180,1,110,201002,2030,100,50,100,0,4012,"Stage_FLO_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4903,6047,"ggo_at_dusk_015_ex","ggo_at_dusk_015_mapData","ggo_at_dusk_015_unitData","ggo_at_dusk","BGM_F_RUIN_EVENING_BASIC","BGM_F_RUIN_BOSS",False,10008,10031,0,20000,20001,130,380,0,0,190,1,110,201003,2030,100,50,100,0,4013,"Stage_GGO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4904,6048,"sao_wm_night_002_ex","sao_wm_night_002_mapData","sao_wm_night_002_unitData","sao_wm_night","BGM_F_WEST_MOUNTAIN_BASIC","BGM_F_WEST_MOUNTAIN_BOSS",False,10013,10029,0,20000,20001,130,400,0,0,200,1,120,201004,2030,100,50,100,0,4014,"Stage_WM_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4905,6049,"sao_fm_74_018_ex","sao_fm_74_018_mapData","sao_fm_74_018_unitData","sao_fm_47","BGM_F_LABYRINTH_BASIC","BGM_F_LABYRINTH_BOSS",False,10005,10024,0,20000,20001,130,340,0,0,170,1,110,202001,2030,100,50,100,0,4021,"Stage_FM",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4906,6050,"alo_ip_basic_016_ex","alo_ip_basic_016_mapData","alo_ip_basic_016_unitData","alo_id_srymheimr","BGM_F_SRYMHEIMR_BASIC","BGM_F_SRYMHEIMR_BOSS",False,10007,10024,10029,20000,20001,130,380,0,0,190,1,110,202002,2030,100,50,100,0,4022,"Stage_SF",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4907,6051,"sao_wf_night_015_ex","sao_wf_night_015_mapData","sao_wf_night_015_unitData","sao_wf_night","BGM_F_WEST_NIGHT_BASIC","BGM_F_WEST_BOSS",False,10004,10023,0,20000,20001,130,380,0,0,190,1,110,202003,2030,100,50,100,0,4023,"Stage_WF_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4908,6052,"ggo_gt_day_006_ex","ggo_gt_day_006_mapData","ggo_gt_day_006_unitData","ggo_gt_day","BGM_F_GLOCKEN_DAYTIME_BASIC","BGM_F_GLOCKEN_BOSS",False,10017,0,0,20000,20001,130,400,0,0,200,1,120,202004,2030,100,50,100,0,4024,"Stage_GLO_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4909,6053,"alo_ti_day_009_ex","alo_ti_day_009_mapData","alo_ti_day_009_unitData","alo_ti_day","BGM_F_COAST_BASIC","BGM_F_WEST_BOSS",False,20004,10025,0,20000,20001,130,360,0,0,180,1,110,203001,2030,100,50,100,0,4031,"Stage_TI_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4910,6054,"alo_ti_day_010_ex","alo_ti_day_010_mapData","alo_ti_day_010_unitData","alo_ti_day","BGM_F_COAST_BASIC","BGM_F_WEST_BOSS",False,20004,10025,0,20000,20001,130,360,0,0,180,1,110,203002,2030,100,50,100,0,4032,"Stage_TI_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4911,6055,"alo_ti_day_011_ex","alo_ti_day_011_mapData","alo_ti_day_011_unitData","alo_ti_day","BGM_F_COAST_BASIC","BGM_F_WEST_BOSS",False,20004,10025,0,20000,20001,130,380,0,0,190,1,110,203003,2030,100,50,100,0,4033,"Stage_TI_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4912,6056,"alo_ti_day_012_ex","alo_ti_day_012_mapData","alo_ti_day_012_unitData","alo_ti_day","BGM_F_COAST_BASIC","BGM_F_WEST_BOSS",False,20004,10025,0,20000,20001,130,400,0,0,200,1,120,203004,2030,100,50,100,0,4034,"Stage_TI_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4913,6057,"sao_wf_night_016_ex","sao_wf_night_016_mapData","sao_wf_night_016_unitData","sao_wf_night","BGM_F_WEST_NIGHT_BASIC","BGM_F_WEST_BOSS",False,10004,10023,0,20000,20001,130,360,0,0,180,1,110,204001,2030,100,50,100,0,4041,"Stage_WF_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4914,6058,"sao_ff_day_007_ex","sao_ff_day_007_mapData","sao_ff_day_007_unitData","sao_ff_day","BGM_F_FLORIA_DAYTIME_BASIC","BGM_F_FLORIA_BOSS",False,10010,10021,0,20000,20001,130,360,0,0,180,1,110,204002,2030,100,50,100,0,4042,"Stage_FLO_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4915,6059,"sao_wf_day_028_ex","sao_wf_day_028_mapData","sao_wf_day_028_unitData","sao_wf_day","BGM_F_WEST_DAYTIME_BASIC","BGM_F_WEST_BOSS",False,10004,10021,0,20000,20001,130,380,0,0,190,1,110,204003,2030,100,50,100,0,4043,"Stage_WF_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4916,6060,"alo_st_day_016_ex","alo_st_day_016_mapData","alo_st_day_016_unitData","alo_st_day","BGM_F_WORLDTREE_DAYTIME_BASIC","BGM_F_WORLDTREE_BOSS",False,10006,10021,10026,20000,20001,130,400,0,0,200,1,120,204004,2030,100,50,100,0,4044,"Stage_ST_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4917,6061,"sao_fm_74_019_ex","sao_fm_74_019_mapData","sao_fm_74_019_unitData","sao_fm_47","BGM_F_LABYRINTH_BASIC","BGM_F_LABYRINTH_BOSS",False,10005,10024,0,20000,20001,130,300,0,0,170,1,110,205001,2030,100,50,100,0,4051,"Stage_FM",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4918,6062,"alo_st_night_013_ex","alo_st_night_013_mapData","alo_st_night_013_unitData","alo_st_night","BGM_F_WORLDTREE_NIGHT_BASIC","BGM_F_WORLDTREE_BOSS",False,10006,10023,10026,20000,20001,130,320,0,0,170,1,110,205002,2030,100,50,100,0,4052,"Stage_ST_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4919,6063,"sao_ff_day_008_ex","sao_ff_day_008_mapData","sao_ff_day_008_unitData","sao_ff_day","BGM_F_FLORIA_DAYTIME_BASIC","BGM_F_FLORIA_BOSS",False,10010,10021,0,20000,20001,130,330,0,0,180,1,110,205003,2030,100,50,100,0,4053,"Stage_FLO_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4920,6064,"sao_lw_night_002_ex","sao_lw_night_002_mapData","sao_lw_night_002_unitData","sao_lw_night","BGM_F_LOST_FOREST_BASIC","BGM_F_LOST_FOREST_BOSS",False,10014,10029,0,20000,20001,130,340,0,0,200,1,110,205004,2030,100,50,100,0,4054,"Stage_LW_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4921,6065,"alo_ip_basic_017_ex","alo_ip_basic_017_mapData","alo_ip_basic_017_unitData","alo_id_srymheimr","BGM_F_SRYMHEIMR_BASIC","BGM_F_SRYMHEIMR_BOSS",False,10007,10024,10029,20000,20001,130,300,0,0,170,1,110,206001,2030,100,50,100,0,4061,"Stage_SF",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4922,6066,"ggo_gt_night_006_ex","ggo_gt_night_006_mapData","ggo_gt_night_006_unitData","ggo_gt_night","BGM_F_GLOCKEN_NIGHT_BASIC","BGM_F_GLOCKEN_BOSS",False,10017,0,0,20000,20001,130,320,0,0,170,1,110,206002,2030,100,50,100,0,4062,"Stage_GLO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4923,6067,"sao_wf_dusk_019_ex","sao_wf_dusk_019_mapData","sao_wf_dusk_019_unitData","sao_wf_dusk","BGM_F_WEST_EVENING_BASIC","BGM_F_WEST_BOSS",False,10004,10022,10025,20000,20001,130,380,0,0,180,1,110,206003,2030,100,50,100,0,4063,"Stage_WF_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4924,6068,"ggo_at_dusk_018_ex","ggo_at_dusk_018_mapData","ggo_at_dusk_018_unitData","ggo_at_dusk","BGM_F_RUIN_EVENING_BASIC","BGM_F_RUIN_BOSS",False,10008,10031,0,20000,20001,130,340,0,0,200,1,120,206004,2030,100,50,100,0,4064,"Stage_GGO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4925,6069,"sao_wf_night_017_ex","sao_wf_night_017_mapData","sao_wf_night_017_unitData","sao_dk_night","BGM_F_DANAK_NIGHT_BASIC","BGM_F_DANAK_BOSS",False,10023,0,0,20000,20001,130,340,0,0,170,1,110,207001,2030,100,50,100,0,4071,"Stage_DNK_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4926,6070,"ggo_at_night_013_ex","ggo_at_night_013_mapData","ggo_at_night_013_unitData","ggo_at_night","BGM_F_RUIN_NIGHT_BASIC","BGM_F_RUIN_BOSS",False,10008,10023,10031,20000,20001,130,340,0,0,170,1,110,207002,2030,100,50,100,0,4072,"Stage_GGO_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4927,6071,"sao_fm_74_021_ex","sao_fm_74_021_mapData","sao_fm_74_021_unitData","sao_fm_47","BGM_F_LABYRINTH_BASIC","BGM_F_LABYRINTH_BOSS",False,10005,10024,0,20000,20001,130,380,0,0,190,1,110,207003,2030,100,50,100,0,4073,"Stage_FM",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4928,6072,"sao_wm_night_003_ex","sao_wm_night_003_mapData","sao_wm_night_003_unitData","sao_lw_night","BGM_F_WEST_MOUNTAIN_BASIC","BGM_F_WEST_MOUNTAIN_BOSS",False,10013,10029,0,20000,20001,130,360,0,0,180,1,120,207004,2030,100,50,100,0,4074,"Stage_WM_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4929,6073,"ggo_at_dusk_015_re_ex","ggo_at_dusk_015_mapData","ggo_at_dusk_015_unitData","ggo_at_dusk","BGM_F_RUIN_EVENING_BASIC","BGM_F_RUIN_BOSS",False,10008,10031,0,20000,20001,130,380,0,0,190,1,110,208001,2030,100,50,100,0,4081,"Stage_GGO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4930,6074,"ggo_at_dusk_019_ex","ggo_at_dusk_019_mapData","ggo_at_dusk_019_unitData","ggo_at_dusk","BGM_F_RUIN_EVENING_BASIC","BGM_F_RUIN_BOSS",False,10008,10031,0,20000,20001,130,380,0,0,190,1,105,208002,2030,100,50,100,0,4082,"Stage_GGO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4931,6075,"alo_ti_day_013_ex","alo_ti_day_013_mapData","alo_ti_day_013_unitData","alo_ti_day","BGM_F_COAST_BASIC","BGM_F_WEST_BOSS",False,20004,10025,0,20000,20001,130,380,0,0,190,1,110,208003,2030,100,50,100,0,4083,"Stage_TI_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4932,6076,"alo_ti_day_014_ex","alo_ti_day_014_mapData","alo_ti_day_014_unitData","alo_ti_day","BGM_F_COAST_BASIC","BGM_F_WEST_BOSS",False,20004,10025,0,20000,20001,130,360,0,0,180,1,120,208004,2030,100,50,100,0,4084,"Stage_TI_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4933,6077,"sao_wf_night_015_re_ex","sao_wf_night_015_mapData","sao_wf_night_015_unitData","sao_wf_night","BGM_F_WEST_NIGHT_BASIC","BGM_F_WEST_BOSS",False,10004,10023,0,20000,20001,130,380,0,0,190,1,110,209001,2030,100,50,100,0,4091,"Stage_WF_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4934,6078,"sao_ff_day_009_ex","sao_ff_day_009_mapData","sao_ff_day_009_unitData","sao_ff_day","BGM_F_FLORIA_DAYTIME_BASIC","BGM_F_FLORIA_BOSS",False,10010,10021,0,20000,20001,130,360,0,0,180,1,105,209002,2030,100,50,100,0,4092,"Stage_FLO_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4935,6079,"ggo_gt_day_007_ex","ggo_gt_day_007_mapData","ggo_gt_day_007_unitData","ggo_gt_day","BGM_F_GLOCKEN_DAYTIME_BASIC","BGM_F_GLOCKEN_BOSS",False,10017,0,0,20000,20001,130,380,0,0,190,1,110,209003,2030,100,50,100,0,4093,"Stage_GLO_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4936,6080,"sao_ff_dusk_007_ex","sao_ff_dusk_007_mapData","sao_ff_dusk_007_unitData","sao_ff_dusk","BGM_F_FLORIA_EVENING_BASIC","BGM_F_FLORIA_BOSS",False,10010,10022,0,20000,20001,130,340,0,0,170,1,120,209004,2030,100,50,100,0,4094,"Stage_FLO_dusk",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4937,6081,"alo_ti_day_011_re_ex","alo_ti_day_011_mapData","alo_ti_day_011_unitData","alo_ti_day","BGM_F_COAST_BASIC","BGM_F_WEST_BOSS",False,20004,10025,0,20000,20001,130,380,0,0,190,1,110,210001,2030,100,50,100,0,4101,"Stage_TI_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4938,6082,"sao_lw_night_003_ex","sao_lw_night_003_mapData","sao_lw_night_003_unitData","sao_lw_night","BGM_F_LOST_FOREST_BASIC","BGM_F_LOST_FOREST_BOSS",False,10014,10029,0,20000,20001,130,360,0,0,180,1,105,210002,2030,100,50,100,0,4102,"Stage_LW_night",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4939,6083,"sao_wm_day_005_ex","sao_wm_day_005_mapData","sao_wm_day_005_unitData","sao_wm_day","BGM_F_WEST_MOUNTAIN_BASIC","BGM_F_WEST_MOUNTAIN_BOSS",False,10013,10029,0,20000,20001,130,380,0,0,190,1,110,210003,2030,100,50,100,0,4103,"Stage_WM_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4940,6084,"alo_ip_basic_018_ex","alo_ip_basic_018_mapData","alo_ip_basic_018_unitData","alo_id_srymheimr","BGM_F_SRYMHEIMR_BASIC","BGM_F_SRYMHEIMR_BOSS",False,10007,10024,10029,20000,20001,130,340,0,0,170,1,120,210004,2030,100,50,100,0,4104,"Stage_SF",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4941,6085,"sao_wf_day_028_re_ex","sao_wf_day_028_mapData","sao_wf_day_028_unitData","sao_wf_day","BGM_F_WEST_DAYTIME_BASIC","BGM_F_WEST_BOSS",False,10004,10021,0,20000,20001,130,380,0,0,190,1,110,211001,2030,100,50,100,0,4111,"Stage_WF_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4942,6086,"alo_ti_day_015_ex","alo_ti_day_015_mapData","alo_ti_day_015_unitData","alo_ti_day","BGM_F_COAST_BASIC","BGM_F_WEST_BOSS",False,20004,10025,0,20000,20001,130,380,0,0,190,1,110,211002,2030,100,50,100,0,4112,"Stage_TI_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4943,6087,"sao_ff_day_010_ex","sao_ff_day_010_mapData","sao_ff_day_010_unitData","sao_ff_day","BGM_F_FLORIA_DAYTIME_BASIC","BGM_F_FLORIA_BOSS",False,10010,10021,0,20000,20001,130,380,0,0,190,1,110,211003,2030,100,50,100,0,4113,"Stage_FLO_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4944,6088,"alo_st_day_018_ex","alo_st_day_018_mapData","alo_st_day_018_unitData","alo_st_day","BGM_F_WORLDTREE_DAYTIME_BASIC","BGM_F_WORLDTREE_BOSS",False,10006,10021,10026,20000,20001,130,380,0,0,190,1,120,211004,2030,100,50,100,0,4114,"Stage_ST_day",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -4945,6089,"sao_wm_dawn_002","sao_wm_dawn_002_mapData","sao_wm_dawn_002_unitData","sao_wm_dawn","BGM_F_WEST_MOUNTAIN_BASIC","BGM_F_PUNISHER_BOSS",False,10013,10029,0,0,0,130,400,0,0,200,1,120,290045,2030,100,50,100,0,4945,"Stage_WM_dawn",20,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -29003,4903,"VS_sao_wf_day_003","VS_sao_wf_day_003_mapData","VS_sao_wf_day_003_unitData","sao_wf_day","BGM_F_WEST_DAYTIME_BASIC","BGM_F_WEST_BOSS",False,20023,0,0,0,0,10,400,0,0,400,1,99,301700,1,100,50,180,0,1,"Stage_WF_day",0,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -29004,4904,"VS_alo_ip_basic_002","VS_alo_ip_basic_002_mapData","VS_alo_ip_basic_002_unitData","alo_id_srymheimr","BGM_F_SRYMHEIMR_BASIC","BGM_F_SRYMHEIMR_BOSS",False,20023,0,0,0,0,10,400,0,0,400,1,99,301700,1,100,50,180,0,1,"Stage_SF",0,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -29005,4905,"VS_ggo_at_dusk_001","VS_ggo_at_dusk_001_mapData","VS_ggo_at_dusk_001_unitData","ggo_at_dusk","BGM_F_RUIN_EVENING_BASIC","BGM_F_RUIN_BOSS",False,20023,0,0,0,0,10,400,0,0,400,1,99,301700,1,100,50,180,0,1,"Stage_GGO_dusk",0,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -29006,4906,"VS_alo_ti_day_001","VS_alo_ti_day_001_mapData","VS_alo_ti_day_001_unitData","alo_ti_day","BGM_F_COAST_BASIC","BGM_F_WEST_BOSS",False,20023,0,0,0,0,10,400,0,0,400,1,99,301700,1,100,50,180,0,1,"Stage_TI_day",0,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -29007,4907,"VS_sao_ff_day_001","VS_sao_ff_day_001_mapData","VS_sao_ff_day_001_unitData","sao_ff_day","BGM_F_FLORIA_DAYTIME_BASIC","BGM_F_FLORIA_BOSS",False,20023,0,0,0,0,10,400,0,0,400,1,99,301700,1,100,50,180,0,1,"Stage_FLO_day",0,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -29008,4908,"VS_sao_wm_day_001","VS_sao_wm_day_001_mapData","VS_sao_wm_day_001_unitData","sao_wm_day","BGM_F_WEST_MOUNTAIN_BASIC","BGM_F_WEST_MOUNTAIN_BOSS",False,20023,0,0,0,0,10,400,0,0,400,1,99,301700,1,100,50,180,0,1,"Stage_WM_day",0,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -30001,5001,"vs_test","vs_test_mapData","vs_test_unitData","sao_wf_day","BGM_F_WEST_DAYTIME_BASIC","BGM_F_WEST_BOSS",False,20023,0,0,0,0,2,999,0,0,999,1,99,300000,1,100,50,180,0,1,"Stage_WF_day",0,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, -30002,5002,"vs_test2","vs_test2_mapData","vs_test2_unitData","alo_id_srymheimr","BGM_F_SRYMHEIMR_BASIC","BGM_F_SRYMHEIMR_BOSS",False,20023,0,0,0,0,2,999,0,0,999,1,99,300000,1,100,50,180,0,1,"Stage_WF_day",0,1,1,1,1,2,1,1,1,1,1,1,3,1,1,10,15,20, diff --git a/titles/sao/data/RareDropTable.csv b/titles/sao/data/RareDropTable.csv deleted file mode 100644 index cb674f6..0000000 --- a/titles/sao/data/RareDropTable.csv +++ /dev/null @@ -1,960 +0,0 @@ -QuestRareDropId,CommonRewardId -7,101000140 -8,102000140 -9,103000130 -10,104000130 -11,105000130 -12,106000130 -13,107000120 -14,108000120 -15,170000 -16,160000 -17,120330 -18,120320 -19,180001 -20,120300 -21,120310 -22,130150 -23,120350 -24,120360 -25,120340 -26,120330 -27,108000005 -28,130090 -29,130100 -30,130120 -31,111000005 -32,170001 -33,109000005 -34,140000 -35,130110 -36,130140 -37,110210 -38,130130 -39,109000005 -40,101000007 -41,170002 -42,103000006 -43,108000006 -44,120000005 -45,140000 -46,130190 -47,120390 -48,105000005 -49,105000006 -50,170003 -51,130200 -52,180003 -53,102000007 -54,112000007 -55,120000006 -56,170004 -57,102000006 -58,103000007 -59,111000006 -60,150040 -61,150030 -62,150020 -63,109000006 -64,109000007 -65,120000004 -66,101000010 -67,103000110 -68,102000130 -69,101000130 -70,170001 -71,150010 -72,109000120 -73,110000120 -74,110044 -75,110024 -76,110034 -77,110054 -78,106000140 -79,130540 -80,121000 -81,120990 -82,121010 -83,110000140 -84,130420 -85,120560 -86,110670 -87,120570 -88,121020 -89,110250 -90,110240 -91,110260 -92,107000006 -93,120430 -94,109000140 -95,102000160 -96,102000160 -97,102000160 -98,102000160 -99,110620 -100,120251 -101,120550 -102,120241 -103,104000150 -104,130180 -105,120261 -106,130680 -107,110700 -108,115000005 -109,110690 -110,106000150 -111,190001 -112,200001 -113,210001 -114,105000140 -115,190000 -116,200000 -117,210000 -118,190000 -119,200000 -120,210000 -121,190000 -122,200000 -123,210000 -124,190000 -125,200000 -126,210000 -127,190000 -128,200000 -129,210000 -130,190000 -131,200000 -132,210000 -133,190000 -134,200000 -135,210000 -136,190000 -137,200000 -138,210000 -139,190000 -140,200000 -141,210000 -142,190000 -143,200000 -144,210000 -145,190000 -146,200000 -147,210000 -148,190000 -149,200000 -150,210000 -151,190000 -152,200000 -153,210000 -154,190000 -155,200000 -156,210000 -157,190000 -158,200000 -159,210000 -160,190000 -161,200000 -162,210000 -163,190000 -164,200000 -165,210000 -166,190000 -167,200000 -168,210000 -169,190000 -170,200000 -171,210000 -172,190000 -173,200000 -174,210000 -175,190000 -176,200000 -177,210000 -178,190000 -179,200000 -180,210000 -181,190000 -182,200000 -183,210000 -184,190000 -185,200000 -186,210000 -187,190000 -188,200000 -189,210000 -190,190000 -191,200000 -192,210000 -193,190000 -194,200000 -195,210000 -196,190000 -197,200000 -198,210000 -199,190000 -200,200000 -201,210000 -202,190000 -203,200000 -204,210000 -205,111000070 -206,112000070 -207,108000007 -208,130080 -209,120370 -210,130060 -211,120380 -212,120410 -213,130720 -214,130070 -215,120710 -216,103000170 -217,108000130 -218,104000210 -219,104000210 -220,190000 -221,200000 -222,210000 -223,190000 -224,200000 -225,210000 -226,190000 -227,200000 -228,210000 -229,190000 -230,200000 -231,210000 -232,190000 -233,200000 -234,210000 -235,190000 -236,200000 -237,210000 -238,190000 -239,200000 -240,210000 -241,190000 -242,200000 -243,210000 -244,190000 -245,200000 -246,210000 -247,190000 -248,200000 -249,210000 -250,105000007 -251,130510 -252,103000190 -253,120740 -254,120400 -255,105000180 -256,109000200 -257,110220 -258,120730 -259,101000210 -260,107000130 -261,102000220 -262,102000220 -263,190000 -264,200000 -265,210000 -266,190000 -267,200000 -268,210000 -269,190000 -270,200000 -271,210000 -272,190000 -273,200000 -274,210000 -275,190000 -276,200000 -277,210000 -278,190000 -279,200000 -280,210000 -281,190000 -282,200000 -283,210000 -284,190000 -285,200000 -286,210000 -287,190000 -288,200000 -289,210000 -290,190000 -291,200000 -292,210000 -293,104000270 -294,110000160 -295,130790 -296,130800 -297,112000100 -298,170005 -299,180005 -300,190000 -301,200000 -302,210000 -303,190000 -304,200000 -305,210000 -306,190000 -307,200000 -308,210000 -309,118000080 -310,170005 -311,103000240 -312,120271 -313,120420 -314,106000220 -315,170005 -316,180005 -317,190000 -318,200000 -319,210000 -320,190000 -321,200000 -322,210000 -323,190000 -324,200000 -325,210000 -326,170005 -327,111000190 -328,131130 -329,130531 -330,118000110 -331,170005 -332,180005 -333,190000 -334,200000 -335,210000 -336,190000 -337,200000 -338,210000 -339,190000 -340,200000 -341,210000 -342,170005 -343,101000290 -344,102000350 -345,120262 -346,104000360 -347,170005 -348,180005 -349,190000 -350,200000 -351,210000 -352,190000 -353,200000 -354,210000 -355,190000 -356,200000 -357,210000 -358,101000019 -359,170005 -360,107000170 -361,121100 -362,120731 -363,103000330 -364,170005 -365,180005 -366,190000 -367,200000 -368,210000 -369,190000 -370,200000 -371,210000 -372,190000 -373,200000 -374,210000 -375,170005 -376,110000210 -377,110191 -378,104000410 -379,109000330 -380,170005 -381,180005 -382,190000 -383,200000 -384,210000 -385,190000 -386,200000 -387,210000 -388,190000 -389,200000 -390,210000 -391,170005 -392,105000310 -393,130700 -394,103000370 -395,102000460 -396,170005 -397,180005 -398,190000 -399,200000 -400,210000 -401,190000 -402,200000 -403,210000 -404,190000 -405,200000 -406,210000 -407,110000160 -408,112000180 -409,130790 -410,109000380 -411,111000270 -412,170005 -413,180005 -414,190000 -415,200000 -416,210000 -417,190000 -418,200000 -419,210000 -420,190000 -421,200000 -422,210000 -423,112000100 -424,103000240 -425,106000350 -426,110221 -427,102000490 -428,118000190 -429,170005 -430,180005 -431,190000 -432,200000 -433,210000 -434,190000 -435,200000 -436,210000 -437,190000 -438,200000 -439,210000 -440,106000220 -441,111000190 -442,105000360 -443,131130 -444,112000190 -445,104000500 -446,170005 -447,180005 -448,190000 -449,200000 -450,210000 -451,190000 -452,200000 -453,210000 -454,190000 -455,200000 -456,210000 -457,118000110 -458,101000290 -459,103000470 -460,101000019 -461,111000340 -462,109000420 -463,170005 -464,180005 -465,190000 -466,200000 -467,210000 -468,190000 -469,200000 -470,210000 -471,190000 -472,200000 -473,210000 -474,104000360 -475,110000300 -476,170006 -477,180006 -478,220002 -479,170006 -480,180006 -481,220002 -482,170006 -483,180006 -484,220002 -485,170006 -486,180006 -487,220002 -488,170006 -489,180006 -490,220002 -491,170006 -492,180006 -493,220002 -494,170006 -495,180006 -496,220002 -497,170006 -498,180006 -499,220002 -500,170006 -501,180006 -502,220002 -503,170006 -504,180006 -505,220002 -506,170006 -507,180006 -508,220002 -509,170006 -510,180006 -511,220002 -512,170006 -513,180006 -514,220002 -515,170006 -516,180006 -517,220002 -518,170006 -519,180006 -520,220002 -521,170006 -522,180006 -523,220002 -524,170006 -525,180006 -526,220002 -527,170006 -528,180006 -529,220002 -530,170006 -531,180006 -532,220002 -533,170006 -534,180006 -535,220002 -536,170006 -537,180006 -538,220002 -539,170006 -540,180006 -541,220002 -542,170006 -543,180006 -544,220002 -545,170006 -546,180006 -547,220002 -548,170006 -549,180006 -550,220002 -551,170006 -552,180006 -553,220002 -554,170006 -555,180006 -556,220002 -557,170006 -558,180006 -559,220002 -560,170006 -561,180006 -562,220002 -563,170006 -564,180006 -565,220002 -566,170006 -567,180006 -568,220002 -569,170006 -570,180006 -571,220002 -572,170006 -573,180006 -574,220002 -575,170006 -576,180006 -577,220002 -578,170006 -579,180006 -580,220002 -581,170006 -582,180006 -583,220002 -584,170006 -585,180006 -586,220002 -587,170006 -588,180006 -589,220002 -590,170006 -591,180006 -592,220002 -593,170006 -594,180006 -595,220002 -596,170006 -597,180006 -598,220002 -599,170006 -600,180006 -601,220002 -602,170006 -603,180006 -604,220002 -605,170006 -606,180006 -607,220002 -608,170006 -609,180006 -610,220002 -611,170006 -612,180006 -613,220002 -614,170006 -615,180006 -616,220002 -617,170006 -618,180006 -619,220002 -620,170006 -621,180006 -622,220002 -623,170006 -624,180006 -625,220002 -626,104000270 -627,110000160 -628,130790 -629,130800 -630,112000100 -631,170005 -632,103000240 -633,120271 -634,120420 -635,106000220 -636,170005 -637,111000190 -638,131130 -639,130531 -640,118000110 -641,170005 -642,101000290 -643,102000350 -644,120262 -645,104000360 -646,101000019 -647,170005 -648,107000170 -649,121100 -650,120731 -651,103000330 -652,170005 -653,110000210 -654,110191 -655,104000410 -656,109000330 -657,170005 -658,105000310 -659,130700 -660,103000370 -661,102000460 -662,110790 -663,112000180 -664,130790 -665,109000380 -666,111000270 -667,110800 -668,110261 -669,110271 -670,110680 -671,106000350 -672,110221 -673,102000490 -674,118000190 -675,110241 -676,110251 -677,110730 -678,110540 -679,105000360 -680,131130 -681,112000190 -682,104000500 -683,110055 -684,111100 -685,120551 -686,120440 -687,103000470 -688,101000019 -689,111000340 -690,109000420 -691,111020 -692,111160 -693,121160 -694,110000300 -695,121140 -696,190000 -697,200000 -698,210000 -699,170007 -700,180007 -701,220002 -702,190000 -703,200000 -704,210000 -705,170007 -706,180007 -707,220002 -708,190000 -709,200000 -710,210000 -711,170007 -712,180007 -713,220002 -714,190000 -715,200000 -716,210000 -717,170007 -718,180007 -719,220002 -720,190000 -721,200000 -722,210000 -723,170007 -724,180007 -725,220002 -726,190000 -727,200000 -728,210000 -729,170007 -730,180007 -731,220002 -732,190000 -733,200000 -734,210000 -735,170007 -736,180007 -737,220002 -738,190000 -739,200000 -740,210000 -741,170007 -742,180007 -743,220002 -744,190000 -745,200000 -746,210000 -747,170007 -748,180007 -749,220002 -750,190000 -751,200000 -752,210000 -753,170007 -754,180007 -755,220002 -756,190000 -757,200000 -758,210000 -759,170007 -760,180007 -761,220002 -762,190000 -763,200000 -764,210000 -765,170007 -766,180007 -767,220002 -768,190000 -769,200000 -770,210000 -771,170007 -772,180007 -773,220002 -774,190000 -775,200000 -776,210000 -777,170007 -778,180007 -779,220002 -780,190000 -781,200000 -782,210000 -783,170007 -784,180007 -785,220002 -786,190000 -787,200000 -788,210000 -789,170007 -790,180007 -791,220002 -792,190000 -793,200000 -794,210000 -795,170007 -796,180007 -797,220002 -798,190000 -799,200000 -800,210000 -801,170007 -802,180007 -803,220002 -804,190000 -805,200000 -806,210000 -807,170007 -808,180007 -809,220002 -810,190000 -811,200000 -812,210000 -813,170007 -814,180007 -815,220002 -816,190000 -817,200000 -818,210000 -819,170007 -820,180007 -821,220002 -822,190000 -823,200000 -824,210000 -825,170007 -826,180007 -827,220002 -828,190000 -829,200000 -830,210000 -831,170007 -832,180007 -833,220002 -834,190000 -835,200000 -836,210000 -837,170007 -838,180007 -839,220002 -840,190000 -841,200000 -842,210000 -843,170007 -844,180007 -845,220002 -846,190000 -847,200000 -848,210000 -849,170007 -850,180007 -851,220002 -852,190000 -853,200000 -854,210000 -855,170007 -856,180007 -857,220002 -858,190000 -859,200000 -860,210000 -861,170007 -862,180007 -863,220002 -864,190000 -865,200000 -866,210000 -867,170007 -868,180007 -869,220002 -870,190000 -871,200000 -872,210000 -873,170007 -874,180007 -875,220002 -876,190000 -877,200000 -878,210000 -879,170007 -880,180007 -881,220002 -882,190000 -883,200000 -884,210000 -885,170007 -886,180007 -887,220002 -888,190000 -889,200000 -890,210000 -891,170007 -892,180007 -893,220002 -894,190000 -895,200000 -896,210000 -897,170007 -898,180007 -899,220002 -900,190000 -901,200000 -902,210000 -903,170007 -904,180007 -905,220002 -906,190000 -907,200000 -908,210000 -909,170007 -910,180007 -911,220002 -912,190000 -913,200000 -914,210000 -915,170007 -916,180007 -917,220002 -918,190000 -919,200000 -920,210000 -921,170007 -922,180007 -923,220002 -924,190000 -925,200000 -926,210000 -927,170007 -928,180007 -929,220002 -930,190000 -931,200000 -932,210000 -933,170007 -934,180007 -935,220002 -936,190000 -937,200000 -938,210000 -939,170007 -940,180007 -941,220002 -942,190000 -943,200000 -944,210000 -945,170007 -946,180007 -947,220002 -948,190000 -949,200000 -950,210000 -951,170007 -952,180007 -953,220002 -954,190000 -955,200000 -956,210000 -957,170007 -958,180007 -959,220002 -960,190000 -961,200000 -962,210000 -963,170007 -964,180007 -965,220002 diff --git a/titles/sao/data/SupportLog.csv b/titles/sao/data/SupportLog.csv deleted file mode 100644 index 92295b1..0000000 --- a/titles/sao/data/SupportLog.csv +++ /dev/null @@ -1,484 +0,0 @@ -SupportLogId,CharaId,Name,Rarity,SupportLogTypeId,SalePrice,CompositionExp,AwakeningExp,UseRank,GroupNo,AdaptableAppearAppend,PunisherAppearAppend,State,PassiveState,PowerDefaultValue,CltDefaultValue,Awakening1Power,Awakening1Clt,Awakening2Power,Awakening2Clt,Awakening3Power,Awakening3Clt,Awakening4Power,Awakening4Clt,Awakening5Power,Awakening5Clt,NormalLeader,HoloLeader,UiDisplayPowerTitle,UiDisplayPowerContent,Prefab,CutinMode,SkillVoiceId,SkillName,NameInSkillCutin,SkillText,SkillTextInSkillCutin,CharaInfo,CutinImage,CutinImageAwake,StatusCharaIcon,StatusCharaIconAwake,CollectionDisplayStartDate,CollectionEmptyFrameDisplayFlag,DateVersionId, -201000010,999,"《赤の盟約》キリト",1,2,100,1500,100,1,0,0,0,"s201000010",,30,90,30,85,40,85,40,80,50,80,50,75,10,20,"ボーナス増加量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_KIR_SUP_006","ブラッド・プレッジ","ブラッド・〇〇〇〇〇〇プレッジ","30秒間、ラストアタックで
得られるボーナスポイントが
増加する。
覚醒で増加量が上昇。","ラストアタックで得られるボーナスポイント量上昇","伝説的に語られる《黒の剣士》
だが、彼にも敗北はあった。
聖騎士に敗れ《血盟騎士団》に
所属した時の記録。","slog_201000010","slog_201000010","Chara_icon/201000010","Chara_icon/201000010","2019/01/01",True,"1", -202000010,999,"《穢れなき願い》アスナ",2,1,200,5000,100,1,0,0,0,"s202000010",,60,120,60,114,65,114,65,108,70,102,75,96,10,20,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_ASU_SUP_006","ホープフル・ティアー","ホープフル・〇〇〇〇〇ティアー","戦闘不能状態のメンバーを2人
復帰させ、耐久値を回復する。
覚醒で復帰時の回復量が上昇。","戦闘不能状態の自パーティメンバー2人を復帰","かの《ALO事件》において特殊
な状況に置かれていた彼女。
どんなに過酷な逆境にあっても
その想いは高潔であった。","slog_202000010","slog_202000010","Chara_icon/202000010","Chara_icon/202000010","2019/01/01",True,"1", -208000010,999,"《灼熱の踊夫》エギル",3,3,400,10000,100,1,0,0,0,"s208000010",,20,130,20,124,25,118,30,112,32,106,34,100,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_AGI_SUP_006","ファイヤーカーニバル","ファイヤー〇〇〇〇〇カーニバル","攻撃に火属性を付与しSTRを
50%上昇させるとともに、
広範囲に火炎トラップを設置。
覚醒で効果時間が延長。","火属性付与+STR上昇+広範囲に火炎トラップ","男は祭りだ!そして炎だ!
ファイヤートーチを手に
血が滾ったのか、誰よりも
ヒートアップしているようだ。","slog_208000010","slog_208000010","Chara_icon/208000010","Chara_icon/208000010","2019/01/01",True,"1", -215000010,999,"《魔性の美貌》ストレア",2,3,200,5000,100,1,0,0,0,"s215000010",,10,90,10,85,12,85,12,80,14,75,15,70,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ミステリアスビューティ","ミステリアス〇〇〇〇〇ビューティ","敵に感知されず、他パーティの
ミニマップ上からも見えなくなり
移動速度も上昇する。
覚醒で効果時間が延長。","ハイディング+移動速度上昇","《SAO》にてキリト達の前に
現れた謎の美女。かつては
ユイと同じ《MHCP》だったが
今では自由の身となっている。","slog_215000010","slog_215000010","Chara_icon/215000010","Chara_icon/215000010","2019/01/01",True,"1", -216000010,999,"《疾駆の斬撃》フィリア",2,2,200,5000,100,1,0,0,0,"s216000010",,10,80,10,75,12,75,12,70,14,65,15,60,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"トレジャーハンティング","トレジャー〇〇〇〇〇ハンティング","スキルポーションとグリードの
位置がミニマップに表示され、
移動速度も上昇する。
覚醒で効果時間が延長。","スキルポーション&グリードサーチ発動+移動速度上昇","《SAO》に現れたホロウエリア
で出会った少女。キリトにより
助け出され、現在はトレジャー
ハンターとして活動している。","slog_216000010","slog_216000010","Chara_icon/216000010","Chara_icon/216000010","2019/01/01",True,"1", -217000010,999,"《七色の歌姫》セブン",2,3,200,5000,100,1,0,0,0,"s217000010",,15,120,15,114,20,114,20,108,25,102,27,96,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"レインボー・コンサート","レインボー・〇〇〇〇コンサート","自パーティに敵の注目を集め、
一定範囲内の全パーティと全敵の
STRを80%上昇させる。
覚醒で効果時間が延長。","自パーティに敵の注目を集める+全ユニットのSTR上昇","リアルでは七色博士としても
有名な少女。VR世界ではアイド
ル活動を行っていたが、現在は
休止している。","slog_217000010","slog_217000010","Chara_icon/217000010","Chara_icon/217000010","2019/01/01",True,"1", -218000010,999,"《千光の剣閃》レイン",2,2,200,5000,100,1,0,0,0,"s218000010",,40,120,40,114,45,114,45,108,50,102,52,96,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_RAI_SUP_004","マーグル・メキアー・レクン","マーグル・〇〇〇メキアー・レクン","攻撃時に50%の確率で
追加攻撃を発生させる。
覚醒で効果時間を延長。","攻撃時に確率で追加攻撃","《ALO》でキリト達と出会った
少女。レプラコーンのスキルを
応用した多刀流の使い手。セブン
の実の姉で、アイドルの卵。","slog_218000010","slog_218000010","Chara_icon/218000010","Chara_icon/218000010","2019/01/01",True,"1", -220000010,999,"《星光の笑顔》サチ",2,1,200,5000,100,1,0,0,0,"s220000010",,40,30,40,28,45,28,45,26,50,24,55,22,10,20,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"ムーンナイト・ウィッシュ","ムーンナイト・〇〇〇〇ウィッシュ","自パーティの耐久値と状態異常を
回復する。
覚醒で回復量が上昇。","耐久値と状態異常を回復","《月夜の黒猫団》メンバー。
《SAO》で一時期キリトと
行動を共にした。臆病な所も
あるが、純粋で優しい少女。","slog_220000010","slog_220000010","Chara_icon/220000010","Chara_icon/220000010","2019/01/01",True,"1", -290000010,999,"《最愛の愛娘》ユイ",2,2,200,5000,100,1,0,0,0,"s290000010",,12,40,12,38,14,38,14,36,16,34,17,32,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_YUI_SUP_004","プログラム・オーソリティ","プログラム・〇〇〇〇オーソリティ","スキルポーションと
レコードメダル、グリードの
位置をミニマップに表示する。
覚醒で効果時間が延長。","スキルポーション&レコードメダル&グリードサーチ発動","《SAO》でキリトとアスナが
出会った女の子。二人のことを
「パパ」「ママ」と呼び慕う。
その絆は血の繋がりにも勝る。","slog_290000010","slog_290000010","Chara_icon/290000010","Chara_icon/290000010","2019/01/01",True,"1", -291000010,999,"《電脳の迷い子》リコ",4,3,800,15000,100,1,0,0,0,"s291000010",,20,120,25,108,30,102,32,96,34,90,36,84,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_REC_SUP_002","ネクストナビゲート","ネクスト〇〇〇〇〇〇ナビゲート","その時点で最も耐久値の少ない
大型モンスターの元へ転移する
ワープポイントを設置する。
覚醒で設置時間が延長。","ボス部屋へ移動するワープポイントを設置","《Unknown》で出会った謎の
少女。何故かプレイヤーに忠誠
心を抱いている。行きたい場所
があるようだが…?
","slog_291000010","slog_291000010","Chara_icon/291000010","Chara_icon/291000010","2019/01/01",True,"1", -292000010,999,"《飄逸な策略家》クリスハイト",1,1,100,1500,100,1,0,0,0,"s292000010",,30,75,30,70,35,70,35,65,40,65,40,60,10,20,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_KIK_SUP_003","エンドレスワーキング","エンドレス〇〇〇〇〇ワーキング","戦闘不能状態のメンバーを1人
復帰させ、自パーティのINTを
10秒間70%上昇させる。
覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー1人復帰+自パーティのINT上昇","政府の役人、菊岡誠二郎の
《ALO》におけるアバター。
キリト達パーティにとっては
貴重なメイジ役。","slog_292000010","slog_292000010","Chara_icon/292000010","Chara_icon/292000010","2019/01/01",True,"1", -202000020,999,"《風雅なる舞踊家》アスナ",5,2,1600,20000,100,1,0,0,0,"s202000020",,50,185,60,169,70,153,80,145,85,137,90,129,10,20,"ボーナス増加量","{0}%","skill_3_1line_mojix6","un_1line_mojix6","CH_ASU_SUP_012","花鳥風月・花","花鳥風月・花","50秒間、ファーストアタックで
得られるボーナスポイントが
増加する。
覚醒で増加量が上昇。","ファーストアタックで得られるボーナスポイント量上昇","和装防具を入手したので早速
着てみたとのこと。祖父母家が
京都にあることもあってか、
舞踊の腕も見事の一言である。","slog_202000020","slog_202000020","Chara_icon/202000020","Chara_icon/202000020","2019/01/01",True,"1", -290000020,999,"《まねっこ舞踊家》ユイ",4,1,800,15000,100,1,0,0,0,"s290000020",,45,100,50,88,55,82,60,76,65,70,70,64,10,20,"耐久値回復量","{0}%","skill_3_1line_mojix6","un_1line_mojix6","CH_YUI_SUP_012","花鳥風月・鳥","花鳥風月・鳥","戦闘不能状態のメンバーを2人
復帰させ、レコードメダルの位置
を30秒間ミニマップに表示。
覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+レコードメダルサーチ","和装防具を入手したので早速
着てみたとのこと。アスナと
共に練習したという舞は、見る者
皆の心を温かくする。","slog_290000020","slog_290000020","Chara_icon/290000020","Chara_icon/290000020","2019/01/01",True,"1", -203000010,999,"《麗傘の武人》直葉",5,2,1600,20000,100,1,0,0,0,"s203000010",,30,210,34,202,38,194,40,190,42,186,44,182,5,10,"効果時間","{0}秒","skill_3_1line_mojix6","un_1line_mojix6","CH_LEA_SUP_012","花鳥風月・風","花鳥風月・風","リーグポイントの獲得量が
45%増加する。
覚醒で効果時間が延長。","リーグポイントの獲得量上昇","和装防具を入手したので早速
着てみたとのこと。竹刀を傘に
持ち替えて舞う姿は、勇猛で
ありながら美しい。
","slog_203000010","slog_203000010","Chara_icon/203000010","Chara_icon/203000010","2019/01/01",True,"1", -204000010,999,"《番傘の佳人》シノン",4,2,800,15000,100,1,0,0,0,"s204000010",,60,155,65,145,70,140,75,135,80,130,82,125,10,20,"ボーナス増加量","{0}%","skill_3_1line_mojix6","un_1line_mojix6","CH_SIN_SUP_012","花鳥風月・月","花鳥風月・月","30秒間、ファーストアタックで
得られるボーナスポイントが
増加する。
覚醒で増加量が上昇。","ファーストアタックで得られるボーナスポイント量上昇","和装防具を入手したので早速
着てみたとのこと。番傘を携え
優雅に佇む彼女。その美しさに
みなが目を奪われる。","slog_204000010","slog_204000010","Chara_icon/204000010","Chara_icon/204000010","2019/01/01",True,"1", -298000010,999,"《英雄の絆》キリト&アスナ",5,3,1600,20000,100,1,0,0,0,"s298000010",,60,165,70,151,80,137,85,130,90,123,95,116,10,20,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_SUP_SUP_001","トラスト・チャージ","トラスト・〇〇〇〇〇〇チャージ","前方へ非常に強力な剣撃を放ち、
敵に大ダメージを与える。
覚醒で攻撃力が上昇。","前方へ非常に強力な剣撃を放つ","攻略最前線において、互いを高め
あい絆を深めていった2人。その
絆の力こそが《SAO》をクリア
するための糧となった。","slog_298000010","slog_298000010","Chara_icon/298000010","Chara_icon/298000010","2019/01/01",True,"1", -298000020,999,"《天空の対影》キリト&リーファ",4,2,800,15000,100,1,0,0,0,"s298000020",,12,90,14,80,16,75,17,70,18,65,19,60,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_SUP_SUP_002","ベンチャーブースト","ベンチャー〇〇〇〇〇〇ブースト","レコードメダルとグリードの位置
をミニマップに表示、自パーティ
の移動速度を上昇させる。
覚醒で効果時間が延長。","レコードメダル&グリードサーチ発動+移動速度上昇","《ALO》において空を駆ける
2人に追いつける者は互いの他に
誰もいない。その姿は流星の如く
二つの影を残すのみ。","slog_298000020","slog_298000020","Chara_icon/298000020","Chara_icon/298000020","2019/01/01",True,"1", -298000030,999,"《戦閃の双撃》リズベット&シリカ",3,2,400,10000,100,3,0,0,0,"s298000030",,100,115,100,110,110,105,120,100,130,95,140,90,10,20,"STR上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_SUP_SUP_003","ネバー・ギブアップ!","ネバー・〇〇〇〇〇〇ギブアップ!","【獲得ポイントが3位の時のみ
発動可能】60秒間、自パーティ
のSTRを上昇させる。
覚醒で上昇量が増加。","【発動条件・3位】自パーティのSTR上昇","気が合う2人は行動を共にする
ことも多く、コンビネーションも
お手の物だ。《ALO》では仲良く
クエスト攻略に勤しんでいる。","slog_298000030","slog_298000030","Chara_icon/298000030","Chara_icon/298000030","2019/01/01",True,"1", -298000040,999,"《愛心の絆》ユウキ&アスナ",4,2,800,15000,100,1,0,0,0,"s298000040",,30,180,34,172,36,168,38,164,40,160,42,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_SUP_SUP_004","ロザリアハーモニー","ロザリア〇〇〇〇〇〇〇ハーモニー","リーグポイントの獲得量を
35%増加させる。
覚醒で効果時間が延長。","リーグポイントの獲得量上昇","2人が互いに対して抱く気持ちは
友情とも親愛とも一概には
言い切れない、何物にも代えられ
ない唯一無二の絆なのだろう。","slog_298000040","slog_298000040","Chara_icon/298000040","Chara_icon/298000040","2019/01/01",True,"1", -298000050,999,"《双麗の英剣》ストレア&フィリア",3,1,400,10000,100,1,0,0,0,"s298000050",,60,145,60,140,65,135,70,130,75,125,80,120,10,20,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"リライアンス・バディ","リライアンス〇〇〇〇〇〇・バディ","自パーティの耐久値と状態異常を
回復し、移動速度を30秒間上昇
させる。
覚醒で回復量が上昇。","自パーティの耐久値と状態異常を回復+移動速度上昇","キリトによって救われた彼女たち
は、今度はキリトのためにその
剣を振るう。今や無くてはなら
ない2人の仲間の勇姿だ。","slog_298000050","slog_298000050","Chara_icon/298000050","Chara_icon/298000050","2019/01/01",True,"1", -298000060,999,"《天響の歌声》セブン&レイン",3,3,400,10000,100,1,0,0,0,"s298000060",,2,125,2,122,3,119,3,116,4,113,4,110,1,2,"召喚体数","{0}体","skill_3_2line_moji86","un_2line_moji86",,"アイドリング・フラッシュ!","アイドリング・〇〇〇フラッシュ!","幻影グリードを召喚し他パーティ
の注意を引き、グリードの位置を
10秒間ミニマップに表示。
覚醒で幻影グリードの数が増加。","グリードの幻影を召喚+グリードサーチ発動","アイドル活動を行うセブンと、
アイドルを夢見るレイン。
夢を叶え共演する姉妹の歌声は
遠く高く、天までも響き渡る。","slog_298000060","slog_298000060","Chara_icon/298000060","Chara_icon/298000060","2019/01/01",True,"1", -299000010,999,"《騎士の矜持》ディアベル",2,2,200,5000,100,1,0,0,0,"s299000010",,40,110,40,104,50,104,50,98,60,92,65,86,10,20,"ボーナス増加量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"プライド・オブ・ナイト","プライド・〇〇〇〇〇オブ・ナイト","20秒間、ラストアタックで
得られるボーナスポイントが
増加する。
覚醒で増加量が上昇。","ラストアタックで得られるボーナスポイント量上昇","ジョブ制の無い《SAO》において
ナイトを名乗る青年。
そのカリスマ性で第1層クリアに
貢献し、大きな影響を残した。","slog_299000010","slog_299000010","Chara_icon/299000010","Chara_icon/299000010","2019/01/01",True,"1", -299000020,999,"《ナニワの継承者》キバオウ",2,3,200,5000,100,1,0,0,0,"s299000020",,20,120,20,114,25,114,25,108,30,102,30,96,5,10,"召喚増強レベル","{0}レベル","skill_3_2line_moji86","un_2line_moji86",,"ちょお待ってんか!","ちょお〇〇〇〇〇〇〇待ってんか!","アインクラッド解放隊員風のやや強い亜人系Mobを5体召喚し、
戦闘支援を受ける。覚醒で
召喚Mobのレベルが増強。","お助けMob【亜人系】を召喚","関西弁が目立つ《SAO》の名物プ
レイヤー。第1層で命を落とした
彼の矜持も受け継ぎ、アインク
ラッド低層の攻略に尽力した。","slog_299000020","slog_299000020","Chara_icon/299000020","Chara_icon/299000020","2019/01/01",True,"1", -299000030,999,"《鉄壁の聖騎士》ヒースクリフ",3,2,400,10000,100,1,0,0,0,"s299000030",,30,150,30,145,35,140,35,135,40,130,42,125,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"オブソリュート・ディフェンス","オブソリュート〇〇・ディフェンス","耐久値が50%を超えて減らなく
なる。既に50%以下ならば
それ以上減らなくなる。
覚醒で効果時間が延長。","スキル発動中、耐久値が5割を超えて減らなくなる","《SAO》最強との呼び声高い
攻略ギルド《血盟騎士団》の長。
ユニークスキル《神聖剣》を持ち
《聖騎士》の異名で呼ばれる。","slog_299000030","slog_299000030","Chara_icon/299000030","Chara_icon/299000030","2019/01/01",True,"1", -299000040,999,"《執念の護衛》クラディール",1,2,100,1500,100,1,0,0,0,"s299000040",,40,90,40,85,45,85,45,80,50,80,50,75,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"オーバーガーディアン","オーバー〇〇〇〇〇〇ガーディアン","自パーティのVITが
250%上昇する。
覚醒で効果時間が延長。
【アスナのみ効果が2倍】","【効果上昇・アスナ】自パーティのVIT上昇","ギルド《血盟騎士団》において
副団長であるアスナの護衛任務に
就いていた人物。そのアスナへの
執着心は危険なほど。","slog_299000040","slog_299000040","Chara_icon/299000040","Chara_icon/299000040","2019/01/01",True,"1", -299000050,999,"《老巧の釣り師》ニシダ",1,3,100,1500,100,1,0,0,0,"s299000050",,1,60,1,55,2,55,2,50,3,50,3,45,1,2,"ドロップ数","{0}個","skill_3_2line_moji86","un_2line_moji86",,"ベテランフィッシャー","ベテラン〇〇〇〇〇〇フィッシャー","ランダムで敵モンスターを1体
釣り上げる。
覚醒でスキルポーションの
ドロップ数が増加。","ランダムで敵モンスターを召喚","アインクラッド第22層コラル
郊外の湖を拠点に釣りを楽しむ
男性。《SAO》の回線保守責任者
で仕事の確認中に巻き込まれた。","slog_299000050","slog_299000050","Chara_icon/299000050","Chara_icon/299000050","2019/01/01",True,"1", -299000060,999,"《悪夢の扇動者》PoH",3,3,400,10000,100,3,0,0,0,"s299000060",,1,125,1,120,2,115,2,110,3,105,3,100,1,2,"ドロップ数","{0}個","skill_3_1line_mojix6","un_1line_mojix6",,"ショータイム","ショータイム","【獲得ポイントが3位の時のみ発動可能】グリードを2体召喚。
覚醒でスキルポーションの
ドロップ数が増加。","【発動条件・3位】グリードを召喚","《SAO》で暗躍する殺人ギルド
《ラフィン・コフィン》の
リーダー。他者を扇動し混乱を
巻き起こす手口を得意とする。","slog_299000060","slog_299000060","Chara_icon/299000060","Chara_icon/299000060","2019/01/01",True,"1", -299000070,999,"《赤眼の狂気》ザザ",1,3,100,1500,100,1,0,0,0,"s299000070",,1,90,1,85,2,85,2,80,3,80,3,75,1,2,"ドロップ数","{0}個","skill_3_2line_moji86","un_2line_moji86",,"コール・ミニオンズ","コール・〇〇〇〇〇〇ミニオンズ","ラフィン・コフィンの微力な
下位メンバーを5体召喚する。
覚醒でスキルポーションの
ドロップ数が増加。","敵Mob【ラフィン・コフィン】を召喚","《SAO》で暗躍する殺人ギルド
《ラフィン・コフィン》の幹部。
キリトに対して深い憎しみを抱く
エストック使い。","slog_299000070","slog_299000070","Chara_icon/299000070","Chara_icon/299000070","2019/01/01",True,"1", -299000080,999,"《タイタンズハンド》ロザリア",1,3,100,1500,100,1,0,0,0,"s299000080",,10,90,10,85,15,85,15,80,20,80,20,75,5,10,"召喚増強レベル","{0}レベル","skill_3_2line_moji86","un_2line_moji86",,"サポート・ミニオンズ","サポート・〇〇〇〇〇〇ミニオンズ","微力な獣人系Mobを5体召喚し
戦闘支援を受ける。
覚醒で召喚Mobのレベルが
増強。","お助けMob【獣人系】を召喚","《SAO》で犯罪に手を染めていた
オレンジギルド《タイタンズ
ハンド》のリーダー。キリトに
よって監獄エリアに送られた。","slog_299000080","slog_299000080","Chara_icon/299000080","Chara_icon/299000080","2019/01/01",True,"1", -299000090,999,"《聡明なる領主》サクヤ",1,1,100,1500,100,1,0,0,0,"s299000090",,15,75,15,70,20,70,20,65,25,65,25,60,10,20,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"ソリッドロード","ソリッドロード","戦闘不能状態のメンバーを1人
復帰させ、自パーティのVITを
30秒間250%上昇させる。
覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー1人復帰+自パーティのVIT上昇","《ALO》においてシルフ領を治め
ている領主。剣の名手でもあり、
プレイヤー間での信頼も厚い
名領主として知られる。","slog_299000090","slog_299000090","Chara_icon/299000090","Chara_icon/299000090","2019/01/01",True,"1", -299000100,999,"《朗らかな領主》アリシャ",1,3,100,1500,100,1,0,0,0,"s299000100",,10,90,10,85,15,85,15,80,20,80,20,75,5,10,"召喚増強レベル","{0}レベル","skill_3_2line_moji86","un_2line_moji86",,"ドラグーンコープス","ドラグーン〇〇〇〇〇〇コープス","やや強い飛竜系Mobを2体
召喚し、戦闘支援を受ける。
覚醒で召喚Mobのレベルが
増強。","お助けMob【飛竜系】を召喚","《ALO》においてケットシー領を
治めている領主。サクヤとは古い
友人。巨大な竜を操り戦う
《竜騎士隊》を率いる。","slog_299000100","slog_299000100","Chara_icon/299000100","Chara_icon/299000100","2019/01/01",True,"1", -299000110,999,"《炎熱の武者》ユージーン",1,3,100,1500,100,1,0,0,0,"s299000110",,30,85,30,80,35,80,35,75,40,75,40,70,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"エアリアルシフト","エアリアルシフト","周囲にいる敵モンスターの
VITを30%低下させる。
覚醒で効果時間が延長。","敵モンスターのVIT低下","《ALO》のサラマンダー領領主の
弟にして、全プレイヤー中最強と
目される男。伝説級武器《魔剣
グラム》の持ち主。","slog_299000110","slog_299000110","Chara_icon/299000110","Chara_icon/299000110","2019/01/01",True,"1", -299000120,999,"《ひたむきな慕情》レコン",1,3,100,1500,100,1,0,0,0,"s299000120",,10,35,10,33,12,33,12,31,14,31,14,29,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ブレイブリーステルス","ブレイブリー〇〇〇〇〇ステルス","攻撃を受けるまで敵に感知されず
他パーティのミニマップ上からも
見えなくなる。
覚醒で効果時間が延長。","攻撃を受けるまでハイディング状態","リーファの友人の少年。リアルで
は直葉のクラスメイトであり、
《ALO》を紹介した。彼女に淡い
想いを抱いているようだ。","slog_299000120","slog_299000120","Chara_icon/299000120","Chara_icon/299000120","2019/01/01",True,"1", -299000130,999,"《尊大な自尊心》シグルド",1,3,100,1500,100,1,0,0,0,"s299000130",,10,90,10,85,15,85,15,80,20,80,20,75,5,10,"召喚増強レベル","{0}レベル","skill_3_2line_moji86","un_2line_moji86",,"サポート・ミニオンズ","サポート・〇〇〇〇〇ミニオンズ","微力な骸霊系Mobを4体召喚し
戦闘支援を受ける。
覚醒で召喚Mobのレベルが
増強。","お助けMob【骸霊系】を召喚","リーファとレコンが一時期所属
していたパーティのメンバー。
シルフ領の幹部だったが、不正が
発覚し領地を追放された。","slog_299000130","slog_299000130","Chara_icon/299000130","Chara_icon/299000130","2019/01/01",True,"1", -299000140,999,"《仮初の妖精王》オベイロン",1,3,100,1500,100,1,0,0,0,"s299000140",,4,60,4,50,6,50,6,40,8,40,8,30,1,2,"召喚体数","{0}体","skill_3_2line_moji86","un_2line_moji86",,"ジャミングスラッグ","ジャミング〇〇〇〇〇〇スラッグ","微力な研究員Mobを他パーティ
それぞれのそばに召喚する。
覚醒で研究員Mobの召喚数が
増加。","敵Mob【研究員】を他パーティのそばに召喚","《ALO》のシンボルである世界樹
頂上で待ち構える妖精王。しかし
その実、《SAO生還者》を幽閉し
違法研究を行っていた。","slog_299000140","slog_299000140","Chara_icon/299000140","Chara_icon/299000140","2019/01/01",True,"1", -299000150,999,"《結ばれる縁》ルクス",2,2,200,5000,100,1,0,0,0,"s299000150",,40,100,40,94,45,88,45,82,50,76,52,70,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ディフェンシブ・ソード","ディフェンシブ〇〇〇〇〇・ソード","自パーティのVITを200%
上昇させるとともに、
状態異常を回復する。
覚醒で効果時間が延長。","自パーティのVIT上昇+状態異常を回復","シリカ達が《ALO》で出会った
少女。キリトに憧れ、片方の剣を
盾のように使用する変則的な二刀
流を扱う。","slog_299000150","slog_299000150","Chara_icon/299000150","Chara_icon/299000150","2019/01/01",True,"1", -299000160,999,"《誇り高き紫花》キズメル",1,2,100,1500,100,1,0,0,0,"s299000160",,30,75,30,70,35,70,35,65,40,65,40,60,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ダークエルフガーディアン","ダークエルフ〇〇〇〇ガーディアン","自パーティのVITが
300%上昇する。
覚醒で効果時間が延長。","自パーティのVIT上昇","キリトとアスナがアインクラッド
下層で冒険を共にしたダークエル
フ族の戦士。NPCだが、人間と
見紛うほど感情豊か。","slog_299000160","slog_299000160","Chara_icon/299000160","Chara_icon/299000160","2019/01/01",True,"1", -299000170,999,"《忍び寄る凶刺》死銃",3,3,400,10000,100,1,0,0,0,"s299000170",,12,90,12,85,14,80,14,75,15,70,16,65,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"メタマテリアルオプチカル・カモ","メタマテリアル〇オプチカル・カモ","敵に感知されず、他パーティの
ミニマップ上からも見えなくなり
移動速度も上昇する。
覚醒で効果時間が延長。","ハイディング+移動速度上昇","《GGO》に現れた殺人鬼。キリト
に並々ならぬ憎しみを抱く。
狙撃はもちろん、刺剣の扱いも
得意とする。","slog_299000170","slog_299000170","Chara_icon/299000170","Chara_icon/299000170","2019/01/01",True,"1", -299000180,999,"《氷檻の姫君》フレイヤ",2,3,200,5000,100,1,1,0,0,"s299000180",,70,120,70,114,65,114,65,108,60,102,58,96,-5,-10,"生育時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"レコード・ハーヴェスト","レコード・〇〇〇〇〇ハーヴェスト","種を蒔く。一定時間後、花が咲き
レコードメダルがランダムな量
ドロップする。
覚醒で生育時間が短縮。","レコードメダルが実る花の種を蒔く","《スリュムヘイム》で出会った
スリュムと敵対する姫。氷の檻に
囚われていたところをクラインが
助け、冒険を共にした。","slog_299000180","slog_299000180","Chara_icon/299000180","Chara_icon/299000180","2019/01/01",True,"1", -299000190,999,"《清涼な癒し》シウネー",2,1,200,5000,100,1,0,0,0,"s299000190",,35,30,35,28,40,28,40,26,45,24,50,22,10,20,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリーピー・ヒーリング","スリーピー・〇〇〇〇ヒーリング","自パーティの耐久値と状態異常を
回復する。
覚醒で回復量が上昇。
【ユウキのみ効果が2倍】","【効果上昇・ユウキ】耐久値と状態異常を回復","ユウキのギルド《スリーピング・
ナイツ》のお姉さん的存在。
パーティの中では後方援護役を
務めている。","slog_299000190","slog_299000190","Chara_icon/299000190","Chara_icon/299000190","2019/01/01",True,"1", -299000200,999,"《明影の速棍》ノリ&ジュン",2,2,200,5000,100,1,0,0,0,"s299000200",,6,40,6,39,8,39,8,38,10,37,11,36,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"スリーピー・ブースト","スリーピー・〇〇〇〇〇ブースト","スキルポーションとレコード
メダルの位置をミニマップに表示
する。覚醒で効果時間が延長。
【ユウキがいると効果時間2倍】","【効果上昇・ユウキ】スキル&メダルサーチ発動","ユウキのギルド《スリーピング・
ナイツ》のメンバー。スピードで
翻弄するノリとパワータイプの
ジュン。賑やかな2人。","slog_299000200","slog_299000200","Chara_icon/299000200","Chara_icon/299000200","2019/01/01",True,"1", -299000210,999,"《堅盾の鍛槍》テッチ&タルケン",2,2,200,5000,100,1,0,0,0,"s299000210",,35,80,35,75,40,75,40,70,45,65,47,60,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"スリーピー・ガーディアン","スリーピー・〇〇〇〇ガーディアン","自パーティのVITが
300%上昇する。
覚醒で効果時間が延長。
【ユウキのみ効果が2倍】","【効果上昇・ユウキ】自パーティのVIT上昇","ユウキのギルド《スリーピング・
ナイツ》のメンバー。中距離型の
タルケンとタンクのテッチ。堅実
さが伺える2人。","slog_299000210","slog_299000210","Chara_icon/299000210","Chara_icon/299000210","2019/01/01",True,"1", -299000220,999,"《華麗な歌姫》ユナ",3,3,400,10000,100,1,0,0,0,"s299000220",,20,135,20,129,25,123,30,117,32,111,34,105,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ライヴ・オン・ステージ!","ライヴ・オン・〇〇〇〇ステージ!","自パーティに敵の注目を集め、
一定範囲内の全パーティと全敵の
STRを100%上昇させる。
覚醒で効果時間が延長。","自パーティに敵の注目を集める+全ユニットのSTR上昇","《オーディナル・スケール》の
電脳アイドル。AIとは思えない
ほど感情豊かな仕草と歌声で
多くの人を魅了している。","slog_299000220","slog_299000220","Chara_icon/299000220","Chara_icon/299000220","2019/01/01",True,"1", -209000010,999,"《晴天の高揚》ユウキ",5,3,1600,20000,100,1,0,0,0,"s209000010",,45,210,48,204,51,201,53,198,55,196,57,194,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86_exa9to13","un_2line_moji86_exa9to13","CH_YUU_SUP_024","パッション・バレー!","パッション・〇〇〇〇〇〇バレー!","ビーチボールで思い切り
スマッシュする。周囲の敵に
非常に強力なダメージを与える。
覚醒で攻撃力が上昇。","ビーチボールにより強力な範囲攻撃を放つ","夏だ!海だ!ビーチバレーだ!
まばゆい日差しに心も弾み、
いてもたってもいられない様子。
楽しい夏はこれからだ!","slog_209000010","slog_209000010","Chara_icon/209000010","Chara_icon/209000010","2019/01/01",True,"1", -205000010,999,"《熱砂の輝き》リズベット",5,2,1600,20000,100,1,0,0,0,"s205000010",,70,210,76,196,82,182,88,175,91,168,94,161,6,12,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_LIS_SUP_024","パーフェクトサマー!","パーフェクト〇〇〇〇〇〇サマー!","自パーティのVITを450%
上昇させ、全属性への耐性も
付与される。
覚醒で効果時間が延長。","自パーティのVIT上昇+全属性耐性を付与","スポーティな水着を纏い海へと
繰り出せば、一面輝きに満ちた
青い水平線に白い砂浜。それに
いつメンが揃えば乙女は無敵!","slog_205000010","slog_205000010","Chara_icon/205000010","Chara_icon/205000010","2019/01/01",True,"1", -215000020,999,"《炎夏のひと時》ストレア",5,3,1600,20000,100,1,0,0,0,"s215000020",,30,190,35,182,40,174,44,170,46,166,48,162,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"サマーテンプテーション","サマー〇〇〇〇〇テンプテーション","敵の注目を集め、リーグポイント
獲得量が20%上昇。範囲内の
全ユニットのSTRも120%
上昇。覚醒で効果時間が延長。","敵からの注目&リーグポイント上昇+全ユニットSTR上昇","グラマラスな体を惜しげもなく
曝け出す彼女。無邪気な性格も
あいまって、無意識にビーチ中の
男性を虜にしてしまいそう。","slog_215000020","slog_215000020","Chara_icon/215000020","Chara_icon/215000020","2019/01/01",True,"1", -220000020,999,"《盛夏の思い出》サチ",5,1,1600,20000,100,1,0,0,0,"s220000020",,35,85,45,75,50,65,55,60,60,55,65,50,10,20,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"ワンサマー・メモリーズ","ワンサマー・〇〇〇〇〇メモリーズ","戦闘不能状態のメンバーを2人
復帰させ、自パーティのINTを
10秒間150%上昇させる。
覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+自パーティのINT上昇","いつしか一緒にいったあの夏の
砂浜。その思い出が色あせること
は無い。彼女の笑顔はいつまでも
眩しく、心に深く刻まれる。","slog_220000020","slog_220000020","Chara_icon/220000020","Chara_icon/220000020","2019/01/01",True,"1", -206000010,999,"《はじける笑顔》シリカ",4,3,800,15000,100,1,0,0,0,"s206000010",,60,100,68,90,76,85,84,80,92,75,96,70,15,30,"攻撃力上昇量","{0}%","skill_3_2line_moji96","un_2line_moji96","CH_SIL_SUP_024","ピナズ・サマーフラッシュ!","ピナズ・〇〇〇〇〇サマーフラッシュ!","ピナが現れ、6属性のブレスの内
ランダムで1つを放ち敵に強力な
ダメージを与える。
覚醒で攻撃力が上昇。","ピナが現れ、6属性のブレスの内ランダムで1つを放つ","いつも仲良しの2人は、もちろん
夏でも一緒!少女は水着で、小竜
はスカーフでおめかしモード。
とっても楽しいね、ピナ!","slog_206000010","slog_206000010","Chara_icon/206000010","Chara_icon/206000010","2019/01/01",True,"1", -201000020,999,"《真夏の遊戯》キリト",4,3,800,15000,100,1,0,0,0,"s201000020",,25,180,25,166,30,159,30,152,35,145,35,138,5,10,"召喚増強レベル","{0}レベル","skill_3_2line_moji86","un_2line_moji86","CH_KIR_SUP_024","ルーキーフィッシャー","ルーキー〇〇〇〇〇〇フィッシャー","ランダムで強力なお助けMobを
釣り上げる。
覚醒で召喚Mobのレベルが
増強。","ランダムでお助けMobを召喚","馴染みの片手剣を、今日は釣り竿
に持ち替え、大海原へ繰り出して
今こそ上達した釣りの腕を見せる
時!負けられない戦いがここに。","slog_201000020","slog_201000020","Chara_icon/201000020","Chara_icon/201000020","2019/01/01",True,"1", -216000020,999,"《納涼の誘い》フィリア",4,2,800,15000,100,1,0,0,0,"s216000020",,20,105,25,95,30,90,32,85,34,80,36,75,7,14,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"フレッシュブレイク","フレッシュ〇〇〇〇〇〇ブレイク","スキルEXPの獲得量を
50%増加させる。
覚醒で効果時間が延長。","スキルEXPの獲得量上昇","砂浜にきらめく純白のワンピース
に目を奪われれば、その誘いを
断れる者などいないだろう。共に
涼をとる心地よさは格別だ。","slog_216000020","slog_216000020","Chara_icon/216000020","Chara_icon/216000020","2019/01/01",True,"1", -290000030,999,"《暑熱の高鳴り》ユイ",4,3,800,15000,100,1,0,0,0,"s290000030",,35,140,45,128,55,122,60,116,65,110,70,104,10,20,"ボーナス増加量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_YUI_SUP_024","ファン・ファン・サマー!!","ファン・ファン・〇〇〇サマー!!","攻撃中のメンバーのスキルを
再使用可能にした後、30秒間
ラストアタックボーナスを増加
させる。覚醒で増加量が上昇。","クールタイムスキップ+ラストアタックボーナス上昇","初めて目の当たりにする海に、
胸の高鳴りがやまない様子。浮輪
も装備し準備は万端!皆で過ごす
夏は最高のものになるだろう。","slog_290000030","slog_290000030","Chara_icon/290000030","Chara_icon/290000030","2019/01/01",True,"1", -297000010,999,"《熱暑の漢達》エギル&クライン",3,3,400,10000,100,1,0,0,0,"s297000010",,6,150,6,143,7,136,7,129,8,122,8,115,1,2,"ドロップ数","{0}個","skill_3_2line_moji86","un_2line_moji86","CH_SUP_SUP_005","ゴールデン・クリッター","ゴールデン・〇〇〇〇〇クリッター","ハニースライムを5体召喚する。
覚醒でスキルポーションの
ドロップ数が増加。","敵Mob【ハニースライム】を召喚","最高の夏には最高の仲間がつき
もの。それなら彼らの誘いを断る
理由は無いだろう。全力で夏を
楽しまないなんて漢じゃねえぜ!","slog_297000010","slog_297000010","Chara_icon/297000010","Chara_icon/297000010","2019/01/01",True,"1", -297000020,999,"《水辺の戯れ》セブン&レイン",3,1,400,10000,100,1,0,0,0,"s297000020",,40,90,40,85,45,80,50,75,55,70,60,65,10,20,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"シスターズレスト","シスターズレスト","自パーティの耐久値と状態異常を
回復し、VITを30秒間250%
上昇させる。
覚醒で回復量が上昇。","耐久値と状態異常を回復+自パーティのVIT上昇","離れ離れだった時間を埋めるよう
に親密な時間を過ごす2人。
姉妹の屈託ない戯れは、周囲の者
までも笑顔にしてくれる。","slog_297000020","slog_297000020","Chara_icon/297000020","Chara_icon/297000020","2019/01/01",True,"1", -204000020,999,"《華夜の巧者》シノン",5,3,1600,20000,100,1,0,0,0,"s204000020",,45,210,48,204,51,201,53,198,55,196,57,194,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_SIN_SUP_025","アメイジング・シューティング","アメイジング・〇〇シューティング","射的ライフルで前方を複数回射撃
し、敵に非常に強力なダメージを
与える。
覚醒で攻撃力が上昇。","射的ライフルで前方を複数回射撃する","率先してはしゃぐタイプではない
彼女だが、夏祭りとなれば話は別
だ。お得意の狙撃技術が大活躍。
さ、お次は何が欲しい?","slog_204000020","slog_204000020","Chara_icon/204000020","Chara_icon/204000020","2019/01/01",True,"1", -202000030,999,"《一夏の華》アスナ",4,3,800,15000,100,1,0,0,0,"s202000030",,25,180,25,172,30,168,30,164,35,160,35,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_ASU_SUP_025","チャーミー・フラワー","チャーミー・〇〇〇〇〇〇フラワー","敵の注目を集め、リーグポイント
獲得量が15%上昇。範囲内の
全ユニットのSTRも100%
上昇。覚醒で効果時間が延長。","敵からの注目&リーグポイント上昇+全ユニットSTR上昇","浴衣姿に身を包んだ彼女は
まさに一輪の華の如く美しい。
夏にしか出会えない艶姿に
惑わされぬ者はいないだろう。","slog_202000030","slog_202000030","Chara_icon/202000030","Chara_icon/202000030","2019/01/01",True,"1", -203000020,999,"《華輝の妖精》リーファ",4,2,800,15000,100,1,0,0,0,"s203000020",,50,155,55,145,60,140,65,135,70,130,72,125,10,20,"ボーナス増加量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_LEA_SUP_025","フェアリーグリッター","フェアリー〇〇〇〇〇〇グリッター","40秒間、ファーストアタックで
得られるボーナスポイントが
増加する。
覚醒で増加量が上昇。","ファーストアタックで得られるボーナスポイント量上昇","夜空に咲く大輪の華と、星々の
きらめき。それらにも負けぬ輝き
を放つ夏の妖精は、無邪気な笑顔
を見せてくれる。","slog_203000020","slog_203000020","Chara_icon/203000020","Chara_icon/203000020","2019/01/01",True,"1", -209000020,999,"《真夏の憧憬》ユウキ",4,1,800,15000,100,1,0,0,0,"s209000020",,30,90,35,80,40,75,45,70,50,65,55,60,10,20,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_YUU_SUP_025","ファストトリップサマーナイト","ファストトリップ〇〇サマーナイト","戦闘不能状態のメンバーを2人
復帰させ、自パーティの移動速度
を10秒間上昇させる。
覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+移動速度上昇","憧れの屋台に、ついついあれも
これもと手が伸びる。今は提灯に
照らされ煌めく真っ赤な甘味に
ご執心のようだ。","slog_209000020","slog_209000020","Chara_icon/209000020","Chara_icon/209000020","2019/01/01",True,"1", -298000070,999,"《犀薔薇の誓い》アリス&ユージオ",5,3,1600,20000,100,1,0,0,0,"s298000070",,65,210,68,204,71,201,73,198,75,196,77,194,10,20,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_SUP_SUP_006","セイクリッド・ディザイア","セイクリッド・〇〇〇〇ディザイア","2つの神器で非常に強力な広範囲
攻撃を放ち、敵にダメージと
氷咲状態&VIT・INT低下を
与える。覚醒で攻撃力上昇。","強力な広範囲攻撃+氷咲&VIT・INT低下を付与","少年は少女のために。
少女は全ての民のために。
己の守るべきものを信じ
彼らはその剣を振るう。","slog_298000070","slog_298000070","Chara_icon/298000070","Chara_icon/298000070","2019/01/01",True,"1", -298000080,999,"《SAOゲーム攻略会議2019》APKZ",5,2,1600,20000,100,1,0,0,0,"s298000080",,20,185,22,177,24,169,26,165,27,161,28,157,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"アピール・カズー","〇〇アピール・〇〇〇〇〇カズー","自パーティのSTRを100%、
リーグポイント獲得量を25%、
移動速度も上昇させる。
覚醒で効果時間が延長。","自パーティのSTR&リーグポイント獲得量&移動速度上昇","攻略会議に集ったアスナ、プレミ
ア、クレハ、ツェリスカの4人。
堅実かつ大胆な作戦を立案して
皆を勝利に導くことだろう。","slog_298000080","slog_298000080","Chara_icon/298000080","Chara_icon/298000080","2019/01/01",False,"1", -298000090,999,"《SAOゲーム攻略会議2019》SPRS",5,3,1600,20000,100,1,0,0,0,"s298000090",,25,185,29,177,33,169,36,165,38,161,40,157,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"スペシャル・ライジング","スペシャル・〇〇〇ライジング","敵の注目を集め、移動速度が上昇
する。範囲内の全ユニットの
STRも150%上昇させる。
覚醒で効果時間が延長。","敵からの注目&移動速度上昇+全ユニットSTR上昇","攻略会議に集ったストレア、フィ
リア、レイン、セブンの4人。
みんなを盛り上げ士気を高め、勝
利に貢献してくれることだろう。","slog_298000090","slog_298000090","Chara_icon/298000090","Chara_icon/298000090","2019/01/01",False,"1", -299000230,999,"《SWORD ART ONLINE》キリト",4,2,800,15000,100,1,0,0,0,"sSaoDot",,50,180,55,172,60,168,62,164,64,160,66,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ドット・アドベンチャー","ドット・〇〇〇〇アドベンチャー","自パーティのVIT・INTを
70%上昇させ、ラストアタック
ボーナスも50%増加させる。
覚醒で効果時間が延長。","自パーティのVIT・INT&ラストアタックボーナス上昇","攻略会議の最中、ドットで描かれ
た英雄達の記録。かの世界を駆け
抜けた彼らの記憶は、永久にここ
に刻まれるであろう。","slog_299000230","slog_299000230","Chara_icon/299000230","Chara_icon/299000230","2019/01/01",True,"1", -299000240,999,"《SWORD ART ONLINE》アスナ",4,2,800,15000,100,1,0,0,0,"sSaoDot",,50,180,55,172,60,168,62,164,64,160,66,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ドット・アドベンチャー","ドット・〇〇〇〇アドベンチャー","自パーティのVIT・INTを
70%上昇させ、ラストアタック
ボーナスも50%増加させる。
覚醒で効果時間が延長。","自パーティのVIT・INT&ラストアタックボーナス上昇","攻略会議の最中、ドットで描かれ
た英雄達の記録。かの世界を駆け
抜けた彼らの記憶は、永久にここ
に刻まれるであろう。","slog_299000240","slog_299000240","Chara_icon/299000240","Chara_icon/299000240","2019/01/01",True,"1", -299000250,999,"《SWORD ART ONLINE》リーファ",4,2,800,15000,100,1,0,0,0,"sSaoDot",,50,180,55,172,60,168,62,164,64,160,66,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ドット・アドベンチャー","ドット・〇〇〇〇アドベンチャー","自パーティのVIT・INTを
70%上昇させ、ラストアタック
ボーナスも50%増加させる。
覚醒で効果時間が延長。","自パーティのVIT・INT&ラストアタックボーナス上昇","攻略会議の最中、ドットで描かれ
た英雄達の記録。かの世界を駆け
抜けた彼らの記憶は、永久にここ
に刻まれるであろう。","slog_299000250","slog_299000250","Chara_icon/299000250","Chara_icon/299000250","2019/01/01",True,"1", -299000260,999,"《SWORD ART ONLINE》シノン",4,2,800,15000,100,1,0,0,0,"sSaoDot",,50,180,55,172,60,168,62,164,64,160,66,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ドット・アドベンチャー","ドット・〇〇〇〇アドベンチャー","自パーティのVIT・INTを
70%上昇させ、ラストアタック
ボーナスも50%増加させる。
覚醒で効果時間が延長。","自パーティのVIT・INT&ラストアタックボーナス上昇","攻略会議の最中、ドットで描かれ
た英雄達の記録。かの世界を駆け
抜けた彼らの記憶は、永久にここ
に刻まれるであろう。","slog_299000260","slog_299000260","Chara_icon/299000260","Chara_icon/299000260","2019/01/01",True,"1", -299000270,999,"《SWORD ART ONLINE》リズベット",4,2,800,15000,100,1,0,0,0,"sSaoDot",,50,180,55,172,60,168,62,164,64,160,66,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ドット・アドベンチャー","ドット・〇〇〇〇アドベンチャー","自パーティのVIT・INTを
70%上昇させ、ラストアタック
ボーナスも50%増加させる。
覚醒で効果時間が延長。","自パーティのVIT・INT&ラストアタックボーナス上昇","攻略会議の最中、ドットで描かれ
た英雄達の記録。かの世界を駆け
抜けた彼らの記憶は、永久にここ
に刻まれるであろう。","slog_299000270","slog_299000270","Chara_icon/299000270","Chara_icon/299000270","2019/01/01",True,"1", -299000280,999,"《SWORD ART ONLINE》シリカ",4,2,800,15000,100,1,0,0,0,"sSaoDot",,50,180,55,172,60,168,62,164,64,160,66,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ドット・アドベンチャー","ドット・〇〇〇〇アドベンチャー","自パーティのVIT・INTを
70%上昇させ、ラストアタック
ボーナスも50%増加させる。
覚醒で効果時間が延長。","自パーティのVIT・INT&ラストアタックボーナス上昇","攻略会議の最中、ドットで描かれ
た英雄達の記録。かの世界を駆け
抜けた彼らの記憶は、永久にここ
に刻まれるであろう。","slog_299000280","slog_299000280","Chara_icon/299000280","Chara_icon/299000280","2019/01/01",True,"1", -299000290,999,"《SWORD ART ONLINE》ユウキ",4,2,800,15000,100,1,0,0,0,"sSaoDot",,50,180,55,172,60,168,62,164,64,160,66,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ドット・アドベンチャー","ドット・〇〇〇〇アドベンチャー","自パーティのVIT・INTを
70%上昇させ、ラストアタック
ボーナスも50%増加させる。
覚醒で効果時間が延長。","自パーティのVIT・INT&ラストアタックボーナス上昇","攻略会議の最中、ドットで描かれ
た英雄達の記録。かの世界を駆け
抜けた彼らの記憶は、永久にここ
に刻まれるであろう。","slog_299000290","slog_299000290","Chara_icon/299000290","Chara_icon/299000290","2019/01/01",True,"1", -299000300,999,"《SWORD ART ONLINE》アリス",4,2,800,15000,100,1,0,0,0,"sSaoDot",,50,180,55,172,60,168,62,164,64,160,66,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ドット・アドベンチャー","ドット・〇〇〇〇アドベンチャー","自パーティのVIT・INTを
70%上昇させ、ラストアタック
ボーナスも50%増加させる。
覚醒で効果時間が延長。","自パーティのVIT・INT&ラストアタックボーナス上昇","攻略会議の最中、ドットで描かれ
た英雄達の記録。かの世界を駆け
抜けた彼らの記憶は、永久にここ
に刻まれるであろう。","slog_299000300","slog_299000300","Chara_icon/299000300","Chara_icon/299000300","2019/01/01",True,"1", -299000310,999,"《SWORD ART ONLINE》ユージオ",4,2,800,15000,100,1,0,0,0,"sSaoDot",,50,180,55,172,60,168,62,164,64,160,66,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ドット・アドベンチャー","ドット・〇〇〇〇アドベンチャー","自パーティのVIT・INTを
70%上昇させ、ラストアタック
ボーナスも50%増加させる。
覚醒で効果時間が延長。","自パーティのVIT・INT&ラストアタックボーナス上昇","攻略会議の最中、ドットで描かれ
た英雄達の記録。かの世界を駆け
抜けた彼らの記憶は、永久にここ
に刻まれるであろう。","slog_299000310","slog_299000310","Chara_icon/299000310","Chara_icon/299000310","2019/01/01",True,"1", -299000320,999,"《SWORD ART ONLINE》ストレア",4,2,800,15000,100,1,0,0,0,"sSaoDot",,50,180,55,172,60,168,62,164,64,160,66,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ドット・アドベンチャー","ドット・〇〇〇〇アドベンチャー","自パーティのVIT・INTを
70%上昇させ、ラストアタック
ボーナスも50%増加させる。
覚醒で効果時間が延長。","自パーティのVIT・INT&ラストアタックボーナス上昇","攻略会議の最中、ドットで描かれ
た英雄達の記録。かの世界を駆け
抜けた彼らの記憶は、永久にここ
に刻まれるであろう。","slog_299000320","slog_299000320","Chara_icon/299000320","Chara_icon/299000320","2019/01/01",True,"1", -299000330,999,"《SWORD ART ONLINE》フィリア",4,2,800,15000,100,1,0,0,0,"sSaoDot",,50,180,55,172,60,168,62,164,64,160,66,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ドット・アドベンチャー","ドット・〇〇〇〇アドベンチャー","自パーティのVIT・INTを
70%上昇させ、ラストアタック
ボーナスも50%増加させる。
覚醒で効果時間が延長。","自パーティのVIT・INT&ラストアタックボーナス上昇","攻略会議の最中、ドットで描かれ
た英雄達の記録。かの世界を駆け
抜けた彼らの記憶は、永久にここ
に刻まれるであろう。","slog_299000330","slog_299000330","Chara_icon/299000330","Chara_icon/299000330","2019/01/01",True,"1", -299000340,999,"《SWORD ART ONLINE》セブン",4,2,800,15000,100,1,0,0,0,"sSaoDot",,50,180,55,172,60,168,62,164,64,160,66,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ドット・アドベンチャー","ドット・〇〇〇〇アドベンチャー","自パーティのVIT・INTを
70%上昇させ、ラストアタック
ボーナスも50%増加させる。
覚醒で効果時間が延長。","自パーティのVIT・INT&ラストアタックボーナス上昇","攻略会議の最中、ドットで描かれ
た英雄達の記録。かの世界を駆け
抜けた彼らの記憶は、永久にここ
に刻まれるであろう。","slog_299000340","slog_299000340","Chara_icon/299000340","Chara_icon/299000340","2019/01/01",True,"1", -299000350,999,"《SWORD ART ONLINE》レイン",4,2,800,15000,100,1,0,0,0,"sSaoDot",,50,180,55,172,60,168,62,164,64,160,66,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ドット・アドベンチャー","ドット・〇〇〇〇アドベンチャー","自パーティのVIT・INTを
70%上昇させ、ラストアタック
ボーナスも50%増加させる。
覚醒で効果時間が延長。","自パーティのVIT・INT&ラストアタックボーナス上昇","攻略会議の最中、ドットで描かれ
た英雄達の記録。かの世界を駆け
抜けた彼らの記憶は、永久にここ
に刻まれるであろう。","slog_299000350","slog_299000350","Chara_icon/299000350","Chara_icon/299000350","2019/01/01",True,"1", -299000360,999,"《SWORD ART ONLINE》プレミア",4,2,800,15000,100,1,0,0,0,"sSaoDot",,50,180,55,172,60,168,62,164,64,160,66,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ドット・アドベンチャー","ドット・〇〇〇〇アドベンチャー","自パーティのVIT・INTを
70%上昇させ、ラストアタック
ボーナスも50%増加させる。
覚醒で効果時間が延長。","自パーティのVIT・INT&ラストアタックボーナス上昇","攻略会議の最中、ドットで描かれ
た英雄達の記録。かの世界を駆け
抜けた彼らの記憶は、永久にここ
に刻まれるであろう。","slog_299000360","slog_299000360","Chara_icon/299000360","Chara_icon/299000360","2019/01/01",True,"1", -299000370,999,"《SWORD ART ONLINE》イツキ",4,2,800,15000,100,1,0,0,0,"sSaoDot",,50,180,55,172,60,168,62,164,64,160,66,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ドット・アドベンチャー","ドット・〇〇〇〇アドベンチャー","自パーティのVIT・INTを
70%上昇させ、ラストアタック
ボーナスも50%増加させる。
覚醒で効果時間が延長。","自パーティのVIT・INT&ラストアタックボーナス上昇","攻略会議の最中、ドットで描かれ
た英雄達の記録。かの世界を駆け
抜けた彼らの記憶は、永久にここ
に刻まれるであろう。","slog_299000370","slog_299000370","Chara_icon/299000370","Chara_icon/299000370","2019/01/01",True,"1", -299000380,999,"《SWORD ART ONLINE》ツェリスカ",4,2,800,15000,100,1,0,0,0,"sSaoDot",,50,180,55,172,60,168,62,164,64,160,66,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ドット・アドベンチャー","ドット・〇〇〇〇アドベンチャー","自パーティのVIT・INTを
70%上昇させ、ラストアタック
ボーナスも50%増加させる。
覚醒で効果時間が延長。","自パーティのVIT・INT&ラストアタックボーナス上昇","攻略会議の最中、ドットで描かれ
た英雄達の記録。かの世界を駆け
抜けた彼らの記憶は、永久にここ
に刻まれるであろう。","slog_299000380","slog_299000380","Chara_icon/299000380","Chara_icon/299000380","2019/01/01",True,"1", -299000390,999,"《SWORD ART ONLINE》クレハ",4,2,800,15000,100,1,0,0,0,"sSaoDot",,50,180,55,172,60,168,62,164,64,160,66,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ドット・アドベンチャー","ドット・〇〇〇〇アドベンチャー","自パーティのVIT・INTを
70%上昇させ、ラストアタック
ボーナスも50%増加させる。
覚醒で効果時間が延長。","自パーティのVIT・INT&ラストアタックボーナス上昇","攻略会議の最中、ドットで描かれ
た英雄達の記録。かの世界を駆け
抜けた彼らの記憶は、永久にここ
に刻まれるであろう。","slog_299000390","slog_299000390","Chara_icon/299000390","Chara_icon/299000390","2019/01/01",True,"1", -299000400,999,"《SWORD ART ONLINE》メディナ",4,2,800,15000,100,1,0,0,0,"sSaoDot",,50,180,55,172,60,168,62,164,64,160,66,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ドット・アドベンチャー","ドット・〇〇〇〇アドベンチャー","自パーティのVIT・INTを
70%上昇させ、ラストアタック
ボーナスも50%増加させる。
覚醒で効果時間が延長。","自パーティのVIT・INT&ラストアタックボーナス上昇","攻略会議の最中、ドットで描かれ
た英雄達の記録。かの世界を駆け
抜けた彼らの記憶は、永久にここ
に刻まれるであろう。","slog_299000400","slog_299000400","Chara_icon/299000400","Chara_icon/299000400","2019/01/01",True,"1", -202000040,999,"《ドリーミィ・バニー》アスナ",5,3,1600,20000,100,1,0,0,0,"s202000040",,20,185,23,177,26,169,28,165,29,161,30,157,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_ASU_SUP_037","ジャックポット・ドリーム","ジャックポット・〇ドリーム","レコードメダルを再配置。メダル
の位置をミニマップに表示し、
リーグポイント獲得量も30%
増加。覚醒で効果時間が延長。","メダル再配置+サーチ発動+リーグポイント獲得量上昇","満月の元、夢を運ぶ彼女の笑顔は
多くの人を癒してくれることだろ
う。麗しき娯楽のひと時をお楽し
みあれ。","slog_202000040","slog_202000040","Chara_icon/202000040","Chara_icon/202000040","2019/01/01",True,"1", -203000030,999,"《ファイン・サービング》直葉",4,3,800,15000,100,1,0,0,0,"s203000030",,1,90,1,85,2,85,2,80,3,80,3,75,1,2,"ドロップ数","{0}個","skill_3_2line_moji86","un_2line_moji86","CH_LEA_SUP_037","ラビッシュ・パーティ","ラビッシュ・〇〇〇〇パーティ","ランダムで敵モンスターを
2~3体召喚する。
覚醒でのスキルポーションの
ドロップ数が増加。","ランダムで敵モンスターを召喚","勝負に夢中になるあまり、グラス
が空になってしまっても大丈夫。
気遣い上手な彼女が、すぐにお代
わりを注いでくれるはずだ。","slog_203000030","slog_203000030","Chara_icon/203000030","Chara_icon/203000030","2019/01/01",True,"1", -209000030,999,"《ベット・タイム!》ユウキ",4,2,800,15000,100,1,0,0,0,"s209000030",,30,165,38,157,42,153,46,149,50,145,54,141,5,10,"ボーナス増加量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_YUU_SUP_037","ベットオンボーナス","ベットオン〇〇〇〇〇ボーナス","40秒間、ファーストアタックと
ラストアタックで得られる
ボーナスポイントが増加する。
覚醒で増加量が上昇。","ファースト&ラストアタックのボーナスポイント量上昇","さあ、準備はいいかな!
高らかな宣言と共に、誰もが夢を
握りしめる。最も胸高鳴る瞬間で
ある。","slog_209000030","slog_209000030","Chara_icon/209000030","Chara_icon/209000030","2019/01/01",True,"1", -215000030,999,"《ベスト・サービス》ストレア",4,2,800,15000,100,1,0,0,0,"s215000030",,30,160,34,152,36,148,38,144,40,140,42,136,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"エンジョイスプリー","エンジョイ〇〇〇〇スプリー","リーグポイント獲得量が
30%増加する。
覚醒で効果時間が延長。","リーグポイントの獲得量上昇","心躍らせる者あれば、夢破れる者
もある。美しき彼女の微笑は、
さながら美酒のように皆を等しく
酔わせ、惑わせる。","slog_215000030","slog_215000030","Chara_icon/215000030","Chara_icon/215000030","2019/01/01",True,"1", -290000040,999,"《スウィートウィッチ》ユイ",5,3,1600,20000,100,1,0,0,0,"s290000040",,20,165,22,151,24,137,26,130,27,123,28,116,3,6,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_YUI_SUP_027","スウィートハロウィーン","スウィート〇〇〇〇ハロウィーン","一定範囲内の敵の6属性全ての
耐性を無効化し、封印状態を
付与する。覚醒で属性耐性無効化
の効果時間が延長。","敵の6属性耐性を無効化+一定時間封印状態を付与","いつもはとっても良い子な彼女も
今日は悪戯魔女っ子に大変身。
沢山のお菓子に囲まれて
甘い甘~い夢が見られそう。","slog_290000040","slog_290000040","Chara_icon/290000040","Chara_icon/290000040","2019/01/01",True,"1", -204000030,999,"《クレバーソーサレス》シノン",4,2,800,15000,100,1,0,0,0,"s204000030",,10,150,12,140,14,135,15,130,16,125,17,120,2,4,"効果時間","{0}秒","skill_3_2line_moji86_exa4","un_2line_moji86_exa4","CH_SIN_SUP_027","ナイトメア・マジック","ナイトメア・〇〇〇マジック","ファーストアタックで得られる
ボーナスポイントが100%
増加し、移動速度も上昇する。
覚醒で効果時間が延長。","ファーストアタックのボーナスポイント量&移動速度上昇","普段はクールな彼女も、今日は
特別。魔女に扮して街へ繰り出す
足取りは、心なしか弾んでいる
ようだ。","slog_204000030","slog_204000030","Chara_icon/204000030","Chara_icon/204000030","2019/01/01",True,"1", -205000020,999,"《マジカルスミス》リズベット",4,3,800,15000,100,1,0,0,0,"s205000020",,60,165,65,151,70,144,72,137,74,130,76,123,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_LIS_SUP_027","マジカルエンチャント","マジカル〇〇〇〇〇エンチャント","一定範囲内の敵のSTR・INTを
30%低下させ、自パーティの
STR・INTを50%上昇させる。
覚醒で効果時間が延長。","敵にSTR・INT低下を付与+自パーティのSTR・INT上昇","甘~い悪戯を考えるのも、ハロ
ウィンの醍醐味のひとつ。彼女も
楽し気に思案中だ。さーて、どん
なことをしちゃおうかしら?","slog_205000020","slog_205000020","Chara_icon/205000020","Chara_icon/205000020","2019/01/01",True,"1", -206000020,999,"《キャットモンスター》シリカ",4,3,800,15000,100,1,0,0,0,"s206000020",,40,165,50,151,55,144,60,137,65,130,70,123,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86_exa9to10","un_2line_moji86_exa9to10","CH_SIL_SUP_027","プランク・パンプキン","プランク・〇〇〇〇パンプキン","無数のカボチャを降らせて強力な
広範囲攻撃を行い、敵にダメージ
と麻痺状態を与える。
覚醒で攻撃力が上昇。","広範囲攻撃+麻痺を付与","キュートな猫耳衣装に身を包んだ
彼女も、今日は悪戯な笑みを浮か
べる。可愛いからって油断して
いると痛い目見ちゃうかも?","slog_206000020","slog_206000020","Chara_icon/206000020","Chara_icon/206000020","2019/01/01",True,"1", -201000030,999,"《ヒミツの魔法少女》キリト",5,3,1600,20000,100,1,0,0,0,"s201000030",,45,165,55,151,65,137,70,130,75,123,80,116,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_KIR_SUP_017","マジカルバーストストリーム","マジカルバースト〇〇ストリーム","星を降らせて強力な広範囲攻撃を
行うとともに、自パーティの移動
速度を18秒間上昇させる。
覚醒で攻撃力が上昇。","広範囲攻撃+自パーティの移動速度上昇","危険を顧みず夜な夜な悪と戦う
彼女(!?)には超特大の秘密が
あった。絶対バレるわけにはいか
ぬのだ!世界を救うその日まで!","slog_201000030","slog_201000030","Chara_icon/201000030","Chara_icon/201000030","2019/01/01",True,"1", -202000050,999,"《愛情の魔法少女》アスナ",4,3,800,15000,100,1,0,0,0,"s202000050",,10,155,12,145,14,140,15,135,16,130,17,125,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_ASU_SUP_017","マジカルラブリー・パワー","マジカル〇〇〇〇ラブリー・パワー","グリードからの注目を集め、
リーグポイント獲得量が
15%上昇する。
覚醒で効果時間が延長。","グリードからの注目&リーグポイント獲得量上昇","普段は超優等生のお嬢様な彼女。
その正体は、なんと悪と戦う魔法
少女だった――!今宵もどこかで
愛の鉄槌が炸裂する。","slog_202000050","slog_202000050","Chara_icon/202000050","Chara_icon/202000050","2019/01/01",True,"1", -206000030,999,"《幼猫の魔法少女》シリカ",5,1,1600,20000,100,1,0,0,0,"s206000030",,45,180,50,166,55,152,57,145,59,138,61,131,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_SIL_SUP_017","にゃんにゃんドリーム","にゃんにゃん〇〇〇〇ドリーム","触れると戦闘不能状態、
状態異常、耐久値を回復する
ヒールスポットを設置する。
覚醒で設置時間が延長。","ヒールスポットを設置","見習い魔法少女のシリカは今日も
ドジばかり。愛猫《ピナ》の力も
借りて、今宵こそはと悪と相対す
るが、果たして――!?","slog_206000030","slog_206000030","Chara_icon/206000030","Chara_icon/206000030","2019/01/01",True,"1", -209000040,999,"《快活の魔法少女》ユウキ",4,2,800,15000,100,1,0,0,0,"s209000040",,60,165,65,151,70,144,72,137,74,130,76,123,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_YUU_SUP_017","キャットスターイリュージョン","キャットスター〇イリュージョン","一定範囲内の敵の物理耐性を無効
化させるとともに、自パーティの
STRを60%上昇させる。
覚醒で効果時間が延長。","敵の物理耐性を無効化+自パーティのSTR上昇","いつでも溌剌と悪に立ち向かう
彼女。その元気さとは裏腹に、
時折寂しげな表情を見せる。彼女
の抱えた秘密とは――?","slog_209000040","slog_209000040","Chara_icon/209000040","Chara_icon/209000040","2019/01/01",True,"1", -211000010,999,"《在りし日の宝物》アリス",5,3,1600,20000,100,1,0,0,0,"s211000010",,30,80,40,78,50,76,60,74,70,72,80,70,10,20,"メダル増加量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_ALI_SUP_007","ブリリアント・メモリーズ","ブリリアント・〇〇メモリーズ","一定範囲内にいるグリードを
メダル排出量の多いゴールデン・
グリードに転成させる。
覚醒でメダル排出量が増加。","グリードをゴールデン・グリードに転成","陽光に髪をきらめかせ、手作りの
昼食を勧める可憐な少女。
これは、いつか、どこかにあった
はずの大切な日常。","slog_211000010","slog_211000010","Chara_icon/211000010","Chara_icon/211000010","2019/01/01",True,"1", -212000010,999,"《在りし日の親友》ユージオ",4,3,800,15000,100,1,0,15,0,"s212000010",,20,140,25,130,30,125,32,120,34,115,36,110,5,10,"効果時間","{0}秒","skill_3_2line_moji86_exb4","un_2line_moji86_exb4","CH_EUG_SUP_007","プレザント・メモリーズ","プレザント・〇〇〇〇メモリーズ","自パーティのSTRを100%上昇
させる。覚醒で効果時間が延長。
【パッシブ】アダプタブル出現率
が上昇。重複で効果増。","自パーティのSTR上昇","暴走気味のあの子に、いつものよ
うに手を差し伸べる。けれどこの
手を握った相手は一体誰だったか
――大切な人だったはずなのに。","slog_212000010","slog_212000010","Chara_icon/212000010","Chara_icon/212000010","2019/01/01",True,"1", -297000050,999,"《竹馬の友》キリト&ユージオ",5,2,1600,20000,100,1,0,0,0,"s297000050",,160,130,170,120,180,110,190,105,195,100,200,95,20,40,"STR上昇量","{0}%","skill_3_2line_moji86_exa15","un_2line_moji86_exa15","CH_SUP_SUP_009","スティミュラス・ライバル","スティミュラス・〇〇〇ライバル","15秒間、自パーティの
STR・移動速度が上昇する。
覚醒でSTR上昇量が増加。","自パーティのSTR&移動速度上昇","活発なキリトと穏やかなユージオ
の相性は抜群と言っていい。好奇
心旺盛な2人は今日も競い合う
ように駆けてゆく。","slog_297000050","slog_297000050","Chara_icon/297000050","Chara_icon/297000050","2019/01/01",True,"1", -297000060,999,"《協同調和》アリス&アスナ",4,3,800,15000,100,1,0,0,0,"s297000060",,25,120,35,110,40,105,45,100,50,95,55,90,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_SUP_SUP_011","ハーモニアス・コーポレーション","ハーモニアス・〇コーポレーション","一定時間、自身の周囲に
心意の剣を召喚する広範囲攻撃。
敵に複数回ダメージを与える。
覚醒で攻撃力が上昇。","一定時間継続する広範囲攻撃","Unknownでないどこかの世界で、
彼を巡って対立した2人だったが
まずは目の前の戦から。本当の
戦いは全てに片がついてからだ。","slog_297000060","slog_297000060","Chara_icon/297000060","Chara_icon/297000060","2019/01/01",True,"1", -297000030,999,"《永久の約束》キリト&アスナ&ユイ",5,3,1600,20000,100,1,1,0,0,"s297000030",,50,100,47,94,44,88,41,85,39,82,38,80,-5,-10,"生育時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_SUP_SUP_007","ナーチャー・ラブ","ナーチャー・ラブ","ミニマップにメダルの位置を表示
し、種を蒔く。一定時間後、開花
しメダルがランダムな量ドロップ
する。覚醒で生育時間が短縮。","レコードメダルが実る花の種を蒔く+メダルサーチ発動","《SAO》で出会った彼らだが、心
の深い所で結びついた本物の親子
だと言える。変わらずずっと、
3人で一緒に。淡い永遠の約束。","slog_297000030","slog_297000030","Chara_icon/297000030","Chara_icon/297000030","2019/01/01",True,"1", -202000060,999,"《優艶な幼な妻》アスナ",4,3,800,15000,100,1,0,0,0,"s202000060",,25,180,30,170,33,160,36,155,38,150,40,145,5,10,"召喚増強レベル","{0}レベル","skill_3_2line_moji86","un_2line_moji86",,"チャーミーブライドガール","チャーミー〇〇〇ブライドガール","強力な飛竜系Mobを2体召喚し
戦闘支援を受ける。
覚醒で召喚Mobのレベルが
増強。","お助けMob【飛竜系】を召喚","《SAO》における結婚生活は短く
も甘く、幸福な時間だった。
甘えるのが苦手だった彼女も、
ここでは素直に頬を赤らめる。","slog_202000060","slog_202000060","Chara_icon/202000060","Chara_icon/202000060","2019/01/01",True,"1", -290000050,999,"《光彩の加護》ユイ",4,3,800,15000,100,1,0,0,0,"s290000050",,60,210,66,200,72,195,75,190,78,185,81,180,6,12,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_YUI_SUP_002","ブレス・マイ・ファミリー","ブレス・マイ・〇〇〇ファミリー","攻撃を受けるまで、一定時間毎に
リーグポイントを獲得する状態に
なる。
覚醒で効果時間が延長。","敵の攻撃を受けるまでリーグポイント自動獲得状態","2人の英雄の心を支えたのは、
彼らの娘の存在だった。彼女の
エールを胸に、一刻も早い再会を
夢見て、2人は再び立ち上がる。","slog_290000050","slog_290000050","Chara_icon/290000050","Chara_icon/290000050","2019/01/01",True,"1", -297000040,999,"《絆の双刃》キリト&アスナ",4,2,800,15000,100,1,0,0,0,"s297000040","s297000040_passive",20,140,25,130,30,125,32,120,34,115,36,110,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ヒロイックテイルズ","ヒロイック〇〇〇〇テイルズ","自パーティのSTRを120%上昇
させる。覚醒で効果時間が延長。
【パッシブ】ラストアタック回数
を重ねる毎にボーナスが増加。","自パーティのSTR上昇","心を通じ合わせ、まるで一対の
双刃のようにシンクロした剣技を
操る彼らの存在は言うまでもなく
《SAO》攻略に不可欠であった。","slog_297000040","slog_297000040","Chara_icon/297000040","Chara_icon/297000040","2019/01/01",True,"1", -205000030,999,"《はにかみ露天》リズベット",5,3,1600,20000,100,1,0,0,0,"s205000030",,30,190,35,182,40,174,44,170,46,166,48,162,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_LIS_SUP_028","湯けむりテンプテーション","湯けむり〇〇〇〇テンプテーション","敵の注目を集め、リーグポイント獲得量が20%上昇。範囲内の
全ユニットのINTも120%
上昇。覚醒で効果時間が延長。","敵からの注目&リーグポイント上昇+全ユニットINT上昇","火照る身体を秋風で冷ますその姿
は、普段の明るい彼女とは少し
雰囲気が異なる様子。いたずらな
笑顔によろめいてしまいそう。","slog_205000030","slog_205000030","Chara_icon/205000030","Chara_icon/205000030","2019/01/01",True,"1", -290000060,999,"《湯煙のひと時》ユイ",5,2,1600,20000,100,1,0,0,0,"s290000060",,65,200,71,186,77,172,83,165,86,158,89,151,6,12,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_YUI_SUP_028","ワンダー・シャワー!","ワンダー・〇〇〇〇〇シャワー!","自パーティの状態異常を回復後、STRを100%上昇させ、
全属性への耐性も付与される。
覚醒で効果時間が延長。","自パーティの状態異常回復+STR上昇+全属性耐性付与","ちゃんと自分で洗えます!と洗い
場へ向かった少女。シャンプー
ハットを携えて完璧に洗い上げ、
仕上げは仲良しのピナと一緒に!","slog_290000060","slog_290000060","Chara_icon/290000060","Chara_icon/290000060","2019/01/01",True,"1", -203000040,999,"《温熱の誘惑》リーファ",4,1,800,15000,100,1,0,0,0,"s203000040",,30,90,35,80,40,75,45,70,50,65,55,60,10,20,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_LEA_SUP_028","アトラクト・トリップ","アトラクト・〇〇〇〇トリップ","戦闘不能状態のメンバーを2人
復帰させ、自パーティのVITを
20秒間330%上昇させる。
覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+自パーティのVIT上昇","しとどに濡れた身体を外気に晒し
休憩中の様子。いつもよりも
しおらしく上気した彼女に惑わさ
れないようにご用心。","slog_203000040","slog_203000040","Chara_icon/203000040","Chara_icon/203000040","2019/01/01",True,"1", -209000050,999,"《浴場の洗戯》ユウキ",4,3,800,15000,100,1,0,0,0,"s209000050",,28,180,30,172,32,168,35,164,38,160,40,156,5,10,"ボーナス増加量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_YUU_SUP_028","チアフル・ウォッシュ!!","チアフル・〇〇〇ウォッシュ!!","攻撃中のメンバーのスキルを
再使用可能にした後、30秒間
リーグポイントの獲得量を増加
させる。覚醒で増加量が上昇。","クールタイムスキップ+リーグポイントの獲得量上昇","大浴場の醍醐味と言えば、皆で
わいわい一緒に入れること!
そんなところにいないで、こっち
で一緒に洗いっこしよ!","slog_209000050","slog_209000050","Chara_icon/209000050","Chara_icon/209000050","2019/01/01",True,"1", -208000020,999,"《湯上りの一本》エギル",3,2,400,10000,100,1,0,0,0,"s208000020",,10,125,10,120,12,115,14,110,15,105,16,100,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_AGI_SUP_028","ぐっと一気に!","ぐっと一気に!","自パーティのSTR・VIT・INT
を50%上昇させ、移動速度も
上昇させる。
覚醒で効果時間が延長。","自パーティのSTR・VIT・INT&移動速度上昇","心ゆくまで温泉を堪能した後は、
定番のアレが欠かせない。腰に
手を当て一気に飲み干す!それが
男の嗜みというものだ。","slog_208000020","slog_208000020","Chara_icon/208000020","Chara_icon/208000020","2019/01/01",True,"1", -204000040,999,"《堅実ナース》シノン",5,2,1600,20000,100,1,0,0,0,"s204000040",,20,150,25,142,30,134,34,130,36,126,38,122,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_SIN_SUP_020","スピリットインジェクション","スピリット〇〇〇インジェクション","自パーティのSTR・VITを
100%上昇させ、ラスト
アタックボーナスも80%増加
させる。覚醒で効果時間が延長。","自パーティのSTR・VIT&ラストアタックボーナス上昇","沢山ある仕事も丁寧迅速にこなす
凄腕ナースの彼女。中でも特に
評判なのは注射の腕。痛みも感じ
ぬ早業で終わらせてくれる。","slog_204000040","slog_204000040","Chara_icon/204000040","Chara_icon/204000040","2019/01/01",True,"1", -206000040,999,"《奔走ナース》シリカ",4,2,800,15000,100,1,0,0,0,"s206000040",,20,140,25,130,30,125,32,120,34,115,36,110,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_SIL_SUP_020","イノセンスプロテクション","イノセンス〇〇〇プロテクション","自パーティのSTRが120%上昇
し、耐久値も50%以下からは
減らなくなる。
覚醒で効果時間が延長。","STR上昇+耐久値が5割を超えて減らなくなる","新米ナースの彼女はいつだって大
忙し!あっちへこっちへ奔走する
毎日だが、それでも患者さんへ
贈る笑顔だけは忘れない。","slog_206000040","slog_206000040","Chara_icon/206000040","Chara_icon/206000040","2019/01/01",True,"1", -203000050,999,"《献身ナース》リーファ",5,3,1600,20000,100,1,0,0,0,"s203000050",,45,165,55,151,65,137,70,130,75,123,80,116,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_LEA_SUP_020","ドーピングロケット!","ドーピング〇〇〇〇ロケット!","特別製の注射器ロケットを前方に
複数回発射し、敵に非常に強力な
ダメージを与える。
覚醒で攻撃力が上昇。","注射器ロケットを前方に複数回発射する","あなたのために用意しました!と
ばかりの笑顔で差し出されれば、
誰しも断れまい。例えそれが、人
間ほどある巨大注射であっても…","slog_203000050","slog_203000050","Chara_icon/203000050","Chara_icon/203000050","2019/01/01",True,"1", -202000070,999,"《微笑みナース》アスナ",4,1,800,15000,100,1,0,0,0,"s202000070",,30,85,35,75,40,70,45,65,50,60,55,55,10,20,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_ASU_SUP_020","エンジェリックスマイル","エンジェリック〇〇〇スマイル","戦闘不能状態のメンバーを2人
復帰させ、自パーティのINTを
10秒間120%上昇させる。
覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+自パーティのINT上昇","白衣の天使の名に相応しく、いつ
でも微笑みを絶やさない彼女の
存在は、全ての患者の癒しとなっ
ていることだろう。","slog_202000070","slog_202000070","Chara_icon/202000070","Chara_icon/202000070","2019/01/01",True,"1", -211000020,999,"《お姫様メイド》アリス",5,3,1600,20000,100,1,0,0,0,"s211000020",,60,130,70,118,80,106,85,100,90,94,95,88,10,20,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_ALI_SUP_013","ブルーム・スイーパー","ブルーム・〇〇〇〇スイーパー","手にした箒で周りをお掃除する
広範囲攻撃。敵に非常に強力な
ダメージと封印状態を与える。
覚醒で攻撃力が上昇。","広範囲攻撃+封印を付与","美しく高貴な姫のごとき容姿に
惑わされ、敷地に踏み込めば最期
だ。細腕に似合わぬ剛力で、即座
に粛清されてしまうことだろう。","slog_211000020","slog_211000020","Chara_icon/211000020","Chara_icon/211000020","2020/09/08 7:00:00",True,"1", -202000080,999,"《お嬢様メイド》アスナ",4,2,800,15000,100,1,0,0,0,"s202000080",,20,100,25,90,30,85,32,80,34,75,36,70,6,12,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_ASU_SUP_013","プレッピー・レティニュー","プレッピー・〇〇〇レティニュー","自パーティの状態異常を回復後、
STR・VITを90%上昇させる。
覚醒で効果時間が延長。","自パーティの状態異常回復+STR・VIT上昇","育ちの良い彼女の所作は、いかに
高貴な家庭においても恥ずるとこ
ろは無い。思いやり溢れる仕事ぶ
りに、誰もが笑顔を取り戻す。","slog_202000080","slog_202000080","Chara_icon/202000080","Chara_icon/202000080","2020/09/08 7:00:00",True,"1", -203000060,999,"《お世話好きメイド》直葉",4,2,800,15000,100,1,0,0,0,"s203000060",,20,150,25,142,30,138,32,134,34,130,38,126,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_LEA_SUP_013","リーブ・イット・トゥ・ミー!","リーブ・イット・〇トゥ・ミー!","自パーティのSTRを120%
上昇させ、ラストアタック
ボーナスも50%増加させる。
覚醒で効果時間が延長。","自パーティのSTR&ラストアタックボーナス上昇","世話焼きな彼女はいつも大忙し。
それでもどこか楽しそうに笑顔を
絶やさない。実家の兄との生活で
お世話慣れしているのだとか。","slog_203000060","slog_203000060","Chara_icon/203000060","Chara_icon/203000060","2020/09/08 7:00:00",True,"1", -290000070,999,"《純真メイド》ユイ",4,3,800,15000,100,1,0,0,0,"s290000070",,10,120,12,110,14,105,15,100,16,95,17,90,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_YUI_SUP_013","ちびっこクリーナー","ちびっこ〇〇〇〇〇クリーナー","敵と他パーティから感知されず、
ファーストアタックボーナスが
50%増加し、移動速度も上昇。
覚醒で効果時間が延長。","ハイディング+ファーストアタックボーナス&速度上昇","幼さ故の純粋さを持つ彼女にとっ
て、お屋敷の仕事は目新しいもの
ばかり。今日は覚えたてのはたき
がけ、一生懸命頑張ります!","slog_290000070","slog_290000070","Chara_icon/290000070","Chara_icon/290000070","2020/09/08 7:00:00",True,"1", -206000050,999,"《食いしん坊トナカイ》シリカ",5,2,1600,20000,100,1,0,0,0,"s206000050",,45,145,55,137,65,129,70,125,75,121,80,117,5,10,"ボーナス増加量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_SIL_SUP_029","スペシャルホーリースイーツ","スペシャル〇〇〇ホーリースイーツ","30秒間、ファーストアタックと
ラストアタックで得られる
ボーナスポイントが増加する。
覚醒で増加量が上昇。","ファースト&ラストアタックのボーナスポイント量上昇","最高のクリスマスパーティ!勿論
お楽しみは、デコレーションいっ
ぱいの特別なケーキ…これは摘み
食いじゃなくって味見ですっ!","slog_206000050","slog_206000050","Chara_icon/206000050","Chara_icon/206000050","2019/01/01",True,"1", -220000030,999,"《聖夜の歌声》サチ",5,3,1600,20000,100,1,0,0,0,"s220000030",,10,90,12,82,14,74,16,70,17,66,18,62,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"シークレットチャント","シークレット〇〇〇〇チャント","敵に感知されず、他パーティの
ミニマップ上からも見えなくなり
INTも160%上昇する。
覚醒で効果時間が延長。","ハイディング+INT上昇","聖なる夜に響くのは、ある平凡な
少女の願いを乗せた清らかな
歌声。願わくば、君の旅路に幸
多からんことを――","slog_220000030","slog_220000030","Chara_icon/220000030","Chara_icon/220000030","2019/01/01",True,"1", -203000070,999,"《魅惑の綺羅星》リーファ",4,3,800,15000,100,1,0,0,0,"s203000070",,20,150,22,140,24,135,25,130,26,125,27,120,3,6,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_LEA_SUP_029","キューティクリスマス","キューティ〇〇〇クリスマス","グリードからの注目を集め、
リーグポイント獲得量が10%
上昇する。
覚醒で効果時間が延長。","グリードからの注目&リーグポイント獲得量上昇","今宵は聖なるクリスマス。煌びや
かな街へ繰り出せば、心も身体も
踊りだしてしまいそう。一夜限り
のショータイムを共に。","slog_203000070","slog_203000070","Chara_icon/203000070","Chara_icon/203000070","2019/01/01",True,"1", -204000050,999,"《雪明りの贈り物》シノン",4,3,800,15000,100,1,0,0,0,"s204000050",,40,165,50,151,55,144,60,137,65,130,70,123,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_SIN_SUP_029","デンジャラスプレゼント","デンジャラス〇〇〇プレゼント","無数の贈り物を降らせて強力な
広範囲攻撃を行い、敵にダメージ
と混乱状態を与える。
覚醒で攻撃力が上昇。","広範囲攻撃+混乱を付与","クリスマスの定番と言えば、親し
い人とのプレゼント交換。悪戯っ
ぽい笑みから手渡される中身は、
おうちに帰ってからのお楽しみ。","slog_204000050","slog_204000050","Chara_icon/204000050","Chara_icon/204000050","2019/01/01",True,"1", -216000030,999,"《雪旅の宝物》フィリア",3,3,400,10000,100,1,0,0,0,"s216000030",,80,150,80,144,90,138,100,132,105,126,110,120,8,16,"INT上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"ギフト・フォー・ユー","ギフト・〇〇〇〇フォー・ユー","攻撃中のメンバーのスキルを
再使用可能にした後、10秒間
自パーティのINTを上昇させる。
覚醒でINT上昇量が増加。","クールタイムスキップ+INT上昇","今夜限りのサンタクロースの使命
は、良い子にプレゼントをあげる
こと。今年一年、良い子にしてた
子はだあれ?","slog_216000030","slog_216000030","Chara_icon/216000030","Chara_icon/216000030","2019/01/01",True,"1", -297000070,999,"《胸懐の欠片》ロニエ&ティーゼ",5,2,1600,20000,100,1,0,0,0,"s297000070",,30,95,32,91,34,87,36,84,37,82,38,80,7,15,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"プルーフオブライフ","プルーフ〇〇〇〇オブライフ","リーグポイントの獲得量が
25%増加する。
覚醒で効果時間が延長。","リーグポイントの獲得量上昇","少女たちの胸に去来するのは、
幸福な思い出、自責の念、恋焦が
れる想いと残酷な別れ――そして
未だ剣に宿る持ち主の意志だ。","slog_297000070","slog_297000070","Chara_icon/297000070","Chara_icon/297000070","2019/01/01",True,"1", -297000080,999,"《時斬の偉丈夫》ベルクーリ",4,3,800,15000,100,1,0,0,0,"s297000080",,105,150,115,138,120,132,125,126,130,120,135,114,8,16,"STR上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"ビヘッド・ザ・タイム","ビヘッド・〇〇〇〇ザ・タイム","攻撃中のメンバーのスキルを
再使用可能にした後、10秒間
自パーティのSTRを上昇させ
る。覚醒でSTR上昇量が増加。","クールタイムスキップ+STR上昇","整合騎士長を務める剣豪。
ゆったりとしつつも威厳ある佇ま
いは、整合騎士第一位の貫禄を感
じさせる。","slog_297000080","slog_297000080","Chara_icon/297000080","Chara_icon/297000080","2019/01/01",True,"1", -201000040,999,"《秘技!二刀剣法》キリト",5,2,1600,20000,100,1,0,15,0,"s201000040",,35,160,37,153,39,146,41,140,42,135,43,130,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_KIR_SUP_018","秘剣ノ舞","秘剣ノ舞","リーグポイントの獲得量が20%
増加する。覚醒で効果時間が
延長。【パッシブ】アダプタブル
出現率が上昇。重複で効果増。","リーグポイントの獲得量上昇","剣の扱いに熟達した者にのみ
許された秘技、今宵ご披露いたし
ましょう!片手剣を刀に持ち替え
魅せる、超絶技巧をご覧あれ。","slog_201000040","slog_201000040","Chara_icon/201000040","Chara_icon/201000040","2019/01/01",True,"1", -204000060,999,"《氷雪の弦音》シノン",4,2,800,15000,100,1,0,0,0,"s204000060",,10,50,12,45,14,41,15,39,16,37,17,35,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_SIN_SUP_018","ズボシ・シューター","ズボシ・〇〇〇〇シューター","自パーティのINTを
160%上昇させる。
覚醒で効果時間が延長。","自パーティのINT上昇","和風な服飾に身を包み、弓を携え
る姿は、常より更に凛として
見える。新しい年を祝した弓武芸
は当然百発百中だ。","slog_204000060","slog_204000060","Chara_icon/204000060","Chara_icon/204000060","2019/01/01",True,"1", -205000040,999,"《彩赤の木槌》リズベット",4,3,800,15000,100,1,0,0,0,"s205000040",,10,200,12,192,14,184,15,180,16,176,17,172,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_LIS_SUP_018","ウチデノ・ハンマー","ウチデノ・〇〇〇〇ハンマー","レコードメダルを再配置し、
メダルの位置をミニマップに
一定時間表示する。
覚醒で効果時間が延長。","レコードメダル再配置+レコードメダルサーチ発動","来たる年が皆にとって、より良き
年になるように。ついでに商売繁
盛も祈りつつ!?祝福の小槌を
高々と掲げ、福を呼び込もう!","slog_205000040","slog_205000040","Chara_icon/205000040","Chara_icon/205000040","2019/01/01",True,"1", -207000010,999,"《戦炎の武将》クライン",4,2,800,15000,100,1,0,0,0,"s207000010",,10,50,12,45,14,41,15,39,16,37,17,35,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_KLE_SUP_018","サムライ・フレイマー","サムライ・〇〇〇〇フレイマー","自パーティの攻撃に
火属性を付与するとともに
STRを125%上昇させる。
覚醒で効果時間が延長。","火属性付与+STR上昇","VR界のサムライと言えばこの男。
今回は年末年始のお祝いとあって
尚更張り切っている模様。大将戦
ならオレに任せとけィ!","slog_207000010","slog_207000010","Chara_icon/207000010","Chara_icon/207000010","2019/01/01",True,"1", -212000020,999,"《天性の剣才》ユージオ",5,2,1600,20000,100,1,0,0,0,"s212000020","s212000020_passive",15,65,17,61,19,57,21,54,22,52,23,50,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_EUG_SUP_018","天砕ノ舞","天砕ノ舞","自パーティのSTRを140%上昇
させる。覚醒で効果時間が延長。
【パッシブ】ラストアタック回数
を重ねる毎にボーナスが増加。","自パーティのSTR上昇","早くも刀の扱いに慣れてきた剣の
天才は、戸惑いよりも初めて見る
和風防具への好奇心が勝り、珍し
く少々はしゃいでいる様子。","slog_212000020","slog_212000020","Chara_icon/212000020","Chara_icon/212000020","2019/01/01",True,"1", -203000080,999,"《真剣勝負!》直葉",4,2,800,15000,100,1,0,0,0,"s203000080",,10,50,12,45,14,41,15,39,16,37,17,35,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_LEA_SUP_018","タチカゼ・ストーマー","タチカゼ・〇〇〇〇ストーマー","自パーティの攻撃に
風属性を付与するとともに
STRを125%上昇させる。
覚醒で効果時間が延長。","風属性付与+STR上昇","真剣での立ち合いは常ではない
とはいえ、剣道は彼女の独壇場。
刀を抜き向き合えば、その鋭い
眼光が隙を見逃すことは無い。","slog_203000080","slog_203000080","Chara_icon/203000080","Chara_icon/203000080","2019/01/01",True,"1", -210000010,999,"《隠密の傑忍》アルゴ",4,3,800,15000,100,1,0,0,0,"s210000010",,12,90,14,80,16,75,17,70,18,65,19,60,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_ARG_SUP_018","トコヤミ・シークレシー","トコヤミ・〇〇〇シークレシー","レコードメダルとグリードの位置
をミニマップに表示、自パーティ
の移動速度を上昇させる。
覚醒で効果時間が延長。","レコードメダル&グリードサーチ発動+移動速度上昇","闇夜の空を音もなく駆ける小さな
影が一つ、誰に気付かれることも
なく去ってゆく。街を見守る義忍
は今宵も孤独に戦い続ける。","slog_210000010","slog_210000010","Chara_icon/210000010","Chara_icon/210000010","2019/01/01",True,"1", -290000080,999,"《招福の舞姫》ユイ",4,1,800,15000,100,1,0,0,0,"s290000080",,45,25,50,23,55,21,60,20,65,19,70,18,10,20,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_YUI_SUP_030","カホウ・カミング","カホウ・カミング","自パーティ全員の耐久値を回復し
状態異常も解除する。
覚醒で回復量が上昇。","耐久値と状態異常を回復","新年の祈りを込めた舞を捧げる
愛らしい巫女さんのお陰で、今年
も良い年になること間違いなし。
皆に幸福が訪れますように。","slog_290000080","slog_290000080","Chara_icon/290000080","Chara_icon/290000080","2019/01/01",True,"1", -218000020,999,"《安息のひと時》レイン",5,3,1600,20000,100,1,0,0,0,"s218000020",,45,165,55,151,65,137,70,130,75,123,80,116,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_RAI_SUP_035","スターリーナイト","スターリーナイト","星を降らせて強力な広範囲攻撃を
行い、敵にダメージと麻痺状態を
与える。
覚醒で攻撃力が上昇。","広範囲攻撃+麻痺を付与","柔らかな寝具に包まれて、穏やか
な時間を過ごす彼女の表情に偽り
は感じられない。自ら人を遠ざけ
た寂しい嘘つきはもういない。","slog_218000020","slog_218000020","Chara_icon/218000020","Chara_icon/218000020","2019/01/01",True,"1", -205000050,999,"《夢境のくつろぎ》リズベット",4,1,800,15000,100,1,0,0,0,"s205000050",,30,65,35,55,40,50,45,45,50,40,55,35,10,20,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_LIS_SUP_035","ウィッシュ・オン・スター","ウィッシュ・〇〇〇オン・スター","戦闘不能状態のメンバーを2人
復帰させ、耐久値を回復する。
覚醒で復帰時の回復量が上昇。","戦闘不能状態の自パーティメンバー2人を復帰","朝から晩まで忙しい鍛冶職人に
とって休息の時間は貴重なもの。
寝ぼけた眼を瞬かせて、夢の狭間
を揺蕩う時間は至福の時だ。","slog_205000050","slog_205000050","Chara_icon/205000050","Chara_icon/205000050","2019/01/01",True,"1", -209000060,999,"《剣聖の覚醒》ユウキ",4,2,800,15000,100,1,0,0,0,"s209000060",,10,50,12,45,14,41,15,39,16,37,17,35,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_YUU_SUP_035","ナイトリー・ドリーマー","ナイトリー・〇〇〇ドリーマー","自パーティの攻撃に
闇属性を付与するとともに
STRを125%上昇させる。
覚醒で効果時間が延長。","闇属性付与+STR上昇","《絶剣》と謳われる彼女も、装備
を解けば無防備な笑顔を見せてく
れる。寝起きの良い彼女の覚醒は
清々しい朝の空気を匂わせる。","slog_209000060","slog_209000060","Chara_icon/209000060","Chara_icon/209000060","2019/01/01",True,"1", -216000040,999,"《夢想の願い》フィリア",4,3,800,15000,100,1,0,0,0,"s216000040",,20,100,30,98,40,96,50,94,60,92,70,90,10,20,"メダル増加量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"ヴィジョナリー・トレジャー","ヴィジョナリー・〇トレジャー","一定範囲内にいるグリードを
メダル排出量の多いゴールデン・
グリードに転成させる。
覚醒でメダル排出量が増加。","グリードをゴールデン・グリードに転成","心休まる眠りを得られない期間を
長く過ごした彼女にとって、あた
たかに安らげる今は幸福な時だ。
願わくば、いつまでも平穏に…。","slog_216000040","slog_216000040","Chara_icon/216000040","Chara_icon/216000040","2019/01/01",True,"1", -299000410,999,"《咲き誇る技巧》スズネ",5,2,1600,20000,100,1,0,15,0,"s299000410",,10,150,11,140,12,130,13,125,14,120,15,115,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"壮健満開はなざかり","壮健満開〇〇〇〇〇はなざかり","自パーティがダメージを受けなく
なる。覚醒で効果時間が延長。
【パッシブ】アダプタブル出現率
が上昇。重複で効果増。","ダメージを受けなくなる","和風防具専門の鍛冶師。裁縫の腕
にも秀で、《SAO》で多くの作品
を生み出した。自作の品で笑顔に
なる人を見るのが何よりの喜び。","slog_299000410","slog_299000410","Chara_icon/299000410","Chara_icon/299000410","2019/01/01",True,"1", -202000090,999,"《シャイニーオース》アスナ",5,2,1600,20000,100,1,0,0,0,"s202000090",,10,60,12,55,14,51,16,49,17,47,18,45,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_ASU_SUP_015","ブライト・シャイン","ブライト・〇〇〇〇シャイン","自パーティのINTを
130%上昇させるとともに、
よろけ・ダウン無効状態にする。
覚醒で効果時間が延長。","INT上昇+よろけ・ダウン無効付与","誰もが愛するアイドルは、その
輝きで世界を照らし、神々しさ
すら感じさせる。みんなの声援が
わたしに力をくれるんだよ。","slog_202000090","slog_202000090","Chara_icon/202000090","Chara_icon/202000090","2019/01/01",True,"1", -209000070,999,"《ブレイブリースター》ユウキ",4,2,800,15000,100,1,0,0,0,"s209000070",,10,50,12,45,14,41,15,39,16,37,17,35,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_YUU_SUP_015","ファイト・カムオン!","ファイト・〇〇〇〇カムオン!","自パーティのSTRを
220%上昇させる。
覚醒で効果時間が延長。","自パーティのSTR上昇","彼女が魅せる何にでも立ち向かう
強さは、見る者すべてを勇気づけ
てくれる。アイドルに大事なのは
勇気…なんてね♪","slog_209000070","slog_209000070","Chara_icon/209000070","Chara_icon/209000070","2019/01/01",True,"1", -211000030,999,"《バッシュフルセイバー》アリス",5,2,1600,20000,100,1,0,0,0,"s211000030",,30,210,34,202,38,194,40,190,42,186,44,182,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_ALI_SUP_015","イノセント・フェアー","イノセント・〇〇〇〇フェアー","リーグポイントの獲得量が
45%増加する。
覚醒で効果時間が延長。","リーグポイントの獲得量上昇","異界の少女は初めて知るアイドル
文化に戸惑いを隠せない様子。そ
の初々しさに目が離せない。待つ
のです!この姿は…流石に…!
","slog_211000030","slog_211000030","Chara_icon/211000030","Chara_icon/211000030","2019/01/01",True,"1", -201000050,999,"《ヴァーチャルルシファー》キリト",4,3,800,15000,100,1,0,0,0,"s201000050",,20,80,23,75,26,72,28,69,29,67,30,65,3,6,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_KIR_SUP_015","ヴァーチャル・エディクション","ヴァーチャル・〇〇エディクション","一定範囲内の敵のバフを解除し、
INTを60%低下させる。
覚醒で効果時間が延長。","敵のバフ解除+敵にINT低下を付与","仮想世界に舞い降りた、可憐な
少女(?!)。熱狂的ファン急増
中、一大ブームを巻き起こすや
も。君たち…応援してねっ!","slog_201000050","slog_201000050","Chara_icon/201000050","Chara_icon/201000050","2019/01/01",True,"1", -203000090,999,"《恋情パティシエ》リーファ",5,2,1600,20000,100,1,0,0,0,"s203000090","s203000090_passive",10,80,11,75,12,72,13,69,14,67,15,65,1,2,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_LEA_SUP_032","ハートグロウ・ファウンダー","ハートグロウ・〇〇〇ファウンダー","自パーティの移動速度を上昇させ
る。覚醒で効果時間が延長。
【パッシブ】ファーストアタック
を重ねる毎にボーナスが増加。","自パーティの移動速度上昇","妖精界でもバレンタインは超重要
イベント。恋する乙女は大忙し!
愛しの彼に渡すため、真心込めて
元気一杯レッツクッキング!","slog_203000090","slog_203000090","Chara_icon/203000090","Chara_icon/203000090","2019/01/01",True,"1", -202000100,999,"《最愛パティシエ》アスナ",4,2,800,15000,100,1,0,0,0,"s202000100",,10,50,12,45,14,41,15,39,16,37,17,35,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_ASU_SUP_032","ウンディーネ・パティスリー","ウンディーネ・〇〇〇パティスリー","自パーティの攻撃に
水属性を付与するとともに
STRを125%上昇させる。
覚醒で効果時間が延長。","水属性付与+STR上昇","なんでもこなす才女には、チョコ
作りだってお任せあれ。愛する彼
のため、とびきり美味しい逸品を
目指して日夜研究中。","slog_202000100","slog_202000100","Chara_icon/202000100","Chara_icon/202000100","2019/01/01",True,"1", -204000070,999,"《純情ショコラ》シノン",4,3,800,15000,100,1,0,0,0,"s204000070",,25,180,30,170,33,160,36,155,38,150,40,145,5,10,"召喚増強レベル","{0}レベル","skill_3_2line_moji86","un_2line_moji86","CH_SIN_SUP_032","リサーチヘルプ・テイスト","リサーチヘルプ・〇〇テイスト","守護騎士系上位種Mobを2体
召喚し、戦闘支援を受ける。
覚醒で召喚Mobのレベルが
増強。","お助けMob【守護騎士系】を召喚","数あるチョコに差をつけるべく、
まずは標的のリサーチから。甘す
ぎるより、ちょっぴりビターが
お好みかも?","slog_204000070","slog_204000070","Chara_icon/204000070","Chara_icon/204000070","2019/01/01",True,"1", -206000060,999,"《恋慕ショコラ》シリカ",4,2,800,15000,100,1,0,0,0,"s206000060",,40,90,45,85,50,81,52,79,54,77,55,75,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_SIL_SUP_032","スウィート・ヒーリング","スウィート・〇〇〇〇ヒーリング","自パーティのVITを
250%上昇させるとともに、
耐久値を徐々に回復する。
覚醒で効果時間が延長。","VIT上昇+リジェネレイト付与","幼い恋心を募らせる少女は、甘い
甘~いチョコを作る。例え不格好
でも、込められたとろけるような
甘い気持ちはきっと届くはず。","slog_206000060","slog_206000060","Chara_icon/206000060","Chara_icon/206000060","2019/01/01",True,"1", -215000040,999,"《千夜の遊楽》ストレア",5,2,1600,20000,100,1,0,0,0,"s215000040",,20,150,22,142,24,134,26,130,27,127,28,125,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"グラマー・ノックアウト","グラマー・〇〇〇ノックアウト","ボスに与えるダメージが上昇し、
リーグポイントの獲得量も
30%増加する。
覚醒で効果時間が延長。","ボスキラー+リーグポイントの獲得量上昇","千夜一夜の一幕で、束の間の遊楽
に溺れてみるのはいかが?
甘い誘惑によろめかない人は、
恐らくひとりもいないだろう。","slog_215000040","slog_215000040","Chara_icon/215000040","Chara_icon/215000040","2019/01/01",True,"1", -203000100,999,"《純金の輝風》リーファ",4,1,800,15000,100,1,0,0,0,"s203000100",,45,25,50,23,55,21,60,20,65,19,70,18,10,20,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_LEA_SUP_014","ファンシー・サルテイション","ファンシー・〇〇〇サルテイション","自パーティ全員の耐久値を回復し
状態異常も解除する。
覚醒で回復量が上昇。","耐久値と状態異常を回復","金糸を編んだようなポニーテール
を翻し、風のように舞い踊る妖精
のような少女。楽し気な舞に思わ
ず見とれてしまいそう。","slog_203000100","slog_203000100","Chara_icon/203000100","Chara_icon/203000100","2019/01/01",True,"1", -205000060,999,"《桃閃の舞踏》リズベット",4,2,800,15000,100,1,0,0,0,"s205000060",,10,50,12,45,14,41,15,39,16,37,17,35,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_LIS_SUP_014","グラウンド・ダンサー","グラウンド・〇〇〇〇ダンサー","自パーティの攻撃に
土属性を付与するとともに
STRを125%上昇させる。
覚醒で効果時間が延長。","土属性付与+STR上昇","彼女のトレードマークとも言える
桃色を弾ませて、舞を捧げる快活
な彼女。親し気な微笑みにこちら
も一緒に踊りだしたくなる。","slog_205000060","slog_205000060","Chara_icon/205000060","Chara_icon/205000060","2019/01/01",True,"1", -209000080,999,"《紫電の舞踊》ユウキ",4,3,800,15000,100,1,0,0,0,"s209000080",,30,90,35,85,38,82,41,79,43,77,45,75,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_YUU_SUP_014","ヴィゴラスリー・トランス","ヴィゴラスリー・〇トランス","周囲にいる敵モンスターの
STR・VIT・INTを
45%低下させる。
覚醒で効果時間が延長。","敵にSTR・VIT・INT低下を付与","人一倍小さな身体に、人一倍豪奢
な衣装を纏い、弾むように元気に
踊る。純粋無垢な笑顔を見れば、
誰もが元気を貰えるだろう。","slog_209000080","slog_209000080","Chara_icon/209000080","Chara_icon/209000080","2019/01/01",True,"1", -212000030,999,"《恒久の愛情》ユージオ",5,3,1600,20000,100,1,0,0,0,"s212000030",,65,210,68,204,71,201,73,198,75,196,77,194,10,20,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_EUG_SUP_033","エタニティ・ディザイア","エタニティ・〇〇〇ディザイア","非常に強力な広範囲攻撃を放ち、
敵にダメージと氷咲状態を
与える。
覚醒で攻撃力上昇。","強力な広範囲攻撃+氷咲を付与","生真面目な彼にとって、言葉にの
せた愛は、すなわち永遠を表す。
心込めた花束と、永久の誓いを
あなたに――。","slog_212000030","slog_212000030","Chara_icon/212000030","Chara_icon/212000030","2020/03/03 7:00:00",True,"1", -201000060,999,"《赤誠の求愛》キリト",4,2,800,15000,100,1,0,0,0,"s201000060",,10,50,12,45,14,41,15,39,16,37,17,35,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_KIR_SUP_033","トゥルー・ラバー","トゥルー・ラバー","自パーティの攻撃に
聖属性を付与するとともに
STRを125%上昇させる。
覚醒で効果時間が延長。","聖属性付与+STR上昇","普段は少し照れくさいけれど、
誠実な情愛を示すこの時は、真摯
に静謐に向き合いたい。静かに手
渡したそれは、大きな愛の証。","slog_201000060","slog_201000060","Chara_icon/201000060","Chara_icon/201000060","2020/03/03 7:00:00",True,"1", -297000090,999,"《狂愛の誓い》クラディール",4,3,800,15000,100,1,0,0,0,"s297000090",,40,165,50,151,55,144,60,137,65,130,70,123,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"ディジェネレイト・ギフト","ディジェネレイト〇〇・ギフト","無数の贈り物を降らせて強力な
広範囲攻撃を行い、敵にダメージ
と麻痺状態を与える。
覚醒で攻撃力が上昇。","広範囲攻撃+麻痺を付与","狂信とも呼べるほどの激しい想い
を胸に秘め、男は恭しく愛を差し
出す。全てはあなたを我が物とす
るため。","slog_297000090","slog_297000090","Chara_icon/297000090","Chara_icon/297000090","2020/03/03 7:00:00",True,"1", -297000100,999,"《刻苦の訴え》ユイ&アスナ",5,2,1600,20000,100,1,0,0,0,"s297000100",,10,50,12,47,14,44,16,41,17,38,18,36,4,8,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_YUI_SUP_005","アーネスト・アシスト","アーネスト・〇〇〇〇アシスト","自パーティの
スキルEXPの獲得量を100%、
VITを300%増加させる。
覚醒で効果時間が延長。","スキルEXPの獲得量+VIT上昇","後輩とも、妹とも言える彼女の
苦悩に、少女の小さな胸は張り裂
けんばかりに痛む。母に寄り添わ
れ、訴えるは切なる想い。","slog_297000100","slog_297000100","Chara_icon/297000100","Chara_icon/297000100","2020/06/09 7:00:00",True,"1", -291000020,999,"《背信の下僕》リコ",5,3,1600,20000,100,1,0,0,0,"s291000020",,65,210,68,204,71,201,73,198,75,196,77,194,10,20,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_REC_SUP_006","トレイター・インサニティ","トレイター・〇〇〇インサニティ","周囲のフィールドデータを改ざん
して敵を攻撃、甚大なダメージを
与える。
覚醒で攻撃力が上昇。","強力な広範囲攻撃","プレイヤー達に敵対し、自身を
フィールドオブジェクトに変じた
リコ。彼女の意志は、想いは、
果たして――。","slog_291000020","slog_291000021","Chara_icon/291000020","Chara_icon/291000021","2020/06/09 7:00:00",True,"1", -290000090,999,"《一路順風》ユイ",5,3,1600,20000,100,1,0,0,0,"s290000090",,1,100,2,95,2,90,3,85,3,80,3,70,1,2,"ドロップ数","{0}個","skill_3_2line_moji86","un_2line_moji86","CH_YUI_SUP_002","アニバーサリー・トレジャー","アニバーサリー・〇〇〇トレジャー","グリードを1体召喚。
覚醒でスキルポーションの
ドロップ数が増加。","敵Mob【グリード】を召喚","調査が始まって1年。楽しいこと
ばかりでは無かったけれど、それ
でも皆で笑ってこの日を迎えられ
た。この先の旅路にも幸あれ。","slog_290000090","slog_290000090","Chara_icon/290000090","Chara_icon/290000090","2020/06/23 7:00:00",True,"1", -291000030,999,"《一言芳恩》リコ",5,3,1600,20000,100,1,0,0,0,"s291000030",,30,200,35,192,40,184,44,180,46,176,48,172,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_REC_SUP_019","アニバーサリー・アテンション","アニバーサリー・〇アテンション","グリードからの注目を集め、
リーグポイント獲得量が
30%上昇する。
覚醒で効果時間が延長。","グリードからの注目&リーグポイント獲得量上昇","主との出会いから1年、彼女の
世界は今も広がり続けている。
これからも新しい旅へと、お供
いたします、マスター。","slog_291000030","slog_291000030","Chara_icon/291000030","Chara_icon/291000030","2020/06/23 7:00:00",True,"1", -297000110,999,"《一念天に通ず》オールキャスト",5,2,1600,20000,100,1,0,0,0,"s297000110",,10,85,12,82,14,79,16,76,18,73,20,70,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ファースト・アニバーサリー","ファースト・〇〇アニバーサリー","自パーティのSTR・VIT・INT
を150%上昇させる。
覚醒で効果時間が延長。","自パーティのSTR・VIT・INT上昇","全員の協力が無ければ、ここまで
来ることは叶わなかっただろう。
1年間の軌跡が鮮やかに記憶を
彩る。","slog_297000110","slog_297000110","Chara_icon/297000110","Chara_icon/297000110","2020/06/23 7:00:00",True,"1", -209000090,999,"《愛おしい日常》ユウキ",5,2,1600,20000,100,1,0,0,0,"s209000090",,160,130,170,120,180,110,190,105,195,100,200,95,20,40,"STR上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_YUU_SUP_034","ドリーミン・オーディナリー","ドリーミン・〇〇オーディナリー","15秒間、自パーティの
STR・移動速度が上昇する。
覚醒でSTR上昇量が増加。","自パーティのSTR&移動速度上昇","制服に腕を通し、友と語らい、
通いなれた道を踏みしめて――
そんな、どこかにあったかもしれ
ない、愛おしい日常の景色。","slog_209000090","slog_209000090","Chara_icon/209000090","Chara_icon/209000090","2020/07/07 7:00:00",True,"1", -202000110,999,"《早朝の誘い》アスナ",4,2,800,15000,100,1,0,0,0,"s202000110",,30,160,34,152,36,148,38,144,40,140,42,136,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_ASU_SUP_034","モーニング・グリーティング","モーニング・〇〇グリーティング","リーグポイント獲得量が
30%増加する。
覚醒で効果時間が延長。","リーグポイントの獲得量上昇","優等生の彼女は朝も早い。でも、
早起きすれば一緒に登校できるか
も。ほら、いつまでも寝ぼけてな
いの。置いてっちゃうよ!","slog_202000110","slog_202000110","Chara_icon/202000110","Chara_icon/202000110","2020/07/07 7:00:00",True,"1", -205000070,999,"《花の女子高生》リズベット",4,3,800,15000,100,1,0,0,0,"s205000070",,40,165,50,151,55,144,60,137,65,130,70,123,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_LIS_SUP_034","ガールズ・フォース!","ガールズ・〇〇〇〇フォース!","無数の岩を呼び出して周囲の敵を
攻撃、大きなダメージを与える。
覚醒で攻撃力が上昇。","強力な広範囲攻撃","カラオケにお買い物に、いつもの
カフェで勉強会。花の女子高生の
日常は忙しくも華やかで、楽しい
時間ほどあっという間なのだ。","slog_205000070","slog_205000070","Chara_icon/205000070","Chara_icon/205000070","2020/07/07 7:00:00",True,"1", -206000070,999,"《通学路の乙女》シリカ",4,1,800,15000,100,1,0,0,0,"s206000070",,20,130,24,120,27,110,29,105,30,100,31,98,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_SIL_SUP_034","ヒーリング・ロリポップ","ヒーリング・〇〇ロリポップ","触れると戦闘不能状態、
状態異常、耐久値を回復する
ヒールスポットを設置する。
覚醒で設置時間が延長。","ヒールスポットを設置","おはようございまーす!
町内を駆ける軽やかな挨拶に、
誰もが顔を綻ばせる。通学路の
乙女は今日も元気に邁進中。","slog_206000070","slog_206000070","Chara_icon/206000070","Chara_icon/206000070","2020/07/07 7:00:00",True,"1", -204000080,999,"《スターリーハート》シノン",5,3,1600,20000,100,1,0,0,0,"s204000080",,20,124,22,113,24,102,26,97,27,92,28,87,3,6,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_SIN_SUP_015","ファシネイテッド・スター","ファシネイテッド〇〇〇〇・スター","一定範囲内の敵の6属性全ての
耐性を無効化し、封印状態を
付与する。覚醒で属性耐性無効化
の効果時間が延長。","敵の6属性耐性を無効化+一定時間封印状態を付与","普段はクールな彼女が浮かべる、
羞恥に赤らむ表情に、観客は皆
魅了される。恥ずかしいけど、
みんなに負けてられないわ…!","slog_204000080","slog_204000080","Chara_icon/204000080","Chara_icon/204000080","2020/06/09 7:00:00",True,"1", -203000110,999,"《フェアリードリーマー》リーファ",4,3,800,15000,100,1,0,0,0,"s203000110",,28,180,30,172,32,168,35,164,38,160,40,156,5,10,"ボーナス増加量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_LEA_SUP_015","シスターズ・チアリング","シスターズ・〇〇〇チアリング","攻撃中のメンバーのスキルを
再使用可能にした後、30秒間
リーグポイントの獲得量を増加
させる。覚醒で増加量が上昇。","クールタイムスキップ+リーグポイントの獲得量上昇","妹ゆえの人懐っこい笑顔を向けら
れれば、誰もが心癒される。
来てくれてありがとう!これから
も応援してね!","slog_203000110","slog_203000110","Chara_icon/203000110","Chara_icon/203000110","2020/06/09 7:00:00",True,"1", -206000080,999,"《プリティシンガー》シリカ",5,2,1600,20000,100,1,0,0,0,"s206000080",,20,185,22,177,24,169,26,165,28,161,30,157,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_SIL_SUP_015","エンカレッジ・ソング","エンカレッジ・〇〇〇〇ソング","自パーティのINTを100%、
リーグポイント獲得量を25%、
移動速度も上昇させる。
覚醒で効果時間が延長。","自パーティのINT&リーグポイント獲得量&移動速度上昇","普段からアイドルに憧れる彼女に
とって、この舞台は独壇場!その
歌声は会場を熱狂させる。
えへへっ皆!応援ありがとうー!","slog_206000080","slog_206000080","Chara_icon/206000080","Chara_icon/206000080","2020/06/09 7:00:00",True,"1", -205000080,999,"《スイートスミス》リズベット",4,1,800,15000,100,1,0,0,0,"s205000080",,30,85,35,75,40,70,45,65,50,60,55,55,10,20,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_LIS_SUP_015","ブリング・オポチュニティ","ブリング・〇〇〇オポチュニティ","戦闘不能状態のメンバーを2人
復帰させ、自パーティのSTRを
10秒間120%上昇させる。
覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+自パーティのSTR上昇","面倒見の良い彼女がいれば、お客
さん皆が喜ぶステージを作り上げ
られるはず!
さぁみんな、いっくわよー!","slog_205000080","slog_205000080","Chara_icon/205000080","Chara_icon/205000080","2020/06/09 7:00:00",True,"1", -291000040,999,"《マスターの贈り物》リコ",5,3,1600,20000,100,1,0,0,0,"s291000040",,95,155,97,154,100,152,104,149,110,145,120,140,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_REC_SUP_032","ジェニュイン・ナビゲーション","ジェニュイン・〇ナビゲーション","スキルポーションと
レコードメダル、グリードの
位置をミニマップに表示する。
覚醒で効果時間が延長。","スキルポーション&レコードメダル&グリードサーチ発動","大切な方からいただいた、信頼の
証。私の心のままに過ごして良い
のだと、あなたと共にいて良いの
だと言って貰えているようで。","slog_291000040","slog_291000040","Chara_icon/291000040","Chara_icon/291000040","2020/06/16 7:00:00",True,"1", -216000050,999,"《チアー・アップ!》フィリア",5,2,1600,20000,100,1,0,0,0,"s216000050",,25,180,30,160,33,150,36,140,38,130,40,120,4,8,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"スウィフト・アドベンチャー","スウィフト・〇〇〇アドベンチャー","レコードメダルとグリードの位置
をミニマップに表示、自パーティ
の移動速度を上昇させる。
覚醒で効果時間が延長。","レコードメダル&グリードサーチ発動+移動速度上昇","数多の冒険を経験した彼女に応援
されれば逆転の発想が閃くかも。
フレー!フレー!まだまだいける
よ!","slog_216000050","slog_216000050","Chara_icon/216000050","Chara_icon/216000050","2020/09/22 7:00:00",True,"1", -202000120,999,"《チアフル・リーダー》アスナ",4,1,800,15000,100,1,0,0,0,"s202000120",,40,90,40,85,45,80,50,75,55,70,60,65,10,20,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_ASU_SUP_016","リカバー・ディレクション","リカバー・〇〇〇ディレクション","自パーティの耐久値と状態異常を
回復し、STRを10秒間200%
上昇させる。
覚醒で回復量が上昇。","耐久値と状態異常を回復+自パーティのSTR上昇","副団長として攻略組を率いた彼女
の統率力で、皆も一致団結。
みんな、もうひと踏ん張りだよ!
頑張ってね♪","slog_202000120","slog_202000120","Chara_icon/202000120","Chara_icon/202000120","2020/09/22 7:00:00",True,"1", -203000120,999,"《チアフル・ジャンプ》リーファ",4,2,800,15000,100,1,0,0,0,"s203000120",,60,60,66,58,71,56,75,54,78,52,80,50,10,20,"ボーナス増加量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_LEA_SUP_016","エフィシェント・コール","エフィシェント・〇〇〇〇コール","10秒間、ラストアタックで
得られるボーナスポイントが
増加する。
覚醒で増加量が上昇。","ラストアタックで得られるボーナスポイント量上昇","エネルギッシュな妖精の応援を
受ければ、攻撃も冴え渡るはず。
精一杯応援するよ!あたしの元気
を分けてあげる!","slog_203000120","slog_203000120","Chara_icon/203000120","Chara_icon/203000120","2020/09/22 7:00:00",True,"1", -206000090,999,"《チアフル・エール》シリカ",4,2,800,15000,100,1,0,0,0,"s206000090",,60,60,66,58,71,56,75,54,78,52,80,50,10,20,"ボーナス増加量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_SIL_SUP_016","ファースト・スピリット","ファースト・〇〇〇〇スピリット","10秒間、ファーストアタックで
得られるボーナスポイントが
増加する。
覚醒で増加量が上昇。","ファーストアタックで得られるボーナスポイント量上昇","アイドル的な愛らしさを持つ
彼女の声援で、皆の士気も上昇
間違いなし。
みなさーん!ファイトですよっ!","slog_206000090","slog_206000090","Chara_icon/206000090","Chara_icon/206000090","2020/09/22 7:00:00",True,"1", -202000130,999,"《愛を紡ぐ天使》アスナ",5,2,1600,20000,100,1,0,0,0,"s202000130",,80,200,86,185,91,172,95,161,98,153,100,150,10,20,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_ASU_SUP_036","エンジェル・ブレッシング","エンジェル・〇〇ブレッシング","自パーティに全属性への耐性を
付与するとともに、
耐久値を徐々に回復する。
覚醒で効果時間が延長。","全属性耐性+リジェネレイト付与","純白の羽根に包まれた美しき天使
は、豊かな愛を教えてくれる。
一片の穢れ無き清らかな想いが
世界を導くことだろう。","slog_202000130","slog_202000130","Chara_icon/202000130","Chara_icon/202000130","2020/10/13 7:00:00",True,"1", -212000040,999,"《闇の貴公子》ユージオ",4,3,800,15000,100,1,0,0,0,"s212000040",,25,180,30,170,33,160,36,155,38,150,40,145,5,10,"召喚増強レベル","{0}レベル","skill_3_2line_moji86","un_2line_moji86","CH_EUG_SUP_036","コール・デーモン","コール・デーモン","強力な悪魔系Mobを
2体召喚し、戦闘支援を
受ける。覚醒で召喚Mobの
レベルが増強。","お助けMob【悪魔系】を召喚","凍てついた闇のオーラを纏った
漆黒の貴公子。全てを手にし、意
のままにする彼の、それでもなお
満たされぬ心を埋めるものとは。","slog_212000040","slog_212000040","Chara_icon/212000040","Chara_icon/212000040","2020/10/13 7:00:00",True,"1", -201000070,999,"《宵闇の戦士》キリト",5,3,1600,20000,100,1,0,0,0,"s201000070",,60,130,70,118,80,106,85,100,90,94,95,88,10,20,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_KIR_SUP_036","デーモン・ブロー","デーモン・ブロー","闇属性の強力な広範囲攻撃を放ち
敵にダメージと麻痺状態を
与える。
覚醒で攻撃力上昇。","闇属性の広範囲攻撃+麻痺を付与","夜闇を彷徨う最強の戦士。彼に
敵わぬ相手など、この世界のどこ
にもいない。それでも彼は頂点を
目指して、矛を振るい続ける。","slog_201000070","slog_201000070","Chara_icon/201000070","Chara_icon/201000070","2020/10/13 7:00:00",True,"1", -211000040,999,"《光輝の天使》アリス",4,1,800,15000,100,1,0,0,0,"s211000040",,45,85,50,75,55,70,60,65,65,60,70,55,10,20,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_ALI_SUP_036","ホーリー・セラピー","ホーリー・〇〇〇〇〇セラピー","戦闘不能状態のメンバーを2人
復帰させ、自パーティのSTRを
10秒間100%上昇させる。
覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+自パーティのSTR上昇","光り輝く美しき天使は、規律ある
正しき世の中へと導いてくれる。
世界中が思いやりにあふれ、秩序
ある暮らしが保たれますように。","slog_211000040","slog_211000040","Chara_icon/211000040","Chara_icon/211000040","2020/10/13 7:00:00",True,"1", -290000100,999,"《愛しき闇天使》ユイ",3,2,400,10000,100,3,0,0,0,"s290000100",,800,300,820,299,840,298,860,297,880,296,900,295,10,20,"ボーナス増加量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_YUI_SUP_036","ヘヴンアンドヘル","ヘヴンアンドヘル","【獲得ポイントが3位の時のみ
発動可能】30秒間、ラスト
アタックボーナスポイントが
増加する。覚醒で増加量が上昇。","【発動条件・3位】ラストアタックボーナスポイント上昇","愛情深き天使と最強の戦士の間に
生まれた子は、両翼の愛らしい
闇天使。彼女こそが、二つの世界
の架け橋となることだろう。","slog_290000100","slog_290000100","Chara_icon/290000100","Chara_icon/290000100","2020/10/13 7:00:00",True,"1", -204000090,999,"《揺るぎない信愛》シノン",5,2,1600,20000,100,1,0,0,0,"s204000090",,20,200,22,194,24,189,26,185,27,182,28,180,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_SIN_SUP_022","トラスト・オブ・ラブ","トラスト・〇〇〇〇オブ・ラブ","自パーティのSTRを120%、
リーグポイント獲得量を30%、
移動速度も上昇させる。
覚醒で効果時間が延長。","自パーティのSTR&リーグポイント獲得量&移動速度上昇","普段はクールな彼女も、絆を結ん
だ伴侶と寄り添い、穏やかな笑み
を浮かべる。こんな日が来るとは
ね…嬉しいに決まってるわよ。","slog_204000090","slog_204000090","Chara_icon/204000090","Chara_icon/204000090","2020/07/21 7:00:00",True,"1", -209000100,999,"《夢へ至る純愛》ユウキ",5,3,1600,20000,100,1,0,0,0,"s209000100",,30,300,34,298,38,296,40,294,42,292,44,290,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_YUU_SUP_022","ピュアリー・ウィッシュ","ピュアリー・〇〇〇ウィッシュ","レコードメダルを再配置。メダル
の位置をミニマップに表示し、
リーグポイント獲得量も50%
増加。覚醒で効果時間が延長。","メダル再配置+サーチ発動+リーグポイント獲得量上昇","傍らに佇むは絆を結んだ伴侶。少
女の儚い願いが、花開く瞬間だっ
た。こんなに幸せでいいのかな?
嬉しくて仕方ないんだ。","slog_209000100","slog_209000100","Chara_icon/209000100","Chara_icon/209000100","2020/07/21 7:00:00",True,"1", -201000080,999,"《運命の寵愛》キリト",4,3,800,15000,100,1,0,0,0,"s201000080",,10,90,12,85,14,81,15,79,16,77,17,75,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_KIR_SUP_022","ブラッキー・ブライド","ブラッキー・〇〇〇〇ブライド","攻撃中のメンバーのスキルを
再使用可能にした後、自パーティ
の攻撃に闇属性を付与する。
覚醒で効果時間が延長。","クールタイムスキップ+闇属性付与","はぁ、仕方ないな…たまには流さ
れてみますか。そんなことを言う
彼女(?)だが、堂に入った花嫁
姿に、伴侶も思わず困惑ぎみ。","slog_201000080","slog_201000080","Chara_icon/201000080","Chara_icon/201000080","2020/07/21 7:00:00",True,"1", -202000140,999,"《美貌の愛妻》アスナ",4,3,800,15000,100,1,0,0,0,"s202000140",,10,90,12,85,14,81,15,79,16,77,17,75,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_ASU_SUP_022","ビューティ・ブライド","ビューティ・〇〇〇〇ブライド","攻撃中のメンバーのスキルを
再使用可能にした後、自パーティ
の攻撃に火属性を付与する。
覚醒で効果時間が延長。","クールタイムスキップ+火属性付与","純白のドレスを纏った彼女は、眩
い閃光の如く美しい。絆を結んだ
伴侶は幸せ者だろう。わたしね、
今日のことは永遠に忘れないよ。","slog_202000140","slog_202000140","Chara_icon/202000140","Chara_icon/202000140","2020/07/21 7:00:00",True,"1", -203000130,999,"《豊かなる恵愛》直葉",4,3,800,15000,100,1,0,0,0,"s203000130",,10,90,12,85,14,81,15,79,16,77,17,75,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_LEA_SUP_022","グレイス・ブライド","グレイス・〇〇〇〇〇ブライド","攻撃中のメンバーのスキルを
再使用可能にした後、自パーティ
の攻撃に風属性を付与する。
覚醒で効果時間が延長。","クールタイムスキップ+風属性付与","豊かな想いを胸に、一歩踏み出す
少女の姿は、祝福する誰もの心を
温かく満たす。あたし、今…とっ
ても満たされてるんです。","slog_203000130","slog_203000130","Chara_icon/203000130","Chara_icon/203000130","2020/07/21 7:00:00",True,"1", -205000090,999,"《端麗な深愛》リズベット",4,3,800,15000,100,1,0,0,0,"s205000090",,10,90,12,85,14,81,15,79,16,77,17,75,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_LIS_SUP_022","スナッズ・ブライド","スナッズ・〇〇〇〇〇ブライド","攻撃中のメンバーのスキルを
再使用可能にした後、自パーティ
の攻撃に土属性を付与する。
覚醒で効果時間が延長。","クールタイムスキップ+土属性付与","美しいウェディングドレスを纏う
彼女は喜びを露に伴侶を見やる。
見惚れちゃった?あたしとしては
ちょっと自信あるんだけど。","slog_205000090","slog_205000090","Chara_icon/205000090","Chara_icon/205000090","2020/07/21 7:00:00",True,"1", -206000100,999,"《純真な情愛》シリカ",4,3,800,15000,100,1,0,0,0,"s206000100",,10,90,12,85,14,81,15,79,16,77,17,75,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_SIL_SUP_022","プリティ・ブライド","プリティ・〇〇〇〇〇ブライド","攻撃中のメンバーのスキルを
再使用可能にした後、自パーティ
の攻撃に水属性を付与する。
覚醒で効果時間が延長。","クールタイムスキップ+水属性付与","ドレスを纏いあどけない笑顔を見
せる彼女に、絆を結んだ伴侶も喜
ばしいだろう。ウェディングドレ
ス姿は、女の子の憧れですよね!","slog_206000100","slog_206000100","Chara_icon/206000100","Chara_icon/206000100","2020/07/21 7:00:00",True,"1", -211000050,999,"《高潔なる忠愛》アリス",4,3,800,15000,100,1,0,0,0,"s211000050",,10,90,12,85,14,81,15,79,16,77,17,75,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_ALI_SUP_022","エレガント・ブライド","エレガント・〇〇〇〇ブライド","攻撃中のメンバーのスキルを
再使用可能にした後、自パーティ
の攻撃に聖属性を付与する。
覚醒で効果時間が延長。","クールタイムスキップ+聖属性付与","美しい衣装に思わず顔を綻ばせる
も、騎士の矜持で素直に受け入れ
られない様子。せ、整合騎士たる
私がこのような…しかし…!","slog_211000050","slog_211000050","Chara_icon/211000050","Chara_icon/211000050","2020/07/21 7:00:00",True,"1", -202000150,999,"《彩華の水精》アスナ",5,2,1600,20000,100,1,0,0,0,"s202000150",,160,130,170,120,180,110,190,105,195,100,200,95,20,40,"INT上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_ASU_SUP_019","ブライト・パーティ","ブライト・〇〇〇〇〇パーティ","15秒間、自パーティの
INT・移動速度が上昇する。
覚醒でINT上昇量が増加。","自パーティのINT&移動速度上昇","パーティドレスに身を包み、華や
かな出で立ち。白を基調とした
シンプルなドレスは彼女の美しさ
を引き立てること請け合いだ。","slog_202000150","slog_202000150","Chara_icon/202000150","Chara_icon/202000150","2020/08/11 7:00:00",True,"1", -290000110,999,"《祝福の愛妖精》ユイ",4,3,800,15000,100,1,0,0,0,"s290000110",,17,255,20,245,23,240,25,235,27,230,28,225,4,8,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_YUI_SUP_019","パフォーム・パーティ","パフォーム・〇〇〇パーティ","グリードからの注目を集め、
リーグポイント獲得量が
20%上昇する。
覚醒で効果時間が延長。","グリードからの注目&リーグポイント獲得量上昇","晴れの舞台に、おめかしして
ちょっぴり背伸びをしてみるも、
美味しそうな果物の誘惑には
勝てなかった様子で愛らしい。","slog_290000110","slog_290000110","Chara_icon/290000110","Chara_icon/290000110","2020/08/11 7:00:00",True,"1", -203000140,999,"《祝風の舞踊》リーファ",5,3,1600,20000,100,1,0,0,0,"s203000140",,20,120,23,114,26,109,29,105,31,102,32,100,4,8,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_LEA_SUP_019","チャーミー・パーティ","チャーミー・〇〇〇パーティ","敵の注目を集め、リーグポイント
獲得量が20%上昇。範囲内の
全ユニットのSTRも120%
上昇。覚醒で効果時間が延長。","敵からの注目&リーグポイント上昇+全ユニットSTR上昇","爽やかなライムグリーンのドレス
は、快活な彼女にぴったりだ。
大胆な胸元で、無意識のうちに
皆を魅了してしまうことだろう。","slog_203000140","slog_203000140","Chara_icon/203000140","Chara_icon/203000140","2020/08/11 7:00:00",True,"1", -205000100,999,"《華麗な踊匠》リズベット",4,1,800,15000,100,1,0,0,0,"s205000100",,50,27,55,25,60,23,65,22,70,21,75,20,10,20,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_LIS_SUP_019","ヒーリング・パーティ","ヒーリング・〇〇〇パーティ","自パーティ全員の耐久値を回復し
状態異常も解除する。
覚醒で回復量が上昇。","耐久値と状態異常を回復","深い赤とピンクのコントラストが
美しいドレスに身を包んで、決め
ポーズ。本人は冗談めかしている
が、その魅力は本物だ。","slog_205000100","slog_205000100","Chara_icon/205000100","Chara_icon/205000100","2020/08/11 7:00:00",True,"1", -298000100,999,"《絆の女神》リーファ&シノン",5,3,1600,20000,100,1,0,0,0,"s298000100",,40,130,50,120,55,112,60,106,65,102,70,100,10,20,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_SUP_SUP_014","天地の鼓動","天地の鼓苔","強力な広範囲攻撃を行う。
その攻撃中、自パーティの耐久値
が徐々に回復し続ける。
覚醒で攻撃力が上昇。","強力な広範囲攻撃+リジェネレイト付与","大切な人を守るため、大切な友を
助けるため、少女たちは戦場へと
その身を投じる。女神たちの絆が
世界に奇跡を呼ぶことだろう。","slog_298000100","slog_298000100","Chara_icon/298000100","Chara_icon/298000100","2020/07/09 7:00:00",False,"1", -299000420,999,"《大戦》アリシゼーションWoU",5,3,1600,20000,100,1,0,0,0,"s299000420",,20,185,23,177,26,169,28,165,29,161,30,157,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"アンダーワールド・ラストホープ","アンダーワールド・〇ラストホープ","レコードメダルを再配置。メダルの位置をミニマップに表示し、自パーティの移動速度を上昇。
覚醒で効果時間が延長。","メダル再配置+サーチ発動+移動速度上昇","アンダーワールドに襲い来る悪意
の手に、立ち向かうは伝承の三女
神。光の巫女の、この世界の危機
に、英雄は再び立ち上がる。","slog_299000420","slog_299000420","Chara_icon/299000420","Chara_icon/299000420","2020/07/11 7:00:00",False,"1", -299000430,999,"《華》アリシゼーション・リコリス",5,2,1600,20000,100,1,0,0,0,"s299000430",,80,75,86,73,91,71,95,69,98,67,100,65,10,20,"ボーナス増加量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"ユア・メモライズ","ユア・メモライズ","15秒間、ファーストアタックで得られるボーナスポイントが増加する。
覚醒で増加量が上昇。","ファーストアタックで得られるボーナスポイント量上昇","動乱のアンダーワールドを生きる
剣士たちは、それぞれの信念を胸
に、正しき道を目指す。その高潔
な姿は一輪の華のよう。","slog_299000430","slog_299000430","Chara_icon/299000430","Chara_icon/299000430","2020/07/09 7:00:00",True,"1", -299000440,999,"《彼岸花の君》メディナ",4,2,800,15000,100,1,0,0,0,"s299000440",,30,180,34,172,36,168,38,164,40,160,42,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"クランズ・プライド","クランズ・〇〇〇〇プライド","自パーティのSTRを20%上昇させ、リーグポイントの獲得量も25%増加させる。
覚醒で効果時間が延長。","自パーティのSTR上昇+リーグポイントの獲得量上昇","オルティナノス家の汚名を雪ぐ
べく奮戦する若き九代目当主。
本来穏やかな気質だが、
剣を取ると好戦的になる一面も。","slog_299000440","slog_299000440","Chara_icon/299000440","Chara_icon/299000440","2020/07/14 7:00:00",True,"1", -299000450,999,"《柔靭なる剣士》ソルティリーナ",4,3,800,15000,100,1,1,0,0,"s299000450",,55,110,52,104,49,98,46,92,44,88,43,86,-5,-10,"生育時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"エフォート・ブロッサム","エフォート・〇〇〇ブロッサム","種を蒔く。一定時間後、花が咲きレコードメダルがランダムな量ドロップする。
覚醒で生育時間が短縮。","レコードメダルが実る花の種を蒔く","北セントリア帝立修剣学院の
三年生で学院次席の上級修剣士。
片手剣と鞭を併用した柔の剣術
《セルルト流》の後継者。","slog_299000450","slog_299000450","Chara_icon/299000450","Chara_icon/299000450","2020/07/14 7:00:00",True,"1", -299000460,999,"《図書室の賢者》カーディナル",4,1,800,15000,100,1,0,0,0,"s299000460",,30,85,35,75,40,70,45,65,50,60,55,55,10,20,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"ラスト・サポート","ラスト・サポート","戦闘不能状態のメンバーを2人復帰させ、自パーティの攻撃に10秒間麻痺を付与させる。
覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+自パーティの攻撃に麻痺付与","《セントラル・カセドラル》の
大図書室に住まう世界の理を知る
者。ある事情により外へ出られず
鳥や虫を介して外界を見守る。","slog_299000460","slog_299000460","Chara_icon/299000460","Chara_icon/299000460","2020/07/14 7:00:00",True,"1", -299000470,999,"《絶対的支配者》アドミニストレータ",4,3,800,15000,100,1,0,0,0,"s299000470",,20,105,30,103,40,101,50,99,60,97,70,95,10,20,"メダル増加量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"アブソリュート・ルーラー","アブソリュート・〇〇〇ルーラー","一定範囲内のグリードをゴールデン・グリードに転生し、さらに周囲のMobに魅了を付与する。
覚醒で排出メダル量が増加。","グリードをゴールデン・グリードに転成+周囲のMobを魅了","公理教会の最高司祭。禁忌目録を
制定し、絶対的な支配力で人界の
全ての民の頂点に君臨する。","slog_299000470","slog_299000470","Chara_icon/299000470","Chara_icon/299000470","2020/07/14 7:00:00",True,"1", -299000480,999,"《愛の忠臣》チュデルキン",4,3,800,15000,100,1,0,0,0,"s299000480",,40,165,50,151,55,144,60,137,65,130,70,123,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"デーモン・オブ・ファイア","デーモン・オブ・〇〇〇ファイア","周囲に炎の弾をばら撒いて
広範囲攻撃を行い、
敵にダメージを与える。
覚醒で攻撃力が上昇。","火属性の広範囲攻撃","アドミニストレータに絶対の忠誠
を誓う、公理教会元老長。体格を
利用し、最大二十の素因を同時に
操る強力な神聖術の使い手。","slog_299000480","slog_299000480","Chara_icon/299000480","Chara_icon/299000480","2020/07/14 7:00:00",True,"1", -204000100,999,"《可憐な蒼華》シノン",5,2,1600,20000,100,1,0,0,0,"s204000100",,22,145,27,137,32,129,35,125,38,121,40,117,5,10,"ボーナス増加量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_SIN_SUP_019","ビューティ・パーティ","ビューティ・〇〇〇パーティ","60秒間、ファーストアタックと
ラストアタックで得られる
ボーナスポイントが増加する。
覚醒で増加量が上昇。","ファースト&ラストアタックのボーナスポイント量上昇","普段はクールな彼女も、今日は
ミニ丈のワンピースで甘めな
コーデ。魅惑的な装いでパーティ
会場に花を添える。","slog_204000100","slog_204000100","Chara_icon/204000100","Chara_icon/204000100","2020/10/06 7:00:00",True,"1", -206000110,999,"《遊宴の竜姫》シリカ",4,1,800,15000,100,1,0,0,0,"s206000110",,30,85,35,75,40,70,45,65,50,60,55,55,10,20,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_SIL_SUP_019","グロウン・パーティ","グロウン・〇〇〇〇パーティ","戦闘不能状態のメンバーを2人
復帰させ、自パーティのINTを
10秒間120%上昇させる。
覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+自パーティのINT上昇","涼やかなドレスに身を包み、今夜
はちょっぴりオトナな気分?
祝いの宴を楽しみながらも、
お姫様のようにおしとやかに。","slog_206000110","slog_206000110","Chara_icon/206000110","Chara_icon/206000110","2020/10/06 7:00:00",True,"1", -209000110,999,"《祝宴の輝珠》ユウキ",5,3,1600,20000,100,1,0,0,0,"s209000110",,65,210,68,204,71,201,73,198,75,196,77,194,10,20,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_YUU_SUP_019","スマイル・パーティ","スマイル・〇〇〇〇パーティ","周囲に闇属性の弾をばら撒いて
広範囲攻撃を行い、
敵にダメージを与える。
覚醒で攻撃力が上昇。","闇属性の広範囲攻撃","みんなと一緒におめかしして楽し
むパーティに、眩しい笑顔が弾け
る。常と違うフォーマルな衣装も
特別な時間を演出してくれる。","slog_209000110","slog_209000110","Chara_icon/209000110","Chara_icon/209000110","2020/10/06 7:00:00",True,"1", -220000040,999,"《静淑の蒼星》サチ",4,2,800,15000,100,1,0,0,0,"s220000040",,30,180,34,172,36,168,38,164,40,160,42,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"フリーティング・パーティ","フリーティング・〇〇〇パーティ","敵と他パーティから感知されず、リーグポイントの獲得量も25%増加させる。
覚醒で効果時間が延長。","ハイディング+リーグポイントの獲得量上昇","深い青が印象的な美しいドレスは
夜空にまたたく星々を思わせる。
小さく、しかし鮮烈な輝きは
彼女の儚さを一層引き立てる。","slog_220000040","slog_220000040","Chara_icon/220000040","Chara_icon/220000040","2020/10/06 7:00:00",True,"1", -215000050,999,"《無邪気な愛》ストレア",4,2,800,15000,100,1,0,0,0,"s215000050",,30,180,34,172,36,168,38,164,40,160,42,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ビーターズ・ラバー","ビーターズ・〇〇〇〇ラバー","レコードメダルの位置をミニマップに表示し、リーグポイントの獲得量を25%増加させる。
覚醒で効果時間が延長。","レコードメダルの位置表示+リーグポイントの獲得量上昇","《SAO》でキリト達に救われ、
以来、様々なゲームで共に冒険し
てきた。明るく無邪気で、その
愛情表現は時に大胆に見える。","slog_215000050","slog_215000050","Chara_icon/215000050","Chara_icon/215000050","2020/07/28 7:00:00",True,"1", -216000060,999,"《宝物はここに》フィリア",4,2,800,15000,100,1,0,0,0,"s216000060",,30,180,34,172,36,168,38,164,40,160,42,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ビーターズ・トレジャー","ビーターズ・〇〇トレジャー","レコードメダルの位置をミニマップに表示し、リーグポイントの獲得量を25%増加させる。
覚醒で効果時間が延長。","レコードメダルの位置表示+リーグポイントの獲得量上昇","《SAO》の《ホロウ・エリア》で
キリトに救われた少女。以前より
トレジャーハンターを名乗る彼女
は仲間と言う宝物を手に入れた。","slog_216000060","slog_216000060","Chara_icon/216000060","Chara_icon/216000060","2020/07/28 7:00:00",True,"1", -218000030,999,"《七色の夢》レイン",4,2,800,15000,100,1,0,0,0,"s218000030",,30,180,34,172,36,168,38,164,40,160,42,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ビーターズ・ドリーム","ビーターズ・〇〇〇ドリーム","レコードメダルの位置をミニマップに表示し、リーグポイントの獲得量を25%増加させる。
覚醒で効果時間が延長。","レコードメダルの位置表示+リーグポイントの獲得量上昇","《ALO》でキリト達と出会った
多刀流使いの少女。アイドルとし
て人々を魅了したいという夢に
向かい、日々邁進中。","slog_218000030","slog_218000030","Chara_icon/218000030","Chara_icon/218000030","2020/07/28 7:00:00",True,"1", -217000020,999,"《真の繋がり》セブン",4,2,800,15000,100,1,0,0,0,"s217000020",,30,180,34,172,36,168,38,164,40,160,42,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ビーターズ・リンク","ビーターズ・〇〇〇〇リンク","レコードメダルの位置をミニマップに表示し、リーグポイントの獲得量を25%増加させる。
覚醒で効果時間が延長。","レコードメダルの位置表示+リーグポイントの獲得量上昇","《ALO》でキリト達と出会った
天才研究者。人々の繋がりに可能
性を見出した彼女だが、レインの
お陰で真の絆を知った。","slog_217000020","slog_217000020","Chara_icon/217000020","Chara_icon/217000020","2020/07/28 7:00:00",True,"1", -299000500,999,"《芽吹きの時》プレミア",4,2,800,15000,100,1,0,0,0,"s299000500",,30,180,34,172,36,168,38,164,40,160,42,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ビーターズ・グロウス","ビーターズ・〇〇〇グロウス","レコードメダルの位置をミニマップに表示し、リーグポイントの獲得量を25%増加させる。
覚醒で効果時間が延長。","レコードメダルの位置表示+リーグポイントの獲得量上昇","《SA:O》でキリト達と出会った
特別なクエストNPC。皆との交流
を通じて、本物の感情を芽生えさ
せてゆく。","slog_299000500","slog_299000500","Chara_icon/299000500","Chara_icon/299000500","2020/07/28 7:00:00",True,"1", -299000510,999,"《私の証明》クレハ",4,2,800,15000,100,1,0,0,0,"s299000510",,30,180,34,172,36,168,38,164,40,160,42,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ビーターズ・プルーフ","ビーターズ・〇〇〇プルーフ","レコードメダルの位置をミニマップに表示し、リーグポイントの獲得量を25%増加させる。
覚醒で効果時間が延長。","レコードメダルの位置表示+リーグポイントの獲得量上昇","主人公を《GGO》へ誘った幼馴染
で、ベテランプレイヤー。明るく
世話焼きな彼女だが、強さを追い
求めるストイックな面も。","slog_299000510","slog_299000510","Chara_icon/299000510","Chara_icon/299000510","2020/07/28 7:00:00",True,"1", -299000520,999,"《無冠の女王》ツェリスカ",4,2,800,15000,100,1,0,0,0,"s299000520",,30,180,34,172,36,168,38,164,40,160,42,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ビーターズ・クイーン","ビーターズ・〇〇〇クイーン","レコードメダルの位置をミニマップに表示し、リーグポイントの獲得量を25%増加させる。
覚醒で効果時間が延長。","レコードメダルの位置表示+リーグポイントの獲得量上昇","《GGO》で主人公やキリト達と
出会った凄腕ソロプレイヤー。
相棒のアファシスを溺愛してい
る。","slog_299000520","slog_299000520","Chara_icon/299000520","Chara_icon/299000520","2020/07/28 7:00:00",True,"1", -299000530,999,"《特別な存在》イツキ",4,2,800,15000,100,1,0,0,0,"s299000530",,30,180,34,172,36,168,38,164,40,160,42,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ビーターズ・スペシャル","ビーターズ・〇〇スペシャル","レコードメダルの位置をミニマップに表示し、リーグポイントの獲得量を25%増加させる。
覚醒で効果時間が延長。","レコードメダルの位置表示+リーグポイントの獲得量上昇","《GGO》で出会ったプレイヤー。
戦闘、策略共に長けた人気者。
次第に主人公に対する執着心を
垣間見せるようになり…。","slog_299000530","slog_299000530","Chara_icon/299000530","Chara_icon/299000530","2020/07/28 7:00:00",True,"1", -299000540,999,"《踏み出す一歩》コハル",4,2,800,15000,100,1,0,0,0,"s299000540",,30,180,34,172,36,168,38,164,40,160,42,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ビーターズ・パートナー","ビーターズ・〇〇パートナー","レコードメダルの位置をミニマップに表示し、リーグポイントの獲得量を25%増加させる。
覚醒で効果時間が延長。","レコードメダルの位置表示+リーグポイントの獲得量上昇","《SAO》で君と共に攻略を目指す
若きプレイヤー。βテストで
出会ったのを切っ掛けに、正式稼
働後も一緒に冒険している。","slog_299000540","slog_299000540","Chara_icon/299000540","Chara_icon/299000540","2020/07/28 7:00:00",True,"1", -201000090,999,"《異世界の英雄》キリト",4,2,800,15000,100,1,0,0,0,"s201000090",,10,50,12,45,14,41,15,39,16,37,17,35,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ライズアップ・ヒーロー","ライズアップ・〇〇〇ヒーロー","自パーティのSTR・VIT・INT
を100%上昇させる。
覚醒で効果時間が延長。","自パーティのSTR・VIT・INT上昇","様々なVRMMOで活躍してきた
英雄《黒の剣士》。最先端フルダ
イブ技術で生まれた全く新しい世
界で、彼は一体何と出会うのか。","slog_201000090","slog_201000090","Chara_icon/201000090","Chara_icon/201000090","2020/07/14 7:00:00",True,"1", -202000160,999,"《愛する人へ》アスナ",4,2,800,15000,100,1,0,0,0,"s202000160",,30,60,33,58,36,56,38,54,39,52,40,50,10,20,"ボーナス増加量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"ディアレスト・エンラージ","ディアレスト・〇〇エンラージ","10秒間、獲得リーグポイントが増加する。覚醒で増加量が上昇。【キリトが前衛時に発動すると効果時間2倍】","【効果上昇・キリト】獲得リーグポイント上昇","失踪したキリトを追って《アン
ダーワールド》へダイブした。
愛する人の無事を確かめたい一心
で、未知の世界を奔走する。","slog_202000160","slog_202000160","Chara_icon/202000160","Chara_icon/202000160","2020/08/11 7:00:00",True,"1", -203000150,999,"《大切な家族》リーファ",4,2,800,15000,100,1,0,0,0,"s203000150",,60,60,66,58,71,56,75,54,78,52,80,50,10,20,"ボーナス増加量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"ディアレスト・スピアヘッド","ディアレスト・〇スピアヘッド","10秒間、ファーストアタックでのボーナスポイントが増加。覚醒で増加量が上昇。【キリトが前衛時に発動すると効果時間2倍】","【効果上昇・キリト】ファーストアタックで得られるボーナスポイント量上昇","失踪したキリトを追って《アン
ダーワールド》へダイブした。
大切な兄のためならば、どんな
苦難だって乗り越えてみせる。","slog_203000150","slog_203000150","Chara_icon/203000150","Chara_icon/203000150","2020/08/11 7:00:00",True,"1", -204000110,999,"《強さを知って》シノン",4,2,800,15000,100,1,0,0,0,"s204000110",,60,60,66,58,71,56,75,54,78,52,80,50,10,20,"ボーナス増加量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"ディアレスト・リアー","ディアレスト・〇〇〇〇リアー","10秒間、ラストアタックでのボーナスポイントが増加。覚醒で増加量が上昇。【キリトが前衛時に発動すると効果時間2倍】","【効果上昇・キリト】ラストアタックで得られるボーナスポイント量上昇","失踪したキリトを追って《アン
ダーワールド》へダイブした。
磨いてきた技術を、強さとは何か
教えてくれた貴方のために。","slog_204000110","slog_204000110","Chara_icon/204000110","Chara_icon/204000110","2020/08/11 7:00:00",True,"1", -205000110,999,"《この温度を君に》リズベット",4,2,800,15000,100,1,0,0,0,"s205000110",,160,60,165,58,170,56,175,54,178,52,180,50,10,20,"STR上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"ディアレスト・ビルドアップ","ディアレスト・〇ビルドアップ","10秒間、自パーティのSTRを上昇させる。覚醒でSTR上昇量が増加。【キリトが前衛時に発動すると効果時間2倍】","【効果上昇・キリト】STR上昇","失踪したキリトを追って《アン
ダーワールド》へダイブした。
心の温度を教えてくれた君に、
今度はあたしが熱を届けたい。","slog_205000110","slog_205000110","Chara_icon/205000110","Chara_icon/205000110","2020/08/11 7:00:00",True,"1", -206000120,999,"《今度はあたしが》シリカ",4,2,800,15000,100,1,0,0,0,"s206000120",,160,60,165,58,170,56,175,54,178,52,180,50,10,20,"INT上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"ディアレスト・サポート","ディアレスト・〇〇〇サポート","10秒間、自パーティのINTが上昇する。覚醒でINT上昇量が増加。【キリトが前衛時に発動すると効果時間2倍】","【効果上昇・キリト】INT上昇","失踪したキリトを追って《アン
ダーワールド》へダイブした。
沢山助けてくれた大切な人を、
今度は自身が助けると誓って。","slog_206000120","slog_206000120","Chara_icon/206000120","Chara_icon/206000120","2020/08/11 7:00:00",True,"1", -212000050,999,"《傍らの相棒》ユージオ",4,2,800,15000,100,1,0,0,0,"s212000050",,200,60,205,58,210,56,215,54,218,52,220,50,10,20,"ダメージ増加量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"バディ・レジスタンス","バディ・〇〇〇〇レジスタンス","10秒間、ボスに与えるダメージを上昇させる。覚醒で攻撃力上昇量が増加。【キリトが前衛時に発動すると効果時間2倍】","【効果上昇・キリト】ボスキラー","キリトが《アンダーワールド》で
出会った青年。かの地での冒険を
常に共にした、親友とも呼ぶべき
相棒だ。","slog_212000050","slog_212000050","Chara_icon/212000050","Chara_icon/212000050","2020/08/11 7:00:00",True,"1", -299000550,999,"《整合騎士長》ベルクーリ",4,3,800,15000,100,1,0,0,0,"s299000550",,40,165,50,151,55,144,60,137,65,130,70,123,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"タイム・スプリット","タイム・〇〇〇〇スプリット","10秒後、発動した地点にVITと耐性を無視する強力な斬属性の広範囲攻撃が発生する。覚醒で攻撃力が上昇。","10秒後、発動地点に強力な広範囲攻撃","《公理教会》の整合騎士を束ねる
整合騎士長。飄々として掴み所の
無い印象も受けるが、自信と威厳
にあふれた風格も併せ持つ。","slog_299000550","slog_299000550","Chara_icon/299000550","Chara_icon/299000550","2020/09/08 7:00:00",True,"1", -299000560,999,"《天穿の光》ファナティオ",4,3,800,15000,100,1,0,0,0,"s299000560",,40,165,50,151,55,144,60,137,65,130,70,123,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"ヘヴン・ピアス","ヘヴン・ピアス","一定時間、前方にレーザーを照射し敵にダメージを与える。覚醒で攻撃力が上昇。","一定時間、前方にレーザーを照射","《公理教会》の整合騎士第二位の
副騎士団長。自身の性別に劣等感
を抱いていたが、キリトに敗北し
たことを機に徐々に受け入れる。","slog_299000560","slog_299000560","Chara_icon/299000560","Chara_icon/299000560","2020/09/08 7:00:00",True,"1", -299000570,999,"《焔の弓騎士》デュソルバート",4,3,800,15000,100,1,0,0,0,"s299000570",,40,165,50,151,55,144,60,137,65,130,70,123,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"フレイム・アロー","フレイム・アロー","一定時間、前方に炎の矢を連射して敵にダメージを与える。覚醒で攻撃力が上昇。","一定時間、前方に炎の矢を連射","《公理教会》の整合騎士の一人で
炎の弓《熾焔弓》の使い手。剛毅
な武人で、その実力は折り紙付。","slog_299000570","slog_299000570","Chara_icon/299000570","Chara_icon/299000570","2020/09/08 7:00:00",True,"1", -299000580,999,"《師への敬愛》エルドリエ",4,2,800,15000,100,1,0,0,0,"s299000580",,35,70,38,68,41,66,43,64,44,62,45,60,10,20,"ボーナス増加量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"リスペクト・フォー・メンター","リスペクト・〇〇フォー・メンター","10秒間、獲得リーグポイントが増加する。覚醒で増加量が上昇。【アリスが前衛時に発動すると効果時間2倍】","【効果上昇・アリス】獲得リーグポイント上昇","《公理教会》の整合騎士として
召喚されたばかりの三十一番目の
騎士。アリスに師事しており、
彼女を敬愛している。","slog_299000580","slog_299000580","Chara_icon/299000580","Chara_icon/299000580","2020/09/08 7:00:00",True,"1", -299000590,999,"《黒百合の一閃》シェータ",4,2,800,15000,100,1,0,0,0,"s299000590",,10,50,12,45,14,41,15,39,16,37,17,35,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ブラックリリィ","ブラックリリィ","自パーティのSTRを200%上昇させ、周囲の敵のVITを大幅に低下させる。
覚醒で効果時間が延長。","自パーティのSTR上昇+周囲の敵のVITを低下","《公理教会》の整合騎士の一人で
《無音》の二つ名を持つ。あらゆ
るものを斬るという神器《黒百合
の剣》の使い手。","slog_299000590","slog_299000590","Chara_icon/299000590","Chara_icon/299000590","2020/09/08 7:00:00",True,"1", -299000600,999,"《失くした片翼》レンリ",4,3,800,15000,100,1,0,0,0,"s299000600",,40,165,50,151,55,144,60,137,65,130,70,123,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"ツイン・ハンター","ツイン・ハンター","斬属性の誘導弾を二発射出し、敵にダメージを与える。覚醒で攻撃力が上昇。","斬属性の誘導弾を射出","《公理教会》の整合騎士の一人だ
が、《武装完全支配術》を使うこ
とが出来ず、失敗作の烙印を押さ
れて何年も凍結されていた。","slog_299000600","slog_299000600","Chara_icon/299000600","Chara_icon/299000600","2020/09/08 7:00:00",True,"1", -211000060,999,"《誇り高き騎士》アリス",4,3,800,15000,100,1,0,0,0,"s211000060",,40,165,50,151,55,144,60,137,65,130,70,123,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"フレグラント・オリーブ","フレグラント・〇〇〇〇オリーブ","斬属性の誘導弾をばら撒き、敵にダメージを与える。覚醒で攻撃力が上昇。","斬属性の誘導弾を射出","誇りと強固な意志、そして圧倒的
な強さを持つ高潔な整合騎士。
人界の民を守るため、成すべき
ことを成すために戦う。","slog_211000060","slog_211000060","Chara_icon/211000060","Chara_icon/211000060","2020/10/20 7:00:00",True,"1", -299000610,999,"《姉想う祈り》セルカ",4,2,800,15000,100,1,0,0,0,"s299000610",,30,60,33,58,36,56,38,54,39,52,40,50,10,20,"ボーナス増加量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"シスターズ・グレイス","シスターズ・〇〇〇グレイス","10秒間、獲得リーグポイントが増加する。覚醒で増加量が上昇。【アリスが前衛時に発動すると効果時間2倍】","【効果上昇・アリス】獲得リーグポイント上昇","アリスの実の妹。ルーリッドの村
で神聖術を学んでいる。優秀だっ
た姉に追いつこうと懸命に努力を
続けている。","slog_299000610","slog_299000610","Chara_icon/299000610","Chara_icon/299000610","2020/10/20 7:00:00",True,"1", -299000620,999,"《正しき道》ロニエ",4,2,800,15000,100,1,0,0,0,"s299000620",,30,60,33,58,36,56,38,54,39,52,40,50,10,20,"ボーナス増加量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"ジャスティス・ウィル","ジャスティス・〇〇〇〇〇ウィル","10秒間、獲得リーグポイントが増加する。覚醒で増加量が上昇。【キリトが前衛時に発動すると効果時間2倍】","【効果上昇・キリト】獲得リーグポイント上昇","北セントリア帝立修剣学院で
キリトの傍付きを務めた下級
修剣士。彼の教えを胸に、自身の
信ずる正しき道を歩まんとする。","slog_299000620","slog_299000620","Chara_icon/299000620","Chara_icon/299000620","2020/10/20 7:00:00",True,"1", -299000630,999,"《受け継ぐ希望》ティーゼ",4,2,800,15000,100,1,0,0,0,"s299000630",,30,60,33,58,36,56,38,54,39,52,40,50,10,20,"ボーナス増加量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"リリーブ・ウィル","リリーブ・ウィル","10秒間、獲得リーグポイントが増加する。覚醒で増加量が上昇。【ユージオが前衛時に発動すると効果時間2倍】","【効果上昇・ユージオ】獲得リーグポイント上昇","北セントリア帝立修剣学院で
ユージオの傍付きを務めた下級
修剣士。救ってくれた彼を今度は
自身が助けようと修練に励む。","slog_299000630","slog_299000630","Chara_icon/299000630","Chara_icon/299000630","2020/10/20 7:00:00",True,"1", -299000640,999,"《無邪気な毒牙》フィゼル",4,2,800,15000,100,1,0,0,0,"s299000640",,60,120,65,115,70,110,75,105,78,102,80,100,10,20,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"パラライズ・プレイ","パラライズ・〇〇〇〇プレイ","Mobに与えるダメージを100%上昇させ、自パーティの攻撃に麻痺を付与させる。覚醒で効果時間が延長。","Mobキラー+自パーティの攻撃に麻痺付与","《公理教会》の整合騎士見習い。
リネルと共に、整合騎士として
認めてもらおうと手柄を立てる
機会を狙う。","slog_299000640","slog_299000640","Chara_icon/299000640","Chara_icon/299000640","2020/10/20 7:00:00",True,"1", -299000650,999,"《穏やかな殺戮》リネル",4,2,800,15000,100,1,0,0,0,"s299000650",,60,120,65,115,70,110,75,105,78,102,80,100,10,20,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"パラライズ・フィニッシュ","パラライズ・〇〇フィニッシュ","ボスに与えるダメージを100%上昇させ、自パーティの攻撃に麻痺を付与させる。覚醒で効果時間が延長。","ボスキラー+自パーティの攻撃に麻痺付与","《公理教会》の整合騎士見習い。
フィゼルと共に、アドミニスト
レータの実験体となっていた過去
があり、急所を突くのが得意。","slog_299000650","slog_299000650","Chara_icon/299000650","Chara_icon/299000650","2020/10/20 7:00:00",True,"1", -202000170,999,"《納涼の令嬢》アスナ",5,3,1600,20000,100,1,0,0,0,"s202000170",,65,210,68,204,71,201,73,198,75,196,77,194,10,20,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_ASU_SUP_024","ウォーター・スプラッシュ","ウォーター・〇〇スプラッシュ","一定時間周囲に留まった後、敵に向かって飛んで行く水の弾を呼び出す。覚醒で攻撃力が上昇。","水属性の弾による強力な攻撃","納涼に訪れたお嬢様は、涼やかな
色合いの上品な水着に身を包み、
仲間たちと楽しむ。その優雅な姿
はさながらサマープリンセス。","slog_202000170","slog_202000170","Chara_icon/202000170","Chara_icon/202000170","2020/08/18 7:00:00",True,"1", -204000120,999,"《真夏の誘惑》シノン",5,2,1600,20000,100,1,0,0,0,"s204000120",,5,150,6,145,7,140,8,135,9,132,10,130,1,2,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_SIN_SUP_024","サマー・ブースト","サマー・ブースト","自パーティのSTRとラストアタックで得られるボーナスポイントが500%増加する。覚醒で効果時間が延長。","STR上昇+ラストアタックで得られるボーナスポイント量上昇","真夏の熱砂も、彼女の涼やかな
魅力には敵わない。彼女こそ、
クールビューティならぬ、サマー
ビューティに相応しい。","slog_204000120","slog_204000120","Chara_icon/204000120","Chara_icon/204000120","2020/08/18 7:00:00",True,"1", -203000160,999,"《優美な夏姫》直葉",4,1,800,15000,100,1,0,0,0,"s203000160",,40,90,45,85,50,81,52,79,54,77,55,75,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_LEA_SUP_024","トロピカル・リフレッシュ","トロピカル・〇〇リフレッシュ","戦闘不能状態のメンバーを2人復帰させ、自パーティに自動回復を付与する。
覚醒で効果時間が延長。","戦闘不能状態のメンバー2人復帰+リジェネレイト付与","夏らしい快活な水着に身を包み、
手にするは南国のトロピカル
フルーツ。瑞々しい果実に喉を
潤す、優美な夏の過ごし方だ。","slog_203000160","slog_203000160","Chara_icon/203000160","Chara_icon/203000160","2020/08/18 7:00:00",True,"1", -208000030,999,"《海の漢》エギル",4,3,800,15000,100,1,0,0,0,"s208000030",,25,180,30,170,33,160,36,155,38,150,40,145,5,10,"召喚増強レベル","{0}レベル","skill_3_2line_moji86","un_2line_moji86","CH_AGI_SUP_007","マグロ・バディ","マグロ・バディ","お助けMobスピリット・ザ・グースロードを召喚し、戦闘支援を受ける。覚醒で召喚Mobのレベルが増強。","お助けMob【スピリット・ザ・グースロード】を召喚","夏と言えば海!海と言えば漢!
漢エギル、真夏の海に繰り出して
獲って来たるは巨大なマグロ!!
どうだ?立派なモンだろう?","slog_208000030","slog_208000030","Chara_icon/208000030","Chara_icon/208000030","2020/08/18 7:00:00",True,"1", -209000120,999,"《南風の爛漫娘》ユウキ",4,2,800,15000,100,1,0,0,0,"s209000120",,60,120,65,115,70,110,75,105,78,102,80,100,10,20,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_YUU_SUP_024","フレッシュ・スイープ","フレッシュ・〇〇〇スイープ","Mobに与えるダメージを200%上昇させる。覚醒で効果時間が延長。","Mobキラー","元気いっぱいの天真爛漫娘には、
真夏の海が良く似合う!
フレッシュなグレープフルーツを
片手に、愛らしい笑顔が弾ける。","slog_209000120","slog_209000120","Chara_icon/209000120","Chara_icon/209000120","2020/08/18 7:00:00",True,"1", -210000020,999,"《炎天の疾影》アルゴ",4,2,800,15000,100,1,0,0,0,"s210000020",,10,150,12,140,14,135,15,130,16,125,17,120,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_ARG_SUP_024","クイック・バケーション","クイック・〇〇〇バケーション","ファーストアタックで得られる
ボーナスポイントが100%
増加し、移動速度も上昇する。
覚醒で効果時間が延長。","ファーストアタックのボーナスポイント量&移動速度上昇","普段はマントに隠れた身体を、今
だけは惜しみなく太陽の元に晒す
彼女。常でない姿に動揺した誰か
さんの姿に、彼女も楽し気だ。","slog_210000020","slog_210000020","Chara_icon/210000020","Chara_icon/210000020","2020/08/18 7:00:00",True,"1", -216000070,999,"《清夏の羽休め》フィリア",4,2,800,15000,100,1,0,0,0,"s216000070",,40,180,44,172,46,168,48,164,50,160,52,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"クール・ハンター","クール・ハンター","リーグポイントの獲得量を25%増加させる。覚醒で効果時間が延長。","リーグポイントの獲得量上昇","シンプルな水着にガーリーな小物
が良く似合う、クールさと可愛ら
しさが両立した出で立ち。夏を
楽しむ準備は万端!","slog_216000070","slog_216000070","Chara_icon/216000070","Chara_icon/216000070","2020/08/18 7:00:00",True,"1", -297000120,999,"《夏空の遊翼》ユイ&ピナ",4,3,800,15000,100,1,0,0,0,"s297000120",,40,165,50,151,55,144,60,137,65,130,70,123,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_SUP_SUP_013","サマー・アドベンチャー","サマー・〇〇〇〇アドベンチャー","10秒間、レコードメダルの位置をミニマップに表示する。さらにピナが現れブレスで敵を攻撃。覚醒で攻撃力が上昇。","レコードメダルの位置表示+ピナがブレスで攻撃","小さな身体に好奇心をめいっぱい
詰め込んで飛び出せば、なんだっ
て煌めいて見える。1人と1匹の
真夏の大冒険が今、始まる!","slog_297000120","slog_297000120","Chara_icon/297000120","Chara_icon/297000120","2020/08/18 7:00:00",True,"1", -217000030,999,"《盛夏の花形》セブン",5,2,1600,20000,100,1,0,0,0,"s217000030",,12,190,13,182,14,178,15,174,16,170,17,166,1,2,"ボーナス増加量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"フェスティバル・アイドル","フェスティバル〇〇〇・アイドル","85秒間、グリードからの注目を集め、獲得リーグポイントを増加させる。覚醒でボーナス増加量が上昇。","グリードからの注目+リーグポイントの獲得量上昇","アイドルらしいキュートなミニ丈
浴衣で、夏を彩るゲリラリサイタ
ル!?その愛らしさと歌声に、誰
もが魅了されること間違いなし。","slog_217000030","slog_217000030","Chara_icon/217000030","Chara_icon/217000030","2020/08/25 7:00:00",True,"1", -205000120,999,"《艶やか彩花》リズベット",4,2,800,15000,100,1,0,0,0,"s205000120",,10,180,11,172,12,168,13,164,14,160,15,156,1,2,"ボーナス増加量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_LIS_SUP_025","フェスティバル・リポート","フェスティバル〇〇〇・リポート","75秒間、レコードメダルの位置をミニマップに表示し、獲得リーグポイントを増加させる。覚醒でボーナス増加量が上昇。","レコードメダルの位置表示+リーグポイントの獲得量上昇","夏祭りと言えば美味しい物に綺麗
な花火…楽しみがいっぱい。全部
満喫するには時間が足りないくら
い!女子高生は欲張りなのです。","slog_205000120","slog_205000120","Chara_icon/205000120","Chara_icon/205000120","2020/08/25 7:00:00",True,"1", -206000130,999,"《祭礼の彩り》シリカ",4,2,800,15000,100,1,0,0,0,"s206000130",,10,180,11,172,12,168,13,164,14,160,15,156,1,2,"ボーナス増加量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_SIL_SUP_025","フェスティバル・メイクアップ","フェスティバル〇〇・メイクアップ","75秒間、グリードの位置をミニマップに表示し、獲得リーグポイントを増加させる。覚醒でボーナス増加量が上昇。","グリードサーチ発動+リーグポイントの獲得量上昇","愛らしい薄桃色の浴衣に身を包み
相棒と一緒に待ち合わせ場所へ。
大好きな人の反応を想像し、胸を
膨らませる。似合ってますか…?","slog_206000130","slog_206000130","Chara_icon/206000130","Chara_icon/206000130","2020/08/25 7:00:00",True,"1", -218000040,999,"《賑わいの明光》レイン",4,2,800,15000,100,1,0,0,0,"s218000040",,10,180,11,172,12,168,13,164,14,160,15,156,1,2,"ボーナス増加量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_RAI_SUP_025","フェスティバル・プロモーション","フェスティバル〇・プロモーション","75秒間、攻撃時に25%の確率で追加攻撃を発生させ、獲得リーグポイントを増加させる。覚醒でボーナス増加量が上昇。","攻撃時に確率で追加攻撃+リーグポイントの獲得量上昇","黒を基調とした少し大人びた浴衣
は、彼女の奥ゆかしい性格をよく
表している。賑わう会場で、花火
の光をうけ輝く彼女は美しい。","slog_218000040","slog_218000040","Chara_icon/218000040","Chara_icon/218000040","2020/08/25 7:00:00",True,"1", -299000660,999,"《攻略者憩う店》テリア",4,2,800,15000,100,1,0,0,0,"s299000660",,30,180,34,172,36,168,38,164,40,160,42,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ビーターズ・マネージャー","ビーターズ・〇〇マネージャー","レコードメダルの位置をミニマップに表示し、リーグポイントの獲得量を25%増加させる。
覚醒で効果時間が延長。","レコードメダルの位置表示+リーグポイントの獲得量上昇","ここは攻略者が集う憩いの場、
《βeater's cafe》。
最新情報と癒しを届けるべく、
幼き店長は今日も奮闘中です。","slog_299000660","slog_299000660","Chara_icon/299000660","Chara_icon/299000660","2020/07/28 7:00:00",True,"1", -299000670,999,"《未確認領域の女帝》ブリーゼ",4,3,800,15000,100,1,0,0,0,"s299000670",,40,165,50,151,55,144,60,137,65,130,70,123,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_BRE_SUP_004","インビジブル・マスターマインド","インビジブル・〇マスターマインド","ボス部屋へ移動するワープポイン
トを短時間設置し、その後に聖属
性・闇属性の広範囲攻撃を行う。
覚醒で攻撃力が上昇。","ワープポイント設置+広範囲攻撃","Unknownに現れた、黒幕と
名乗る女性。リコを操り、キリト
達《SAO生還者》の記憶を集めて
鑑賞しようとしていた。","slog_299000670","slog_299000670","Chara_icon/299000670","Chara_icon/299000670","2020/10/06 7:00:00",True,"1", -299000680,999,"《皮肉屋な相棒》ニュイ",4,2,800,15000,100,1,0,0,0,"s299000680",,180,165,200,151,220,144,230,137,235,130,240,123,10,20,"ボーナス増加量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_NUI_SUP_005","プロヴォーキング・フィクサー","プロヴォーキング〇〇・フィクサー","15秒間、自パーティのINTが200%上昇し、ラストアタックのボーナスも増加する。
覚醒でボーナス増加量が上昇。","INT上昇+ラストアタックで得られるボーナスポイント量上昇","Unknownに現れた、黒幕と
名乗る猫で、ブリーゼの相棒。
憎まれ口が多い皮肉屋だが、ブ
リーゼの事は尊重する素振りも。","slog_299000680","slog_299000680","Chara_icon/299000680","Chara_icon/299000680","2020/10/06 7:00:00",True,"1", -297000130,999,"《真の姿》ブリーゼ",5,3,1600,20000,100,1,0,0,0,"s297000130",,65,210,68,204,71,201,73,198,75,196,77,194,10,20,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_BRE_SUP_005","ライトアンドダーク","ライトアンド〇〇〇〇〇ダーク","聖属性と闇属性の弾による
広範囲攻撃を行い、
敵にダメージを与える。
覚醒で攻撃力が上昇。","聖属性と闇属性の広範囲攻撃","菊岡によって、ブリーゼの現実の
姿が明かされる。過酷な環境に
おかれた幼き彼女が望むものと
は、一体――。","slog_297000130","slog_297000130","Chara_icon/297000130","Chara_icon/297000130","2020/10/06 7:00:00",True,"1", -297000140,999,"《育む絆》風音&リコ",5,2,1600,20000,100,1,0,0,0,"s297000140",,35,190,40,182,43,178,46,174,48,170,50,166,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_KAZ_SUP_004","フレンドリー・リクアイタル","フレンドリー・〇リクアイタル","レコードメダルとグリードの位置をミニマップに表示し、リーグポイントの獲得量を30%増加させる。覚醒で効果時間が延長。","レコードメダル&グリードサーチ発動+リーグポイントの獲得量上昇","管理者権限を返却し、Unknown
を立ち去る風音とニュイ。彼らに
リコが声をかける。またここで
会える日を楽しみに。","slog_297000140","slog_297000140","Chara_icon/297000140","Chara_icon/297000140","2020/10/06 7:00:00",True,"1", -290000120,999,"《朝露の少女》ユイ",3,2,400,10000,100,1,0,0,0,"s290000120",,20,50,22,48,24,46,26,44,27,43,28,42,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_YUI_SUP_002","アナライズ・サポート","アナライズ・〇〇〇サポート","スキルポーションと
レコードメダル、グリードの
位置をミニマップに表示する。
覚醒で効果時間が延長。","スキルポーション&レコードメダル&グリードサーチ発動","《SAO》でキリトとアスナが
出会った少女。本来は《MHCP》
と呼ばれるプレイヤーのメンタル
をフォローするAIだった。","slog_290000120","slog_290000120","Chara_icon/290000120","Chara_icon/290000120","2020/10/06 7:00:00",True,"1", -291000050,999,"《あなたに寄り添う》リコ",3,2,400,10000,100,1,0,0,0,"s291000050",,30,75,32,70,34,65,36,61,37,59,38,57,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_REC_SUP_005","アナライズ・サポート","アナライズ・〇〇〇サポート","スキルポーションと
レコードメダル、グリードの
位置をミニマップに表示する。
覚醒で効果時間が延長。","スキルポーション&レコードメダル&グリードサーチ発動","元々はプレイヤーの持つ携帯端末
に搭載されたレコメンドAI。
カーディナルに人格を付与され
NPC化されていた。","slog_291000050","slog_291000050","Chara_icon/291000050","Chara_icon/291000050","2020/10/06 7:00:00",True,"1", -292000020,999,"《仮想空間管理課》菊岡誠二郎",3,2,400,10000,100,1,0,0,0,"s292000020",,40,100,42,93,44,86,46,79,47,75,48,72,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_KIK_SUP_002","アナライズ・サポート","アナライズ・〇〇〇サポート","スキルポーションと
レコードメダル、グリードの
位置をミニマップに表示する。
覚醒で効果時間が延長。","スキルポーション&レコードメダル&グリードサーチ発動","通称《仮想課》に所属する役人。
SAO事件の際には、率先して動き
多くの人命を助けた。キリト達に
協力を仰ぎUnknown調査を行う。","slog_292000020","slog_292000020","Chara_icon/292000020","Chara_icon/292000020","2020/10/06 7:00:00",True,"1", -209000130,999,"《キューティ・ウルフ》ユウキ",5,2,1600,20000,100,1,0,0,0,"s209000130",,30,180,32,160,34,155,35,150,36,145,37,140,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_YUU_SUP_027","ハロウィンロリポップ","ハロウィン〇〇〇〇ロリポップ","ファーストアタックで得られる
ボーナスポイントが150%
増加し、移動速度も上昇する。
覚醒で効果時間が延長。","ファーストアタックのボーナスポイント量&移動速度上昇","愛らしい人狼が、ハロウィンの夜
に大暴れ!?美味しいお菓子に
誘われて、可愛いイタズラでおね
だり攻撃!","slog_209000130","slog_209000130","Chara_icon/209000130","Chara_icon/209000130","2020/10/27 7:00:00",True,"1", -201000100,999,"《ブラッキー・ヴァンプ》キリト",4,3,800,15000,100,1,0,0,0,"s201000100",,25,180,30,170,33,160,36,155,38,150,40,145,5,10,"召喚増強レベル","{0}レベル","skill_3_2line_moji86","un_2line_moji86","CH_KIR_SUP_027","ブラッディー・トリック","ブラッディー・〇〇〇トリック","お助けMobスフィアドラコを6体召喚し、戦闘支援を受ける。覚醒で召喚Mobのレベルが増強。","お助けMob【スフィアドラコ】を召喚","今宵は1年で最も特別な夜。漆黒
の闇より生まれ出し古の血族も、
その力を目覚めさせる。命が惜し
くば、お菓子を献上?!","slog_201000100","slog_201000100","Chara_icon/201000100","Chara_icon/201000100","2020/10/27 7:00:00",True,"1", -202000180,999,"《ラグー・ウィッチ》アスナ",4,2,800,15000,100,1,0,0,0,"s202000180",,15,50,18,45,21,41,23,39,24,37,25,35,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_ASU_SUP_027","スペシャル・シチュー","スペシャル・〇〇〇〇シチュー","自パーティのSTR・INT
を100%上昇させる。
覚醒で効果時間が延長。","自パーティのSTR・INT上昇","美しき魔女が取り出したるは、
パンプキン仕様のラグー・ラビッ
ト!窯で煮込んだ絶品シチューで
皆をおもてなし。","slog_202000180","slog_202000180","Chara_icon/202000180","Chara_icon/202000180","2020/10/27 7:00:00",True,"1", -203000170,999,"《ムーンナイト・ウルフ》リーファ",4,3,800,15000,100,1,0,0,0,"s203000170",,40,165,50,151,55,144,60,137,65,130,70,123,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_LEA_SUP_027","ジャック・オ・ランタン","ジャック・オ・〇〇〇〇ランタン","低速で敵を追尾する打属性の誘導弾を発射し、敵にダメージを与える。覚醒で攻撃力が上昇。","打属性の誘導弾による攻撃","特別な夜、ぽっかり浮かんだ満月
に狼の血が騒ぐ!解き放たれた
美しき獣を鎮めるには、美味しい
お菓子が必要かも?!","slog_203000170","slog_203000170","Chara_icon/203000170","Chara_icon/203000170","2020/10/27 7:00:00",True,"1", -218000050,999,"《笑顔の奉迎》レイン",5,2,1600,20000,100,1,0,0,0,"s218000050",,55,190,60,182,65,178,70,174,75,170,80,166,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_RAI_SUP_013","スマイリー・サービス","スマイリー・〇〇〇サービス","Mobに与えるダメージを100%上昇させ、リーグポイントの獲得量も20%増加させる。
覚醒で効果時間が延長。","Mobキラー+リーグポイントの獲得量上昇","おもてなしならお手の物!
明るい笑顔でお出迎えされて
ご主人様も思わず笑顔。
誰もがメイドさんの虜に。","slog_218000050","slog_218000050","Chara_icon/218000050","Chara_icon/218000050","2020/11/17 7:00:00",True,"1", -204000140,999,"《氷静の差添》シノン",4,2,800,15000,100,1,0,0,0,"s204000140",,50,180,55,172,60,168,65,164,70,160,75,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_SIN_SUP_013","シャイ・サービス","シャイ・サービス","ボスに与えるダメージを100%上昇させ、リーグポイントの獲得量も15%増加させる。
覚醒で効果時間が延長。","ボスキラー+リーグポイントの獲得量上昇","不慣れな衣装に戸惑いが隠せない
様子だが、そこが良い!と熱狂的
ファンが続出?!照れながらの
接客に皆メロメロだ。","slog_204000140","slog_204000140","Chara_icon/204000140","Chara_icon/204000140","2020/11/17 7:00:00",True,"1", -206000140,999,"《桜袖の応援》シリカ",4,2,800,15000,100,1,0,0,0,"s206000140",,50,180,55,172,60,168,65,164,70,160,75,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_SIL_SUP_013","ラブリー・サービス","ラブリー・〇〇〇サービス","自パーティのINTを100%上昇させ、リーグポイントの獲得量も15%増加させる。
覚醒で効果時間が延長。","自パーティのINT上昇+リーグポイントの獲得量上昇","和風メイド服に身を包んだ可憐な
少女。愛情たっぷりに応援され
れば、みんなが元気になること
請け合い。","slog_206000140","slog_206000140","Chara_icon/206000140","Chara_icon/206000140","2020/11/17 7:00:00",True,"1", -215000060,999,"《奉仕の戦華》ストレア",4,2,800,15000,100,1,0,0,0,"s215000060",,50,180,55,172,60,168,65,164,70,160,75,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"グラマー・サービス","グラマー・〇〇〇サービス","自パーティのSTRを50%上昇させ、リーグポイントの獲得量も15%増加させる。
覚醒で効果時間が延長。","自パーティのSTR上昇+リーグポイントの獲得量上昇","豊かな身体を活かした魅惑的な
メイドさん。献身的なご奉仕に
胸を高鳴らせない人はいない
だろう。","slog_215000060","slog_215000060","Chara_icon/215000060","Chara_icon/215000060","2020/11/17 7:00:00",True,"1", -202000190,999,"《輝夜の月うさぎ》アスナ",5,3,1600,20000,100,3,0,0,0,"s202000190",,2,180,2,175,3,170,3,165,4,162,4,160,1,2,"召喚体数","{0}体","skill_3_2line_moji86","un_2line_moji86","CH_ASU_SUP_026","グレイテスト・シャイニング","グレイテスト・〇シャイニング","【獲得ポイントが3位の時のみ発動可能】グリードを召喚。
覚醒で召喚数が増加。","【発動条件・3位】グリードを召喚","満月に宿った不思議な力で、
世界を浄化する美しきうさぎ。
今宵は月夜の奇跡をあなたに。","slog_202000190","slog_202000190","Chara_icon/202000190","Chara_icon/202000190","2020/11/03 7:00:00",True,"1", -211000070,999,"《佳宵の月うさぎ》アリス",5,3,1600,20000,100,1,0,0,0,"s211000070",,30,80,40,78,50,76,60,74,70,72,80,70,10,20,"メダル増加量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_ALI_SUP_026","シャイニング・トリップ","シャイニング・〇〇〇トリップ","一定範囲内にいるグリードを
メダル排出量の多いゴールデン・
グリードに転成させる。
覚醒でメダル排出量が増加。","グリードをゴールデン・グリードに転成","満月の神秘を身に纏い、
舞い踊るは佳宵の剣舞。
軽やかに舞い跳ぶ姿はうさぎ
そのもの。","slog_211000070","slog_211000070","Chara_icon/211000070","Chara_icon/211000070","2020/11/03 7:00:00",True,"1", -203000180,999,"《暁の月うさぎ》リーファ",4,2,800,15000,100,1,0,0,0,"s203000180",,12,90,14,80,16,75,17,70,18,65,19,60,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_LEA_SUP_026","シャイニー・ラピッド","シャイニー・〇〇〇〇ラピッド","レコードメダルとグリードの位置
をミニマップに表示、自パーティ
の移動速度を上昇させる。
覚醒で効果時間が延長。","レコードメダル&グリードサーチ発動+移動速度上昇","満月を見上げ、人々は祈りを、
願いを心に祭りを行う。月より
来たるうさぎが、きっとその祈り
を月まで運んでくれるから。","slog_203000180","slog_203000180","Chara_icon/203000180","Chara_icon/203000180","2020/11/03 7:00:00",True,"1", -204000130,999,"《宵闇の月うさぎ》シノン",4,2,800,15000,100,1,0,0,0,"s204000130",,180,60,200,58,220,56,230,54,235,52,240,50,10,20,"ボーナス増加量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_SIN_SUP_026","シャイニー・プレイヤー","シャイニー・〇〇〇プレイヤー","5秒間、ファーストアタックで
得られるボーナスポイントが
増加する。
覚醒で増加量が上昇。","ファーストアタックで得られるボーナスポイント量上昇","満月の強大な力は、邪な物をも
惹きつける。全ての闇を浄化する
はうさぎの使命。力を宿した弓で
敵を屠る。","slog_204000130","slog_204000130","Chara_icon/204000130","Chara_icon/204000130","2020/11/03 7:00:00",True,"1", -209000140,999,"《黄昏の月うさぎ》ユウキ",3,2,400,10000,100,1,0,0,0,"s209000140",,10,40,12,35,14,31,15,29,16,27,17,25,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_YUU_SUP_026","シャイニー・アタッカー","シャイニー・〇〇〇アタッカー","自パーティのSTRを100%上昇させる。
覚醒で効果時間が延長。","自パーティのSTR上昇","満月の夜は特別な夜。清らかな
願いと、邪な闇が集う夜。月の
神秘を担うは、5人の美しき
うさぎ達。","slog_209000140","slog_209000140","Chara_icon/209000140","Chara_icon/209000140","2020/11/03 7:00:00",True,"1", -299000690,999,"《光照らす時》メモリー・デフラグ",5,2,1600,20000,100,1,0,0,0,"s299000690",,30,200,33,193,36,186,39,179,42,172,45,165,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"レジェンド・オブ・メモリー","レジェンド・〇〇オブ・メモリー","自パーティのSTR・VIT・INT
を100%上昇させ、リーグポイントの獲得量も25%増加させる。覚醒で効果時間が延長。","自パーティのSTR・VIT・INT上昇+リーグポイントの獲得量上昇","三柱の女神と英雄が立ち上がる時
暗黒に覆われた世界に光が射す。
傍らには掛け替えのない友が――
これは、新たな伝説の1ページ。","slog_299000690","slog_299000690","Chara_icon/299000690","Chara_icon/299000690","2020/10/06 7:00:00",True,"1", -202000200,999,"《雪華の美姫》アスナ",5,2,1600,20000,100,1,0,0,0,"s202000200",,30,85,36,82,42,79,48,76,54,73,60,70,4,8,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"スノー・マジック","スノー・マジック","自パーティのSTR・VIT・INT
を70%上昇させる。
覚醒で効果時間が延長。","自パーティのSTR・VIT・INT上昇","雪の煌めきを施した美しいドレス
を纏う姿は、正しく雪華を統べる
姫君であろう。冬を彩る美しい
御姿だ。","slog_202000200","slog_202000200","Chara_icon/202000200","Chara_icon/202000200","2020/12/01 7:00:00",True,"1", -203000190,999,"《プリンセスオリオン》直葉",4,2,800,15000,100,1,0,0,0,"s203000190",,30,50,31,49,32,48,33,47,34,46,35,45,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"スターライト・ギフト","スターライト・〇〇〇〇ギフト","自パーティのSTR・INTを
50%上昇させる。
覚醒で効果時間が延長。","自パーティのSTR・INT上昇","誰もが目を奪われる愛くるしい冬
の装い。姫の秘めたる思いは、
心温まるプレゼントに込めて。","slog_203000190","slog_203000190","Chara_icon/203000190","Chara_icon/203000190","2020/12/01 7:00:00",True,"1", -204000150,999,"《聖夜の妖精》シノン",4,2,800,15000,100,1,0,0,0,"s204000150",,30,50,31,49,32,48,33,47,34,46,35,45,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"スノー・フェアリー","スノー・〇〇〇〇フェアリー","自パーティのSTRを50%
上昇させ、さらにボスに与える
ダメージを30%上昇させる。
覚醒で効果時間が延長。","自パーティのSTR上昇+ボスキラー","煌めくドレスに、彼女の涼やかな
瞳も相まって、その美しさは正に
雪の妖精のよう。","slog_204000150","slog_204000150","Chara_icon/204000150","Chara_icon/204000150","2020/12/01 7:00:00",True,"1", -209000150,999,"《ウインタークラウス》ユウキ",4,2,800,15000,100,1,0,0,0,"s209000150",,30,50,31,49,32,48,33,47,34,46,35,45,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ウインター・ギフト","ウインター・〇〇〇〇ギフト","自パーティのSTRを50%上昇させ、周囲の敵のVITを大幅に低下させる。覚醒で効果時間が延長。","自パーティのSTR上昇+周囲の敵のVITを低下","小柄な彼女には大きすぎるくらい
のコートも、元気いっぱいの彼女
には関係ない!さあ、皆に幸せを
届けに行こう!!","slog_209000150","slog_209000150","Chara_icon/209000150","Chara_icon/209000150","2020/12/01 7:00:00",True,"1", -215000070,999,"《冬宴の魅雪》ストレア",5,3,1600,20000,100,1,0,0,0,"s215000070",,65,210,68,204,71,201,73,198,75,196,77,194,10,20,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"クリスマス・ビッグラブ","クリスマス・〇〇〇ビッグラブ","一定時間後に爆発する
巨大なプレゼントを投下する。
覚醒で攻撃力が上昇。","プレゼントによる強力な広範囲攻撃","丈の短いサンタ衣装で大人の魅力
たっぷり。雪をも魅了する艶やか
なサンタさんが、皆に幸福をくれ
る。","slog_215000070","slog_215000070","Chara_icon/215000070","Chara_icon/215000070","2020/12/15 7:00:00",True,"1", -202000210,999,"《妖精のクリスマス》アスナ",4,3,800,15000,100,1,0,0,0,"s202000210",,40,165,50,151,55,144,60,137,65,130,70,123,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_ASU_SUP_029","ダウンプア・プレゼント","ダウンプア・〇〇〇プレゼント","プレゼントを大量に飛ばして
周囲を攻撃する。
覚醒で攻撃力が上昇。","プレゼントによる広範囲攻撃","涼やかな水妖精の髪色と、クリス
マスらしい赤のコントラストが
眩しい。妖精界にもクリスマスが
やってきた。","slog_202000210","slog_202000210","Chara_icon/202000210","Chara_icon/202000210","2020/12/15 7:00:00",True,"1", -205000130,999,"《秘める恋心》リズベット",4,3,800,15000,100,1,0,0,0,"s205000130",,1,180,1,175,2,170,2,165,3,162,3,160,1,2,"ドロップ数","{0}個","skill_3_2line_moji86","un_2line_moji86","CH_LIS_SUP_029","シークレット・プレゼント","シークレット・〇〇〇プレゼント","グリードを2体召喚。
覚醒でスキルポーションのドロップ数が増加。","グリードを召喚","乙女の心はとっても複雑。あの人
だけのサンタクロースになりたく
て、けれどなかなか素直に渡せ
なくて…。","slog_205000130","slog_205000130","Chara_icon/205000130","Chara_icon/205000130","2020/12/15 7:00:00",True,"1", -220000050,999,"《聖夜の贈り物》サチ",4,3,800,15000,100,1,0,0,0,"s220000050",,25,180,30,170,33,160,36,155,38,150,40,145,5,10,"召喚増強レベル","{0}レベル","skill_3_2line_moji86","un_2line_moji86",,"サンタクロース・カミング","サンタクロース・〇〇カミング","お助けMob背教者ニコラスを
召喚し、戦闘支援を受ける。
覚醒で召喚Mobの
レベルが増強。","お助けMob【背教者ニコラス】を召喚","今日は君だけのサンタさん。一夜
限りの奇跡を携えて、少女は
雪舞う聖夜に微笑む。","slog_220000050","slog_220000050","Chara_icon/220000050","Chara_icon/220000050","2020/12/15 7:00:00",True,"1", -204000160,999,"《温泉の弓姫》シノン",5,3,1600,20000,100,1,0,0,0,"s204000160",,65,210,68,204,71,201,73,198,75,196,77,194,10,20,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_SIN_SUP_028","ベイシン・シャワー","ベイシン・〇〇〇〇シャワー","乱れ飛ぶ風呂桶で周囲の敵に
打属性のダメージを与える。
覚醒で攻撃力が上昇。","打属性の強力な広範囲攻撃","温かな湯の湧く天然温泉。偶然見
つけたその場所で、入浴を楽しむ
少女が一人。偶然であれ出くわせ
ば、叱責の怒号が――。","slog_204000160","slog_204000160","Chara_icon/204000160","Chara_icon/204000160","2021/01/19 7:00:00",True,"1", -201000110,999,"《湯上り剣士》キリト",4,3,800,15000,100,1,0,0,0,"s201000110",,40,165,50,151,55,144,60,137,65,130,70,123,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_KIR_SUP_028","ホットスプリング・スチーム","ホットスプリング〇〇・スチーム","湯気を身に纏い触れた敵に
火属性のダメージを与えつつ、
自パーティに自動回復を付与。
覚醒で攻撃力が上昇。","火属性の攻撃+リジェネレイト付与","男性アバターだと分かっていても
何故かドギマギしてしまう美少女
ぶり。その視線に気付かぬは本人
ばかり…?","slog_201000110","slog_201000110","Chara_icon/201000110","Chara_icon/201000110","2021/01/19 7:00:00",True,"1", -202000220,999,"《背流し癒湯姫》アスナ",4,3,800,15000,100,1,0,0,0,"s202000220",,40,165,50,151,55,144,60,137,65,130,70,123,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_ASU_SUP_028","ホットスプリング・スプラッシュ","ホットスプリング・スプラッシュ","周囲で間欠泉を噴き上げ、
火属性のダメージを与える。
覚醒で攻撃力が上昇。","火属性の広範囲攻撃","洗い場に佇む美しい少女。折角
一緒にいるんだものと目配せを
受けたら、こちらがくらりとして
しまいそう。","slog_202000220","slog_202000220","Chara_icon/202000220","Chara_icon/202000220","2021/01/19 7:00:00",True,"1", -206000160,999,"《仲良し洗いっこ》シリカ",4,1,800,15000,100,1,0,0,0,"s206000160",,30,85,35,75,40,70,45,65,50,60,55,55,10,20,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_SIL_SUP_028","ホットスプリング・ヒーリング","ホットスプリング〇・ヒーリング","戦闘不能状態のメンバーを2人復帰させ、20秒間自パーティに自動回復と状態異常予防を付与。
覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+リジェネレイト付与+状態異常予防付与","いつでも一緒の仲良しコンビ、
今日は揃って体を洗いっこ。元気
な小竜も、ちゃんとじっとしてい
られて偉かったね。","slog_206000160","slog_206000160","Chara_icon/206000160","Chara_icon/206000160","2021/01/19 7:00:00",True,"1", -202000230,999,"《甘々さくらもち》アスナ",5,1,1600,20000,100,1,0,0,0,"s202000230",,50,100,55,98,60,96,65,94,70,92,75,90,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"サクラ・ヒール","サクラ・ヒール","戦闘不能状態のメンバーを2人復帰させ、自パーティに自動回復を付与する。
覚醒で効果時間が延長。","戦闘不能状態のメンバー2人復帰+リジェネレイト付与","甘くて美味しい和菓子はいかが?
愛らしいお給仕さんと、優しく
とろける美味しい和菓子に
魅了される人続出。","slog_202000230","slog_202000230","Chara_icon/202000230","Chara_icon/202000230","2020/12/01 7:00:00",True,"1", -203000200,999,"《献身かしわもち》直葉",4,2,800,15000,100,1,0,0,0,"s203000200",,20,50,22,47,24,44,26,41,28,38,30,35,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"カシワ・フォース","カシワ・フォース","自パーティのSTRを
120%上昇させる。
覚醒で効果時間が延長。","自パーティのSTR上昇","美味しい和菓子はもちろんのこと
優しいお給仕さんがいることも
ここの人気の秘密。甘い甘~い
癒しの空間だ。","slog_203000200","slog_203000200","Chara_icon/203000200","Chara_icon/203000200","2020/12/01 7:00:00",True,"1", -206000170,999,"《きらきらあんみつ》シリカ",5,3,1600,20000,100,1,0,0,0,"s206000170",,65,210,68,204,71,201,73,198,75,196,77,194,10,20,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"アンミツ・ドラゴン","アンミツ・〇〇〇ドラゴン","ピナが現れ突進で敵を攻撃。
覚醒で攻撃力が上昇。","ピナが突進で攻撃","甘い香りに誘われて、迷い込んだ
のは不思議な和菓子カフェ。
可愛い猫ちゃん給仕さんが
お出迎えしてくれる。","slog_206000170","slog_206000170","Chara_icon/206000170","Chara_icon/206000170","2020/12/01 7:00:00",True,"1", -209000160,999,"《愛々わらびもち》ユウキ",4,2,800,15000,100,1,0,0,0,"s209000160",,20,60,24,58,28,56,32,54,36,52,40,50,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ワラビ・キラー","ワラビ・キラー","ボスに与えるダメージを
150%上昇させる。
覚醒で効果時間が延長。","ボスキラー","元気な猫ちゃんに迎えられ、
甘い和菓子に舌鼓。これぞ正に
この世の楽園かも!?
虜になること間違いなし。","slog_209000160","slog_209000160","Chara_icon/209000160","Chara_icon/209000160","2020/12/01 7:00:00",True,"1", -299000700,999,"《鈴の音護身具店》スズネ",4,2,800,15000,100,1,0,0,0,"s299000700",,10,150,11,140,12,130,13,125,14,120,15,115,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ワビサビ・イクイプメント","ワビサビ・〇〇〇イクイプメント","自パーティがダメージを受けなくなり、リーグポイントの獲得量が70%増加する。覚醒で効果時間が延長。","ダメージを受けなくなる+リーグポイントの獲得量上昇","和装を得意とする防具鍛冶師。
年始やお盆の季節には特に人気が
あり、多彩な和服で皆の日常を
取り戻すのに一役買っていた。","slog_299000700","slog_299000700","Chara_icon/299000700","Chara_icon/299000700","2020/12/25",True,"1", -299000710,999,"《私の物語》コハル",5,2,1600,20000,100,1,0,0,0,"s299000710",,45,145,55,137,65,129,70,125,75,121,80,117,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"パートナー・エール","パートナー・〇〇〇〇エール","ファーストアタックとラスト
アタックで得られるボーナス
ポイントが30%増加する。
覚醒で効果時間が延長。","ファースト&ラストアタックのボーナスポイント量上昇","《SAO》の世界においては、
誰もが主人公であった。自分の
道を切り開けるのは、いつだって
自分自身だ。","slog_299000710","slog_299000710","Chara_icon/299000710","Chara_icon/299000710","2020/12/08 7:00:00",True,"1", -299000720,999,"《漆黒を切り裂く》イーディス",5,2,1600,20000,100,1,0,0,0,"s299000720",,20,85,24,82,28,79,32,76,36,73,40,70,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ダーク・レイクベッド","ダーク・〇〇〇〇レイクベッド","自パーティの攻撃に闇属性を付与し、STRを100%上昇させ、周囲の敵の耐性を除去する。覚醒で効果時間が延長。","闇属性付与+自パーティのSTR上昇+周囲の敵の耐性を除去","闇夜に閃く刃が悪を討つ。情の
厚い彼女だからこそ、その信念の
刃は鋭く硬い。湖底の常闇を統べ
悪しきを遍く飲み込むだろう。","slog_299000720","slog_299000720","Chara_icon/299000720","Chara_icon/299000720","2020/12/08 7:00:00",True,"1", -201000120,999,"《世界を結ぶ》キリト",5,3,1600,20000,100,1,0,0,0,"s201000120",,65,210,68,204,71,201,73,198,75,196,77,194,10,20,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"アブゾーブ・レーザー","アブゾーブ・〇〇〇レーザー","前方に強力な闇属性のレーザーを
照射し、敵にダメージを与える。
覚醒で攻撃力が上昇。","前方に強力な闇属性のレーザーを照射","外の世界を知る彼だからこそ、
この世界の人々はいがみ合うべき
でないと知っている。皆が手と手
を取り合える世界を目指して。","slog_201000120","slog_201000120","Chara_icon/201000120","Chara_icon/201000120","2020/12/08 7:00:00",True,"1", -202000240,999,"《永遠を共に》アスナ",5,3,1600,20000,100,1,0,0,0,"s202000240",,65,210,68,204,71,201,73,198,75,196,77,194,10,20,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"グラウンド・オーソリティ","グラウンド・〇〇オーソリティ","周囲に強力な土属性の広範囲攻撃
を行い、敵にダメージを与える。
覚醒で攻撃力が上昇。","土属性の強力な広範囲攻撃","愛する者と共にあれるならば、
どんな困難が訪れようと恐れは
しない。覚悟を胸に彼女は凛と
前を向く。","slog_202000240","slog_202000240","Chara_icon/202000240","Chara_icon/202000240","2020/12/08 7:00:00",True,"1", -211000080,999,"《見通せぬ未来》アリス",5,3,1600,20000,100,1,0,0,0,"s211000080",,20,120,22,116,24,112,26,108,28,104,30,100,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"テナシティ・リアル","テナシティ・〇〇〇〇リアル","攻撃中のメンバーのスキルを再使
用可能にした後、自パーティの
INTを50%上昇させる。
覚醒で効果時間が延長。","クールタイムスキップ+INT上昇","己の成すべきことを成す。しかし
先の見えぬ新天地において、成す
べきは何なのか…。彼女の自問
自答は続く。","slog_211000080","slog_211000080","Chara_icon/211000080","Chara_icon/211000080","2020/12/08 7:00:00",True,"1", -299000730,999,"《決意を秘めて》イーディス",5,3,1600,20000,100,1,0,0,0,"s299000730",,20,120,22,116,24,112,26,108,28,104,30,100,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"フォアサイト・ワンサイド","フォアサイト・〇〇ワンサイド","攻撃中のメンバーのスキルを再使
用可能にした後、ボスに与える
ダメージを50%上昇させる。
覚醒で効果時間が延長。","クールタイムスキップ+ボスキラー","変わりゆく世界の中で、決意に満
ちた瞳が光を宿す。真の自由意志
を手に入れた彼女の目に、世界は
如何に映るのか――。","slog_299000730","slog_299000730","Chara_icon/299000730","Chara_icon/299000730","2020/12/08 7:00:00",True,"1", -299000740,999,"《立ち向かう意志》ロニエ",5,2,1600,20000,100,1,0,0,0,"s299000740",,80,75,84,73,88,71,92,69,96,67,100,65,10,20,"ボーナス増加量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"マイ・ジャスティス","マイ・〇〇〇〇〇ジャスティス","15秒間、ラストアタックでのボーナスポイントが増加。覚醒で増加量が上昇。","ラストアタックで得られるボーナスポイント量上昇","大戦を経て世界を知った少女は今
自分の信じる正義を胸に、自分の
足で大地を踏みしめる。相棒の小
竜と共に、新たな冒険が始まる。","slog_299000740","slog_299000740","Chara_icon/299000740","Chara_icon/299000740","2020/12/08 7:00:00",True,"1", -299000750,999,"《惑う心》ティーゼ",5,2,1600,20000,100,1,0,0,0,"s299000750",,80,75,84,73,88,71,92,69,96,67,100,65,10,20,"ボーナス増加量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"ヴァシレイト・ハート","ヴァシレイト・〇〇〇〇ハート","15秒間、ファーストアタックでのボーナスポイントが増加。覚醒で増加量が上昇。","ファーストアタックで得られるボーナスポイント量上昇","別れと出会いを経た彼女の胸は
答えの出せぬまま惑い揺れ動く。
世界の変革の最中において、彼女
は誰へと想いを馳せる。","slog_299000750","slog_299000750","Chara_icon/299000750","Chara_icon/299000750","2020/12/08 7:00:00",True,"1", -299000760,999,"《薄青の精鋭》エントキア",5,2,1600,20000,100,1,0,0,0,"s299000760",,35,180,38,168,41,156,44,144,47,132,50,120,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ペイルブルー・エリート","ペイルブルー・〇〇〇エリート","グリードの位置をミニマップに表示し、移動速度も上昇する。
覚醒で効果時間が延長。","グリードサーチ発動&移動速度上昇","《公理教会》の整合騎士の一人。
大戦時は山脈の警備にあたってお
り、その実力は未知数。","slog_299000760","slog_299000760","Chara_icon/299000760","Chara_icon/299000760","2020/12/08 7:00:00",True,"1", -299000770,999,"《萌ゆる深緑》ネルギウス",5,2,1600,20000,100,1,0,0,0,"s299000770",,60,120,65,116,70,112,75,108,80,104,85,100,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"スプラウト・プラント","スプラウト・〇〇〇プラント","自パーティがよろけ・ダウン無効状態になる。
覚醒で効果時間が延長。","よろけ・ダウン無効","《公理教会》の整合騎士の一人で
神器《萌嵐槍》の使い手。大戦時
は山脈の警備にあたっていた。","slog_299000770","slog_299000770","Chara_icon/299000770","Chara_icon/299000770","2020/12/08 7:00:00",True,"1", -206000150,999,"《碧海の踊り子》シリカ",5,2,1600,20000,100,1,0,0,0,"s206000150",,5,150,6,145,7,140,8,135,9,132,10,130,1,2,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_SIL_SUP_014","ジョンティ・ステップ","ジョンティ・〇〇〇ステップ","ファーストアタックで得られる
ボーナスポイントが500%増加
する。覚醒で効果時間が延長。","ファーストアタックで得られるボーナスポイント量上昇","飛び跳ね舞い踊る幼い少女の軽快
な足音。その小気味好いリズムは
不思議と観る者に力を与える。彼
女こそが勝利の女神なのかも。","slog_206000150","slog_206000150","Chara_icon/206000150","Chara_icon/206000150","2021/04/20 7:00:00",True,"1", -202000250,999,"《閃花の舞姫》アスナ",4,3,800,15000,100,1,0,0,0,"s202000250",,1,180,1,175,2,170,2,165,3,162,3,160,1,2,"ドロップ数","{0}個","skill_3_2line_moji86","un_2line_moji86","CH_ASU_SUP_014","チャーム・ブルーム","チャーム・〇〇〇〇〇ブルーム","ランダムで敵Mobのミミックか
グリードを4体召喚する。
覚醒でスキルポーションの
ドロップ数が増加。","ランダムで敵Mob【ミミック】かグリードを召喚","美しい長髪を揺らめかせ、刃の光
を閃かせ、笑顔を絶やさず彼女は
踊る。その心は、愛する者の勝利
を願って。","slog_202000250","slog_202000250","Chara_icon/202000250","Chara_icon/202000250","2021/04/20 7:00:00",True,"1", -204000170,999,"《氷弾の闘舞》シノン",4,2,800,15000,100,1,0,0,0,"s204000170",,15,60,18,58,21,56,24,54,27,52,30,50,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_SIN_SUP_014","トリップ・ウィナー","トリップ・〇〇〇〇ウィナー","ボスに与えるダメージを
200%上昇させる。
覚醒で効果時間が延長。","ボスキラー","涼やかな眼差しの奥底には、勝利
への渇望が静かに燃える。そんな
彼女の猛き舞に、闘志を刺激され
ない者はいない。","slog_204000170","slog_204000170","Chara_icon/204000170","Chara_icon/204000170","2021/04/20 7:00:00",True,"1", -216000080,999,"《探求の踊り手》フィリア",4,3,800,15000,100,1,0,0,0,"s216000080",,20,105,30,103,40,101,50,99,60,97,70,95,10,20,"メダル増加量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"トレジャー・サルテーション","トレジャー・〇〇サルテーション","一定範囲内のグリードをゴールデン・グリードに転生し、20秒間グリードの位置をミニマップに表示。覚醒で排出メダル量が増加。","グリードをゴールデン・グリードに転成+グリードサーチ発動","この砂漠のどこかに眠る秘宝を求
め、旅を続ける美しい踊り子。探
求の果てに、彼女は如何なるもの
と出会うのか。","slog_216000080","slog_216000080","Chara_icon/216000080","Chara_icon/216000080","2021/04/20 7:00:00",True,"1", -205000140,999,"《甘い真心》リズベット",5,3,1600,20000,100,1,0,20,0,"s205000140",,20,100,22,97,24,94,26,91,28,88,30,85,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_LIS_SUP_032","スウィート・シャワー","スウィート・〇〇〇〇シャワー","一定時間、魅了効果のあるチョコ
を周囲に投下。覚醒で効果時間が
延長。【パッシブ】アダプタブル
出現率が上昇。重複で効果増。","一定時間、周囲に魅了効果のあるチョコを投下","今宵は女子の祭典。その手に握る
道具を、槌から泡立て器に持ち替
えて。普段は照れ臭くても、今日
ばかりは恋心に素直になろう。","slog_205000140","slog_205000140","Chara_icon/205000140","Chara_icon/205000140","2021/02/02 7:00:00",True,"1", -204000180,999,"《甘い恋のコクハク》シノン",4,2,800,15000,100,1,0,0,0,"s204000180",,20,100,22,97,24,94,26,91,28,88,30,85,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_SIN_SUP_032","シュガー・プレゼント","シュガー・〇〇〇プレゼント","一定時間自パーティの攻撃に魅了
を付与し、ラストアタックボーナ
スを60%増加する。
覚醒で効果時間が延長。","自パーティの攻撃に魅了付与+ラストアタックで得られるボーナスポイント量上昇","普段はクールな彼女も、この日だ
けは甘い笑顔を覗かせる。気持ち
をギフトにいっぱい詰めて、大切
な告白をあなたに。","slog_204000180","slog_204000180","Chara_icon/204000180","Chara_icon/204000180","2021/02/02 7:00:00",True,"1", -206000180,999,"《思慕の猫姫》シリカ",4,2,800,15000,100,1,0,0,0,"s206000180",,20,100,22,97,24,94,26,91,28,88,30,85,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_SIL_SUP_032","ラブリー・プレゼント","ラブリー・〇〇〇プレゼント","一定時間自パーティの攻撃に魅了
を付与し、ファーストアタック
ボーナスを50%増加する。
覚醒で効果時間が延長。","自パーティの攻撃に魅了付与+ファーストアタックで得られるボーナスポイント量上昇","ドキドキで胸が張り裂けそう。
いたいけな少女の切なる想い、
大切なあの人に届けることが
出来るのか――?","slog_206000180","slog_206000180","Chara_icon/206000180","Chara_icon/206000180","2021/02/02 7:00:00",True,"1", -209000170,999,"《恋焦がれるココロ》ユウキ",4,2,800,15000,100,1,0,0,0,"s209000170",,20,100,22,97,24,94,26,91,28,88,30,85,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_YUU_SUP_032","ハニー・プレゼント","ハニー・〇〇〇〇プレゼント","一定時間自パーティの攻撃に魅了
を付与し、リーグポイントの獲得
量を30%増加する。
覚醒で効果時間が延長。","自パーティの攻撃に魅了付与+リーグポイントの獲得量上昇","元気印の少女も今日はちょっぴり
緊張気味?普段は言葉に出来ない
甘いココロを贈る――そんなワガ
ママも今日は許されるよね。","slog_209000170","slog_209000170","Chara_icon/209000170","Chara_icon/209000170","2021/02/02 7:00:00",True,"1", -203000210,999,"《安らかな夢見》リーファ",5,2,1600,20000,100,1,0,0,0,"s203000210","s203000210_passive",20,150,23,145,26,140,29,135,32,132,35,130,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_LEA_SUP_035","フェアリー・ドリーム","フェアリー・〇〇〇ドリーム","ファーストアタックボーナスが
100%増加。覚醒で時間延長。
【パッシブ】ファーストアタック
を重ねる毎にボーナスが増加。","ファーストアタックで得られるボーナスポイント量上昇","今日はのんびりお休みタイム。
スピードホリックな妖精も、翅を
休めてリラックス。あなたと一緒
なら素敵な夢が見られるかも。","slog_203000210","slog_203000210","Chara_icon/203000210","Chara_icon/203000210","2021/02/16 7:00:00",True,"1", -204000190,999,"《早天の射手》シノン",4,2,800,15000,100,1,0,0,0,"s204000190",,12,90,14,80,16,75,17,70,18,65,19,60,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_SIN_SUP_035","アーリー・アングリー","アーリー・〇〇〇アングリー","一定時間スキルポーションと
グリードの位置をミニマップに
表示し、移動速度も上昇する。
覚醒で効果時間が延長。","スキルポーション&グリードサーチ発動+移動速度上昇","目覚めの朝、無防備な彼女。そこ
に居合わせてしまったのは幸か
不幸か?!恥じらいの弾丸で蜂の
巣にされる前に逃亡あるのみ。","slog_204000190","slog_204000190","Chara_icon/204000190","Chara_icon/204000190","2021/02/16 7:00:00",True,"1", -215000080,999,"《夢路への誘い》ストレア",4,2,800,15000,100,1,0,0,0,"s215000080",,16,50,18,47,20,44,22,41,23,38,24,35,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ドリーミー・シェア","ドリーミー・〇〇〇〇〇シェア","自パーティのSTRを
150%上昇させる。
覚醒で効果時間が延長。","自パーティのSTR上昇","まどろみの中、柔らかな体温に
誘われるは幸福な夢の中。優しい
笑みを浮かべた彼女の添い寝に
温かな気持ちがあふれだす。","slog_215000080","slog_215000080","Chara_icon/215000080","Chara_icon/215000080","2021/02/16 7:00:00",True,"1", -217000040,999,"《憩いの歌姫》セブン",4,2,800,15000,100,1,0,0,0,"s217000040",,20,160,22,152,24,148,26,144,28,140,30,136,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"リラックス・ジーニアス","リラックス・〇〇ジーニアス","リーグポイント獲得量が
45%増加する。
覚醒で効果時間が延長。","獲得リーグポイント上昇","忙しい天才少女の束の間の休息。
寝る間も惜しんで研究に勤しむ
彼女も、今日は愛らしい寝間着で
リラックス。","slog_217000040","slog_217000040","Chara_icon/217000040","Chara_icon/217000040","2021/02/16 7:00:00",True,"1", -209000180,999,"《紫光の絶拳》ユウキ",5,2,1600,20000,100,1,0,0,0,"s209000180",,20,180,22,176,24,172,26,168,28,164,30,160,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"カンフー・パワー!","カンフー・〇〇〇〇パワー!","自パーティのSTRを150%
上昇させ、リーグポイントの
獲得量を50%増加させる。
覚醒で効果時間が延長。","自パーティのSTR上昇+獲得リーグポイント上昇","長い三つ編みを揺らして、元気
いっぱいカンフー娘のご登場!
見た目の愛らしさとは裏腹に、
その一撃はかなり重いとか…!?","slog_209000180","slog_209000180","Chara_icon/209000180","Chara_icon/209000180","2021/03/02 7:00:00",True,"1", -202000260,999,"《華麗な紅華》アスナ",4,2,800,15000,100,1,0,0,0,"s202000260",,10,60,12,58,14,56,16,54,18,52,20,50,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"エレガント・カンフー","エレガント・〇〇〇カンフー","自パーティのSTRが120%
上昇し、ラストアタック
ボーナスが50%増加する。
覚醒で効果時間が延長。","自パーティのSTR上昇+ラストアタックで得られるボーナスポイント量上昇","チャイナドレスでエレガントな
夜を。中華娘が見せる今宵の夢は
鮮やかな深紅が織りなす、艶やか
な時間――。","slog_202000260","slog_202000260","Chara_icon/202000260","Chara_icon/202000260","2021/03/02 7:00:00",True,"1", -203000220,999,"《疾風の美技》リーファ",4,2,800,15000,100,1,0,0,0,"s203000220",,10,60,12,58,14,56,16,54,18,52,20,50,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"フェアリー・カンフー","フェアリー・〇〇〇カンフー","自パーティのSTRが120%
上昇し、ファーストアタック
ボーナスが40%増加する。
覚醒で効果時間が延長。","自パーティのSTR上昇+ファーストアタックで得られるボーナスポイント量上昇","元気に誘う妖精娘も、セクシーな
スリットで無意識に皆を誘惑して
しまう。その手を取れば、夢のよ
うな一時が約束されている。","slog_203000220","slog_203000220","Chara_icon/203000220","Chara_icon/203000220","2021/03/02 7:00:00",True,"1", -218000060,999,"《赤影の明花》レイン",4,2,800,15000,100,1,0,0,0,"s218000060",,10,60,12,58,14,56,16,54,18,52,20,50,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"グレイス・カンフー","グレイス・〇〇〇〇カンフー","自パーティのSTRが120%上
昇。ファーストアタックとラスト
アタックのボーナスが35%増加
覚醒で効果時間が延長。","自パーティのSTR上昇+ファーストアタック&ラストアタックで得られるボーナスポイント量上昇","少し控え目な上品なチャイナ服…
と見せかけて、大胆なスリットに
皆の視線は釘付け。彼女には珍し
い青い衣装もよく映えている。","slog_218000060","slog_218000060","Chara_icon/218000060","Chara_icon/218000060","2021/03/02 7:00:00",True,"1", -204000200,999,"《パステルイノセンス》シノン",5,3,1600,20000,100,1,0,0,0,"s204000200",,65,210,68,204,71,201,73,198,75,196,77,194,10,20,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"パステル・アローシャワー","パステル・〇〇〇アローシャワー","一定時間、前方に様々な矢を連射
して敵にダメージを与える。
覚醒で攻撃力が上昇。","一定時間、前方に矢を連射","気品ある白を基調にしたパステル
なゴシック衣装に身を包んだ彼女
は、さながらお人形さんのよう。
可憐な姿で見る者を魅了する。","slog_204000200","slog_204000200","Chara_icon/204000200","Chara_icon/204000200","2021/01/26 7:00:00",True,"1", -209000190,999,"《ロリータプリンセス》ユウキ",4,2,800,15000,100,1,0,0,0,"s209000190",,30,160,34,152,36,148,38,144,40,140,42,136,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ロリータ・チアー","ロリータ・チアー","リーグポイント獲得量が
30%増加する。
覚醒で効果時間が延長。","獲得リーグポイント上昇","ピンクのリボンがアクセントの
愛らしいロリータドレス。小さな
プリンセスにピッタリの衣装で、
気分はお嬢様。","slog_209000190","slog_209000190","Chara_icon/209000190","Chara_icon/209000190","2021/01/26 7:00:00",True,"1", -299000780,999,"《心照らす歌声》ユナ",5,2,1600,20000,100,1,0,0,0,"s299000780",,20,100,24,90,28,80,31,70,33,65,35,60,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"エンチャント・シンガー","エンチャント・〇〇〇シンガー","敵からの注目を集め、さらに自パーティのSTR・VIT・INTを200%上昇させる。覚醒で効果時間が延長。","自パーティに敵の注目を集める+自パーティのSTR・VIT・INT上昇","この歌は、皆のために――。
全ての人を等しく照らす彼女の歌
声は、特別な一人の心にも深く響
く。魂の本質は変わらぬままで。","slog_299000780","slog_299000780","Chara_icon/299000780","Chara_icon/299000780","2021/03/16 7:00:00",True,"1", -299000790,999,"《心奮う戦い》エイジ",5,2,1600,20000,100,1,0,0,0,"s299000790",,30,180,32,160,34,155,35,150,36,145,37,140,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ロンリー・ソルジャー","ロンリー・〇〇〇ソルジャー","リーグポイント獲得量が40%
増加し、移動速度も上昇する。
覚醒で効果時間が延長。","獲得リーグポイント上昇+移動速度上昇","愛するたった一人を取り戻すため
青年は孤独に剣を握る。彼女であ
り彼女でない、その幻影を追い求
めて。この刃は誰のため。","slog_299000790","slog_299000790","Chara_icon/299000790","Chara_icon/299000790","2021/03/16 7:00:00",True,"1", -204000210,999,"《男らしく見えて…》シノン",5,1,1600,20000,100,1,0,0,0,"s204000210",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・チャンス","スリープウィズ・〇〇チャンス","戦闘不能状態のメンバーを2人復帰させ、10秒間ラストアタックボーナスを50%増加させる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+ラストアタックで得られるボーナスポイント量上昇","かの世界で数年を過ごした彼は、
なんだかとても男らしく見えて…
記憶を巡らせる間にも、いつもよ
りドギマギしてしまう乙女心。","slog_204000210","slog_204000210","Chara_icon/204000210","Chara_icon/204000210","2021/02/02 7:00:00",True,"1", -205000150,999,"《温かな距離》リズベット",5,1,1600,20000,100,1,0,0,0,"s205000150",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・アーミー","スリープウィズ・〇〇アーミー","戦闘不能状態のメンバーを2人復帰させ、30秒間自パーティがよろけ・ダウン無効状態になる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+よろけ・ダウン無効","あの時、生きていて良かった。
一緒に居られて良かった。それは
二人の共通の思い。すぐそばで、
心の温度を再確認しよう。","slog_205000150","slog_205000150","Chara_icon/205000150","Chara_icon/205000150","2021/02/02 7:00:00",True,"1", -299000800,999,"《目と目を合わせて》メディナ",5,1,1600,20000,100,1,0,0,0,"s299000800",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・キラー","スリープウィズ・〇〇〇キラー","戦闘不能状態のメンバーを2人復帰させ、20秒間ボスに与えるダメージを100%上昇させる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+ボスキラー","こんな距離で話をするのは、慣れ
ていないから落ち着かない。でも
一緒の思い出だからこそ――
最後には、目と目を合わせて。","slog_299000800","slog_299000800","Chara_icon/299000800","Chara_icon/299000800","2021/02/02 7:00:00",True,"1", -299000810,999,"《幼妹の添寝》セルカ",5,1,1600,20000,100,1,0,0,0,"s299000810",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・ヒール","スリープウィズ・〇〇〇ヒール","戦闘不能状態のメンバーを2人復帰させ、20秒間自パーティに自動回復を付与する。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+リジェネレイト付与","幼い少女は、お世話になった彼の
ため、緊張を抱えながらも添い寝
する。思い出を辿れば、段々緊張
もほぐれるかも。","slog_299000810","slog_299000810","Chara_icon/299000810","Chara_icon/299000810","2021/02/02 7:00:00",True,"1", -299000820,999,"《人との接し方》シェータ",5,1,1600,20000,100,1,0,0,0,"s299000820",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・スルー","スリープウィズ・〇〇〇スルー","戦闘不能状態のメンバーを2人復帰させ、15秒間周囲の敵のVITを大幅に低下させる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+周囲の敵のVITを低下","人との距離感は難しい。彼女に
とって、興味とは斬る対象。そう
でない接し方は分からぬまま…。
近付きすぎにはご用心。","slog_299000820","slog_299000820","Chara_icon/299000820","Chara_icon/299000820","2021/02/02 7:00:00",True,"1", -299000830,999,"《死の天使》サトライザー",5,3,1600,20000,100,1,0,0,0,"s299000830",,65,210,68,204,71,201,73,198,75,196,77,194,10,20,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"オムニポテント・ニヒリティ","オムニポテント・〇ニヒリティ","闇属性の広範囲攻撃を行い、さらに15秒間周囲の敵のSTR・VIT・INTを大幅に低下させる。
覚醒で攻撃力が上昇。","闇属性の広範囲攻撃+周囲の敵のSTR・VIT・INTを低下","虚無を統べる真の邪悪。全能感に
支配され、暗黒の翼を携えたその
姿は、正しく《死の天使》の名に
相応しい。","slog_299000830","slog_299000830","Chara_icon/299000830","Chara_icon/299000830","2021/02/02 7:00:00",True,"1", -201000130,999,"《夜空に煌く星》キリト",5,3,1600,20000,100,1,0,0,0,"s201000130",,65,210,68,204,71,201,73,198,75,196,77,194,10,20,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スターリー・ブルーナイト","スターリー・〇〇ブルーナイト","周囲に氷咲効果のある広範囲攻撃
を行い、その後に強力な闇属性の
レーザーを照射する。
覚醒で攻撃力が上昇。","氷咲効果のある広範囲攻撃+闇属性のレーザーを照射","温かな闇の中、人々を照らす星々
の光。この世界にとって彼という
英雄の存在は、正しく星のよう。
彼にとっての親友と同じように。","slog_201000130","slog_201000130","Chara_icon/201000130","Chara_icon/201000130","2021/02/02 7:00:00",True,"1", -202000270,999,"《女神の笑顔》アスナ",5,2,1600,20000,100,1,0,0,0,"s202000270",,10,60,12,58,14,56,16,54,18,52,20,50,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"キューティ・ゴッデス","キューティ・〇〇〇ゴッデス","自パーティのINTが100%
上昇し、ラストアタック
ボーナスが50%増加する。
覚醒で効果時間が延長。","INT上昇+ラストアタックで得られるボーナスポイント量上昇","攻略の鬼と揶揄される彼女でも、
オフの日に見せる輝く笑顔は女神
のように愛らしい。大切な日は
大切な人と共に。","slog_202000270","slog_202000270","Chara_icon/202000270","Chara_icon/202000270","2021/02/02 7:00:00",True,"1", -206000190,999,"《再生の花》シリカ",5,2,1600,20000,100,1,0,0,0,"s206000190",,10,60,12,58,14,56,16,54,18,52,20,50,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"フラワリー・メモリー","フラワリー・〇〇〇メモリー","自パーティのINTが100%
上昇し、ファーストアタック
ボーナスが40%増加する。
覚醒で効果時間が延長。","INT上昇+ファーストアタックで得られるボーナスポイント量上昇","大事な相棒を蘇らせてくれた、思
い出の花。大切な日にこそ、この
花のことを思い出す。助けてくれ
た優しいあの人のことと一緒に。","slog_206000190","slog_206000190","Chara_icon/206000190","Chara_icon/206000190","2021/02/02 7:00:00",True,"1", -220000060,999,"《憩いの場》サチ",5,2,1600,20000,100,1,0,0,0,"s220000060",,10,60,12,58,14,56,16,54,18,52,20,50,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"グロウリング・レスト","グロウリング・〇〇〇〇レスト","自パーティのINTを100%
上昇させ、リーグポイントの
獲得量を30%増加させる。
覚醒で効果時間が延長。","INT上昇+獲得リーグポイント上昇","あの日の乾杯が、大事な一歩に繋
がった。共に歩む切っ掛けをくれ
た出会いを祝って、大切な日に再
び杯を交わそう。","slog_220000060","slog_220000060","Chara_icon/220000060","Chara_icon/220000060","2021/02/02 7:00:00",True,"1", -206000200,999,"《抱きしめ幼愛》シリカ",5,1,1600,20000,100,1,0,0,0,"s206000200",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・ハグ","スリープウィズ・〇〇〇〇〇ハグ","戦闘不能状態のメンバーを2人復帰させ、10秒間ファーストアタックボーナスを40%増加。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+ファーストアタックで得られるボーナスポイント量上昇","大好きな相棒のぬいぐるみを抱き
しめて、大好きなあの人の隣へ。
火照る頬と高鳴る胸は、ぬいぐる
みに埋めて誤魔化して。","slog_206000200","slog_206000200","Chara_icon/206000200","Chara_icon/206000200","2021/03/30 7:00:00",True,"1", -211000090,999,"《折り重なる感情》アリス",5,1,1600,20000,100,1,0,0,0,"s211000090",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・フィール","スリープウィズ・〇〇〇フィール","戦闘不能状態のメンバーを2人復帰させ、30秒間グリードからの注目を集める。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+グリードからの注目","彼と出会うまで、こんなに色々な
感情を抱くことは無かった。喜び
も、怒りも、悲しみも…そして、
温かく大切な想いも。","slog_211000090","slog_211000090","Chara_icon/211000090","Chara_icon/211000090","2021/03/30 7:00:00",True,"1", -299000840,999,"《憧れの先輩と》ロニエ",5,1,1600,20000,100,1,0,0,0,"s299000840",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・ウィッシュ","スリープウィズ・〇〇ウィッシュ","戦闘不能状態のメンバーを2人復帰させ、15秒間レコードメダルの位置をミニマップに表示する。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+レコードメダルサーチ","傍付き練士として仕えた憧れの
先輩。強くて優しい、大好きな彼
と添い寝して、緊張しないはずが
ない! 少女の心臓は爆発寸前。","slog_299000840","slog_299000840","Chara_icon/299000840","Chara_icon/299000840","2021/03/30 7:00:00",True,"1", -299000850,999,"《悪戯な捕食者》フィゼル&リネル",5,1,1600,20000,100,1,0,0,0,"s299000850",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・ツイン","スリープウィズ・〇〇〇〇ツイン","戦闘不能状態のメンバーを2人復帰させ、周囲の敵に麻痺を付与する。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+周囲の敵に麻痺付与","小さな騎士見習いたちは、自分た
ちを出し抜いた獲物に興味津々。
虎視眈々と機会を伺う幼い捕食者
には、彼も翻弄されるばかり。","slog_299000850","slog_299000850","Chara_icon/299000850","Chara_icon/299000850","2021/03/30 7:00:00",True,"1", -299000860,999,"《坊やの傍ら》ファナティオ",5,1,1600,20000,100,1,0,0,0,"s299000860",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・アダルト","スリープウィズ・〇〇〇アダルト","戦闘不能状態のメンバーを2人復帰させ、20秒間Mobに与えるダメージを100%上昇させる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+Mobキラー","自身を打ち負かし、そして救って
くれた坊や。そんな興味の尽きな
い彼に協力するんだもの、折角な
ら可愛がってあげなくちゃね。","slog_299000860","slog_299000860","Chara_icon/299000860","Chara_icon/299000860","2021/03/30 7:00:00",True,"1", -202000280,999,"《攻略の最前線へ》アスナ",5,2,1600,20000,100,1,0,0,0,"s202000280",,40,120,48,116,56,112,64,108,72,104,80,100,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"フロントランナー","フロントランナー","ボスに与えるダメージを150%上昇させる。
覚醒で効果時間が延長。","ボスキラー","SAO攻略の最前線を切り拓く、
鋭くも厳しい彼女。《攻略の鬼》
とまで呼ばれたその原動力は、
現実への帰還にかける想い。","slog_202000280","slog_202000280","Chara_icon/202000280","Chara_icon/202000280","2021/03/30 7:00:00",True,"1", -204000220,999,"《切り拓く弾道》シノン",5,2,1600,20000,100,1,0,0,0,"s204000220",,30,120,36,116,42,112,48,108,54,104,60,100,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"バレット・ゲットオーバー","バレット・〇〇〇ゲットオーバー","ボスに与えるダメージを200%上昇させる。
覚醒で効果時間が延長。","ボスキラー","ショッキングな過去を乗り越える
ため、強くなるために、少女は銃
を手にした。その弾丸が切り拓く
のは、明るい未来への道筋。","slog_204000220","slog_204000220","Chara_icon/204000220","Chara_icon/204000220","2021/03/30 7:00:00",True,"1", -201000140,999,"《浮遊城の救世主》キリト",5,2,1600,20000,100,1,0,0,0,"s201000140",,15,75,16,73,17,71,18,69,19,67,20,65,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ヒーローオブブラッキー","ヒーローオブ〇〇〇〇ブラッキー","ラストアタックで得られるボーナスポイントが75%増加する。覚醒で効果時間が延長。","ラストアタックで得られるボーナスポイント量上昇","浮遊城アインクラッド。その攻略
を語る上で彼の存在を欠くことは
出来ない。人を思う故に孤独を
選んだ、孤高のブラッキー。","slog_201000140","slog_201000140","Chara_icon/201000140","Chara_icon/201000140","2021/03/30 7:00:00",True,"1", -203000230,999,"《華麗なる妖精》リーファ",5,2,1600,20000,100,1,0,0,0,"s203000230",,15,75,16,73,17,71,18,69,19,67,20,65,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"フェアリー・ダンス","フェアリー・〇〇〇ダンス","ファーストアタックで得られるボーナスポイントが60%増加。覚醒で効果時間が延長。","ファーストアタックで得られるボーナスポイント量上昇","同じ心を持った者同士なら、どん
な相手だって仲良くなれるはず。
しがらみや偏見を嫌う彼女の優し
い精神は、まさに妖精のよう。","slog_203000230","slog_203000230","Chara_icon/203000230","Chara_icon/203000230","2021/03/30 7:00:00",True,"1", -205000160,999,"《鍛冶屋の恋心》リズベット",5,2,1600,20000,100,1,0,0,0,"s205000160",,15,75,16,73,17,71,18,69,19,67,20,65,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"スノウリー・メモリー","スノウリー・〇〇〇メモリー","リーグポイントの獲得量を45%増加させる。覚醒で効果時間が延長。","リーグポイントの獲得量上昇","雪山の山頂で、共に見た朝日の
美しさは、生涯忘れられない大切
な思い出。彼の手の温もりが、心
の温度になって脈を打つ。","slog_205000160","slog_205000160","Chara_icon/205000160","Chara_icon/205000160","2021/03/30 7:00:00",True,"1", -202000290,999,"《愛情の眼差し》アスナ",5,1,1600,20000,100,1,0,0,0,"s202000290",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・ラバー","スリープウィズ・〇〇〇〇ラバー","戦闘不能状態のメンバーを2人復帰させ、10秒間獲得リーグポイントを30%増加させる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+獲得リーグポイント上昇","遠き異界の地において、新たな
戦いに身を投じる愛しい人。彼の
心を癒すため、愛の温もりを届け
るために、寄り添い続ける。","slog_202000290","slog_202000290","Chara_icon/202000290","Chara_icon/202000290","2021/04/27 7:00:00",True,"1", -203000240,999,"《親愛なる兄と》リーファ",5,1,1600,20000,100,1,0,0,0,"s203000240",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・シスター","スリープウィズ・〇〇〇シスター","戦闘不能状態のメンバーを2人復帰させ、10秒間自パーティの移動速度を上昇させる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+移動速度上昇","大切な兄との共寝はどこか懐かし
いような、けれどやっぱりドキド
キするような。家族の温もりは
何物にも代えられない。","slog_203000240","slog_203000240","Chara_icon/203000240","Chara_icon/203000240","2021/04/27 7:00:00",True,"1", -299000870,999,"《尊敬する先輩》ティーゼ",5,1,1600,20000,100,1,0,0,0,"s299000870",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・リスペクト","スリープウィズ・〇〇リスペクト","戦闘不能状態のメンバーを2人復帰させ、15秒間グリードの位置をミニマップに表示する。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+グリードサーチ","尊敬する相手だけれど、つい口を
ついて出るのは憧れの先輩――彼
の相棒のことばかり。互いの大事
な人だからこそ、話も弾む。","slog_299000870","slog_299000870","Chara_icon/299000870","Chara_icon/299000870","2021/04/27 7:00:00",True,"1", -299000880,999,"《流麗なる華》ソルティリーナ",5,1,1600,20000,100,1,0,0,0,"s299000880",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・コーチ","スリープウィズ・〇〇〇〇コーチ","戦闘不能状態のメンバーを2人復帰させ、20秒間自パーティのSTRを75%上昇させる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+自パーティのSTR上昇","後輩の危機とあらばひと肌脱ぐの
が良き先輩というもの。大切なこ
とに気付かせてくれたお礼…
それ以上の気持ちも込めて。","slog_299000880","slog_299000880","Chara_icon/299000880","Chara_icon/299000880","2021/04/27 7:00:00",True,"1", -299000890,999,"《内に潜む想い》カーディナル",5,1,1600,20000,100,1,0,0,0,"s299000890",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・ティーチ","スリープウィズ・〇〇〇ティーチ","戦闘不能状態のメンバーを2人復帰させ、20秒間自パーティのINTを100%上昇させる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+自パーティのINT上昇","感情が表に出にくい彼女だが、そ
れでもその胸中には様々な想いが
巡っている。温かな触れ合いの中
で、彼女の心も色濃く見える。","slog_299000890","slog_299000890","Chara_icon/299000890","Chara_icon/299000890","2021/04/27 7:00:00",True,"1", -212000060,999,"《寄り添う親友》ユージオ",5,1,1600,20000,100,1,0,0,0,"s212000060",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・ベストフレンド","スリープウィズ・〇ベストフレンド","戦闘不能状態のメンバーを2人復帰させ、周囲の敵に氷咲を付与する。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+周囲の敵に氷咲付与","旅を共にした相棒同士、共寝くら
いは慣れたもの。とはいえ、どこ
か気恥ずかしい。親愛なる友との
語らいは夜更けまで続く。","slog_212000060","slog_212000060","Chara_icon/212000060","Chara_icon/212000060","2021/04/27 7:00:00",True,"1", -201000150,999,"《剣に宿る絆》キリト",5,2,1600,20000,100,1,0,0,0,"s201000150",,15,65,17,61,19,57,21,54,22,52,23,50,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ブラッディ・ソード","ブラッディ・〇〇〇ソード","自パーティのSTRを140%上昇させる。覚醒で効果時間が延長。【キリトのみ効果が2倍】","【効果上昇・キリト】STR上昇","世界の命運を握る一戦で、友はそ
の命を賭して戦い抜いた。彼の魂
を映し、赤き一閃はどこまでも
凛々しくひらめく。","slog_201000150","slog_201000150","Chara_icon/201000150","Chara_icon/201000150","2021/04/27 7:00:00",True,"1", -204000230,999,"《ショート・ブレイク》シノン",5,2,1600,20000,100,1,0,0,0,"s204000230",,30,100,31,96,32,92,33,88,34,84,35,80,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ストロング・シューター","ストロング・〇〇シューター","自パーティのSTRを100%上昇させ、さらにボスに与えるダメージを60%上昇させる。覚醒で効果時間が延長。","自パーティのSTR上昇+ボスキラー","己に厳しい弓使いの休息の一幕。
強さを追い求める日々こそ、彼女
の日常。今日も高みを目指して、
信じる道を歩んでゆく。","slog_204000230","slog_204000230","Chara_icon/204000230","Chara_icon/204000230","2021/04/27 7:00:00",True,"1", -209000200,999,"《剣姫の願い》ユウキ",5,2,1600,20000,100,1,0,0,0,"s209000200",,20,85,24,82,28,79,32,76,36,73,40,70,4,8,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"スプリング・ホープ","スプリング・〇〇〇〇ホープ","自パーティのSTR・VIT・INTを110%上昇させる。覚醒で効果時間が延長。","自パーティのSTR・VIT・INT上昇","凄まじい強さを誇る絶剣も、一人
の少女に違いない。これからも皆
と共にあれるよう、皆が幸せでい
られるよう。願いを込めて。","slog_209000200","slog_209000200","Chara_icon/209000200","Chara_icon/209000200","2021/04/27 7:00:00",True,"1", -299000900,999,"《柳絮の守護》コハル",5,2,1600,20000,100,1,0,0,0,"s299000900",,15,60,18,58,21,56,24,54,27,52,30,50,3,6,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"リアル・サバイブ","リアル・サバイブ","自パーティのSTRを50%上昇させ、獲得リーグポイントも20%上昇させる。覚醒で効果時間が延長。","自パーティのSTR上昇+獲得リーグポイント上昇","ゲームの中の死が、現実の死とな
る世界。過酷な環境でそれでも
頑張れるのは、きっと君がいる
から。必ず共に帰ると誓って。","slog_299000900","slog_299000900","Chara_icon/299000900","Chara_icon/299000900","2021/04/27 7:00:00",True,"1", -204000240,999,"《心の傷を越えて》シノン",5,2,1600,20000,100,1,0,0,0,"s204000240","s204000240_passive",15,120,18,116,21,112,24,108,27,104,30,100,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_SIN_SUP_034","フェアレス・トリガー","フェアレス・〇〇〇トリガー","ボスへのダメージを300%上昇させる。覚醒で効果時間が延長。【パッシブ】ラストアタックを重ねる毎にボーナスが増加。","ボスキラー","学校というものに忌避感すら覚え
ていた彼女だったが、ある出会い
が彼女を変えた。恐れを捨て、前
を向いた彼女にもう敵はいない。","slog_204000240","slog_204000240","Chara_icon/204000240","Chara_icon/204000240","2021/04/06 7:00:00",True,"1", -203000250,999,"《春満開》リーファ",4,3,800,15000,100,1,0,0,0,"s203000250",,10,90,12,85,14,81,15,79,16,77,17,75,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_LEA_SUP_034","フルブルーム・スプリング","フルブルーム・〇〇〇スプリング","攻撃中のメンバーのスキルを再使用可能にし、自パーティの移動速度を上昇させる。覚醒で効果時間が延長。","クールタイムスキップ+移動速度上昇","春一番に吹かれて、スポーツ少女
は通学路を駆ける。ハツラツとし
たその姿は、見る者皆に元気を
分けてくれるようだ。","slog_203000250","slog_203000250","Chara_icon/203000250","Chara_icon/203000250","2021/04/06 7:00:00",True,"1", -206000210,999,"《敬慕の瞳》シリカ",4,2,800,15000,100,1,0,0,0,"s206000210",,20,150,24,144,28,138,32,134,35,130,38,126,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_SIL_SUP_034","メイデン・スローブ","メイデン・〇〇〇スローブ","自パーティのINTを150%上昇させ、ラストアタックボーナスを50%増加させる。覚醒で効果時間が延長。","自パーティのINT上昇+ラストアタックで得られるボーナスポイント量上昇","憧れのあの人と同じ学校。それだ
けでも、少女の胸を高鳴らせるに
は十分だ。彼の姿をみとめれば、
その瞳は大きく輝く。","slog_206000210","slog_206000210","Chara_icon/206000210","Chara_icon/206000210","2021/04/06 7:00:00",True,"1", -299000910,999,"《番長》キバオウ",4,3,800,15000,100,1,0,0,0,"s299000910",,25,180,30,170,33,160,36,155,38,150,40,145,5,10,"召喚増強レベル","{0}レベル","skill_3_2line_moji86","un_2line_moji86",,"ジュブナイル・ギャング","ジュブナイル・〇〇〇ギャング","亜人系Mobを5体召喚し、さらに自パーティと召喚Mobがよろけ・ダウン無効状態になる。覚醒で召喚Mobのレベルが増強。","お助けMob【亜人系】を召喚+よろけ・ダウン無効","学園の支配者の座を虎視眈々と
狙うならず者。一大徒党を作り
あげ、学内を我が物顔で闊歩する
迷惑な人だが、根は良い奴だ。","slog_299000910","slog_299000910","Chara_icon/299000910","Chara_icon/299000910","2021/04/06 7:00:00",True,"1", -202000300,999,"《完全無欠メイド》アスナ",5,3,1600,20000,100,1,0,0,0,"s202000300","s202000300_passive",65,210,68,204,71,201,73,198,75,196,77,194,10,20,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_ASU_SUP_013","パーフェクト・スイーパー","パーフェクト・〇〇スイーパー","手にした箒で周りをお掃除する広範囲攻撃。覚醒で攻撃力が上昇。【パッシブ】ラストアタック回数を重ねる毎にボーナスが増加。","広範囲攻撃","どんな家事でもお任せあれ。ご主
人様の危機には戦闘までも華麗に
こなし、類稀なる美貌まで兼ね備
えたスーパーメイド。","slog_202000300","slog_202000300","Chara_icon/202000300","Chara_icon/202000300","2021/05/11 7:00:00",True,"1", -201000160,999,"《有頂天外メイド》キリト",4,3,800,15000,100,1,0,0,0,"s201000160",,40,165,50,151,55,144,60,137,65,130,70,123,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_KIR_SUP_013","トランス・シークレット","トランス・〇〇〇シークレット","一定時間周囲に留まった後、敵に向かって飛んで行く闇の弾を呼び出す。覚醒で攻撃力が上昇。","闇属性の弾による攻撃","愛らしい容姿でご奉仕する姿は、
美少女以外の何物でもない。しか
し正体は…?! 彼女の秘密は、
屋敷のトップシークレットだ。","slog_201000160","slog_201000160","Chara_icon/201000160","Chara_icon/201000160","2021/05/11 7:00:00",True,"1", -209000210,999,"《天真爛漫メイド》ユウキ",4,2,800,15000,100,1,0,0,0,"s209000210",,50,180,55,172,60,168,65,164,70,160,75,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_YUU_SUP_013","イノセント・チアフル","イノセント・〇〇〇チアフル","Mobに与えるダメージを100%上昇させ、リーグポイントの獲得量も15%増加させる。覚醒で効果時間が延長。","Mobキラー+獲得リーグポイント上昇","いつでも明るい笑顔で、屋敷中に
元気を与えてくれる。お仕事も
テキパキこなし、クルクル駆けま
わる姿は皆に愛されている。","slog_209000210","slog_209000210","Chara_icon/209000210","Chara_icon/209000210","2021/05/11 7:00:00",True,"1", -217000050,999,"《才色兼備メイド》セブン",4,2,800,15000,100,1,0,0,0,"s217000050",,20,160,22,152,24,148,26,144,28,140,30,136,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"アイドル・ハウスワーク","アイドル・〇〇〇ハウスワーク","自パーティに敵の注目を集め、リーグポイント獲得量を45%上昇させる。覚醒で効果時間が延長。","自パーティに敵の注目を集める+獲得リーグポイント上昇","知性と可憐さを併せ持つ彼女の歌
は、いつでも皆を奮い立たせる。
家事はまだ、ちょっぴり苦手だけ
れど、それもまたご愛嬌だ。","slog_217000050","slog_217000050","Chara_icon/217000050","Chara_icon/217000050","2021/05/11 7:00:00",True,"1", -298000110,999,"《親愛の一時》アスナ&リズベット",5,2,1600,20000,100,1,0,0,0,"s298000110",,15,120,18,116,21,112,24,108,27,104,30,100,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_SUP_SUP_010","リラックス・ベストフレンド","リラックス・〇〇ベストフレンド","ボスに与えるダメージが300%上昇し、自パーティがよろけ・ダウン無効状態になる。覚醒で効果時間が延長。","ボスキラー+よろけ・ダウン無効","いつも頑張る親友の、珍しい隙だ
らけの姿。疲れを癒せるよう、温
かな膝枕を添えて。今日はゆっく
り、休んでね。","slog_298000110","slog_298000110","Chara_icon/298000110","Chara_icon/298000110","2021/03/23 7:00:00",True,"1", -298000120,999,"《愛恋の双精》リーファ&シリカ",4,2,800,15000,100,1,0,0,0,"s298000120",,10,150,12,140,14,135,15,130,16,125,17,120,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_SUP_SUP_012","フェアリー・シスターズ","フェアリー・〇〇シスターズ","ファーストアタックで得られるボーナスポイントが100%増加し、移動速度も上昇する。覚醒で効果時間が延長。","ファーストアタックのボーナスポイント量&移動速度上昇","妹(?)コンビは、歳が近いのも
相まって、気の合う友達同士。
遊びに恋に勉強に。今日も二人の
妖精は、会話に花を咲かせる。","slog_298000120","slog_298000120","Chara_icon/298000120","Chara_icon/298000120","2021/03/23 7:00:00",True,"1", -203000260,999,"《妖精の風術》リーファ",5,3,1600,20000,100,1,0,0,0,"s203000260",,65,210,68,204,71,201,73,198,75,196,77,194,10,20,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86","CH_LEA_SUP_017","マジカル・タイフーン","マジカル・〇〇〇タイフーン","敵を追尾する竜巻を
複数呼び出して、
周囲の敵を攻撃する。
覚醒で攻撃力が上昇。","風属性の強力な広範囲攻撃","風を司る妖精なら、このくらいの
魔法は朝飯前。あなたのことも
風で捕まえて、ふわり空中散歩に
ご招待しちゃうかも?","slog_203000260","slog_203000260","Chara_icon/203000260","Chara_icon/203000260","2021/06/01 7:00:00",True,"1", -290000130,999,"《プリティーマジック》ユイ",4,3,800,15000,100,1,0,0,0,"s290000130",,25,180,30,170,33,160,36,155,38,150,40,145,5,10,"召喚増強レベル","{0}レベル","skill_3_2line_moji86","un_2line_moji86","CH_YUI_SUP_017","マジカル・サモン","マジカル・〇〇〇サモン","お助けMobスフィアドラコを6体召喚し、戦闘支援を受ける。覚醒で召喚Mobのレベルが増強。","お助けMob【スフィアドラコ】を召喚","幼い少女と侮るなかれ。あらゆる
魔法を熟知し、操ることのできる
天才少女とは彼女のこと。誰より
も頼りになる存在だ。","slog_290000130","slog_290000130","Chara_icon/290000130","Chara_icon/290000130","2021/06/01 7:00:00",True,"1", -202000310,999,"《天幸の恩愛》アスナ",5,3,1600,20000,100,1,0,0,0,"s202000310",,30,300,34,298,38,296,40,294,42,292,44,290,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_ASU_SUP_022","フューチャー・ウィズユー","フューチャー・〇〇〇ウィズユー","レコードメダルを再配置。
自パーティの移動速度が上昇し、
リーグポイント獲得量も50%
増加。覚醒で効果時間が延長。","メダル再配置+移動速度上昇+リーグポイント獲得量上昇","永遠を誓った伴侶と、温かな笑み
を交わして。ふたりのこれからの
歩みが、豊かなものになりますよ
うに。一緒に幸せになろうね。","slog_202000310","slog_202000310","Chara_icon/202000310","Chara_icon/202000310","2021/06/01 7:00:00",True,"1", -204000250,999,"《甘やかな誓愛》シノン",4,3,800,15000,100,1,0,0,0,"s204000250",,10,90,12,85,14,81,15,79,16,77,17,75,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_SIN_SUP_022","ボウ・ブライド","ボウ・〇〇〇〇〇ブライド","攻撃中のメンバーのスキルを
再使用可能にした後、ボスへの
ダメージを75%上昇させる。
覚醒で効果時間が延長。","クールタイムスキップ+ボスキラー","愛する人の前では、普段よりも
ちょっぴり柔らかな表情に。
ずっと隣にいてくれるのよね?
甘い誓いをあなたと。","slog_204000250","slog_204000250","Chara_icon/204000250","Chara_icon/204000250","2021/06/01 7:00:00",True,"1", -209000220,999,"《無邪気な幼愛》ユウキ",4,3,800,15000,100,1,0,0,0,"s209000220",,10,90,12,85,14,81,15,79,16,77,17,75,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_YUU_SUP_022","ピュアリー・ブライド","ピュアリー・〇〇ブライド","攻撃中のメンバーのスキルを
再使用可能にした後、自パーティ
のSTRを50%上昇させる。
覚醒で効果時間が延長。","クールタイムスキップ+自パーティのSTR上昇","慣れない動きにくい衣装だけれど
それは同時に憧れの衣装でもあっ
て。幸福に無邪気な笑みを咲かせ
る。えへへ、似合うかな?","slog_209000220","slog_209000220","Chara_icon/209000220","Chara_icon/209000220","2021/06/01 7:00:00",True,"1", -215000090,999,"《優艶な祝愛》ストレア",4,3,800,15000,100,1,0,0,0,"s215000090",,10,90,12,85,14,81,15,79,16,77,17,75,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"グラマー・ブライド","グラマー・〇〇〇ブライド","攻撃中のメンバーのスキルを再
使用可能にした後、自パーティが
よろけ・ダウン無効状態になる。
覚醒で効果時間が延長。","クールタイムスキップ+よろけ・ダウン無効","無垢なドレスに身を包んでも、
マイペースな彼女は茶目っ気を
忘れない。それでも隠しきれない
艶めきが、2人の愛を物語る。","slog_215000090","slog_215000090","Chara_icon/215000090","Chara_icon/215000090","2021/06/01 7:00:00",True,"1", -205000170,999,"《夜天の桃虹》リズベット",5,3,1600,20000,100,1,0,0,0,"s205000170",,1,365,1,320,2,300,2,280,3,260,3,240,1,2,"ドロップ数","{0}個","skill_3_2line_moji86","un_2line_moji86",,"タナバタ・ギフト","タナバタ・〇〇〇ギフト","グリードを7体、
7秒間だけ召喚する。
覚醒でスキルポーションのドロップ数が増加。","グリードを召喚","7月の空にかかる天の川。その
傍らには世にも美しい天女が。
働き者の彼女だからこそ、七夕の
願いはきっと叶えられるだろう。","slog_205000170","slog_205000170","Chara_icon/205000170","Chara_icon/205000170","2021/07/06 7:00:00",True,"1", -202000320,999,"《光星の天女》アスナ",4,3,800,15000,100,1,0,0,0,"s202000320",,20,105,30,103,40,101,50,99,60,97,70,95,10,20,"メダル増加量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"オリヒメ・ミラクル","オリヒメ・〇〇〇ミラクル","一定範囲内のグリードをゴールデン・グリードに転生し、10秒間攻撃に水属性を付与する。覚醒で排出メダル量が増加。","グリードをゴールデン・グリードに転成+水属性付与","運命のあの人と会える今日だけは
自分の心に正直に。天も舞う程の
喜びが滲む足取りで、約束のあの
地へと向かおう。","slog_202000320","slog_202000320","Chara_icon/202000320","Chara_icon/202000320","2021/07/06 7:00:00",True,"1", -206000220,999,"《星彩の願い》シリカ",4,3,800,15000,100,1,0,0,0,"s206000220",,10,150,12,140,14,130,16,120,18,110,20,100,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"アマノガワ・ループホール","アマノガワ・〇〇ループホール","ボス部屋へ移動するワープポイン
トを設置し、ラストアタックボー
ナスを125%増加させる。
覚醒で効果時間が延長。","ワープポイント設置+ラストアタックで得られるボーナスポイント量上昇","頑張り屋さんの少女は、天の川へ
願いをかける。運命のあの人と
一緒にいられますように。ずっと
隣にいられますように。","slog_206000220","slog_206000220","Chara_icon/206000220","Chara_icon/206000220","2021/07/06 7:00:00",True,"1", -209000230,999,"《宵闇の流星》ユウキ",4,3,800,15000,100,1,0,0,0,"s209000230",,40,165,50,151,55,144,60,137,65,130,70,123,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"カササギ・ダークウィング","カササギ・〇〇〇ダークウィング","左右に闇属性のレーザーを
照射し、敵にダメージを与える。
覚醒で攻撃力が上昇。","左右に闇属性のレーザーを照射","闇夜に流れる願い星。その刹那の
輝きは、儚く美しく、何よりも
尊い。誰もが見上げる星空で、皆
に愛されることだろう。","slog_209000230","slog_209000230","Chara_icon/209000230","Chara_icon/209000230","2021/07/06 7:00:00",True,"1", -217000060,999,"《心舞の熱唱》セブン",5,2,1600,20000,100,1,0,0,0,"s217000060",,30,180,32,160,34,155,35,150,36,145,37,140,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ジャガーノート・サウンド","ジャガーノート・サウンド","自パーティがよろけ・ダウン無効
状態になり、リーグポイント獲得
量が40%増加する。
覚醒で効果時間が延長。","よろけ・ダウン無効+リーグポイント獲得量上昇","天才少女の応援は、何物にも代え
がたい価値を持つ。心の繋がりが
生み出すパワーを信じて、皆の力
を最大限に引き出してくれる。","slog_217000060","slog_217000060","Chara_icon/217000060","Chara_icon/217000060","2021/09/14 7:00:00",True,"1", -204000260,999,"《快勝の導き》シノン",4,2,800,15000,100,1,0,0,0,"s204000260",,20,50,22,47,24,44,26,41,28,38,30,35,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_SIN_SUP_016","ヴィクトリー・ロード","ヴィクトリー・〇〇〇ロード","自パーティがよろけ・ダウン無効
状態になり、ボスへのダメージが
150%上昇する。
覚醒で効果時間が延長。","よろけ・ダウン無効+ボスキラー","勝利への情熱が人一倍強い彼女の
声援は、味方の士気を高めてくれ
る。勝利の女神の応援を胸に、
必ず白星を勝ち取ると誓って。","slog_204000260","slog_204000260","Chara_icon/204000260","Chara_icon/204000260","2021/09/14 7:00:00",True,"1", -205000180,999,"《熱烈な鼓舞》リズベット",4,2,800,15000,100,1,0,0,0,"s205000180",,20,50,22,47,24,44,26,41,28,38,30,35,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_LIS_SUP_016","パワフル・マーチ","パワフル・〇〇〇マーチ","自パーティがよろけ・ダウン無効
状態になり、
STRが100%上昇する。
覚醒で効果時間が延長。","よろけ・ダウン無効+自パーティのSTR上昇","友達想いの溌剌とした応援で、
仲間の元気をフル充電!勇気が
出ない時も、彼女の声援が力強く
背中を押してくれる。","slog_205000180","slog_205000180","Chara_icon/205000180","Chara_icon/205000180","2021/09/14 7:00:00",True,"1", -218000070,999,"《飛躍の舞踊》レイン",4,2,800,15000,100,1,0,0,0,"s218000070",,20,50,22,47,24,44,26,41,28,38,30,35,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ドレッドノート・ステップ","ドレッドノート・ステップ","自パーティがよろけ・ダウン無効
状態になり、攻撃時に75%の
確率で追加攻撃を発生させる。
覚醒で効果時間が延長。","よろけ・ダウン無効+攻撃時に確率で追加攻撃","普段メイドで培ったご奉仕力は、
応援の場でも大活躍。細やかな
気配りを受け、誰もが実力以上の
力を発揮できるはず。","slog_218000070","slog_218000070","Chara_icon/218000070","Chara_icon/218000070","2021/09/14 7:00:00",True,"1", -299000920,999,"《真》インフィニティ・モーメント",5,2,1600,20000,100,1,0,0,0,"s299000920",,76,130,81,128,86,126,91,124,96,122,100,120,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"レジェンド・オブ・モーメント","レジェンド・オブ・モーメント","ファーストアタックとラスト
アタックで得られるボーナス
ポイントが15%増加する。
覚醒で効果時間が延長。","ファースト&ラストアタックのボーナスポイント量上昇","浮遊城第100層。その頂に至る
道は、真の英雄にしか歩めない
だろう。死闘を共にした仲間たち
と共に、救世主は頂を目指す。","slog_299000920","slog_299000920","Chara_icon/299000920","Chara_icon/299000920","2021/06/08 7:00:00",True,"1", -299000930,999,"《裏》ホロウ・フラグメント",5,2,1600,20000,100,1,0,0,0,"s299000930",,76,130,81,128,86,126,91,124,96,122,100,120,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"レジェンド・オブ・ホロウ","レジェンド・オブ・ホロウ","リーグポイント獲得量が
10%増加する。
覚醒で効果時間が延長。","獲得リーグポイント上昇","浮遊城に隠されたもう一つの
物語。かの世界の裏側にある、
もう一つの世界での冒険譚は、
いつしか宝物に変わる。","slog_299000930","slog_299000930","Chara_icon/299000930","Chara_icon/299000930","2021/06/08 7:00:00",True,"1", -299000940,999,"《絆》ロスト・ソング",5,2,1600,20000,100,1,0,0,0,"s299000940",,20,175,23,155,26,145,29,135,32,125,35,115,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"レジェンド・オブ・ソング","レジェンド・オブ・ソング","スキルポーションとレコード
メダルの位置がミニマップに
表示され、移動速度も上昇する。
覚醒で効果時間が延長。","スキルポーション&レコードメダルサーチ発動+移動速度上昇","妖精界に舞い降りた、1人の天才
アイドル。彼女が育む、人々の
繋がりの可能性とは。不器用な
姉妹はいつしか本物の絆を得る。","slog_299000940","slog_299000940","Chara_icon/299000940","Chara_icon/299000940","2021/06/08 7:00:00",True,"1", -299000950,999,"《心》ホロウ・リアリゼーション",5,2,1600,20000,100,1,0,0,0,"s299000950",,25,100,28,90,31,80,34,70,37,65,40,60,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"レジェンド・オブ・オリジン","レジェンド・オブ・オリジン","自パーティのSTR・INTを
120%上昇させる。
覚醒で効果時間が延長。","自パーティのSTR・INT上昇","原初の世界にて、無垢なる存在と
の出会いが生んだ奇跡の物語。
人々との触れ合いが、少女の心を
育んでゆく。","slog_299000950","slog_299000950","Chara_icon/299000950","Chara_icon/299000950","2021/06/08 7:00:00",True,"1", -299000960,999,"《銃》フェイタル・バレット",5,2,1600,20000,100,3,0,0,0,"s299000960",,45,150,47,140,49,135,51,130,53,125,55,120,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"レジェンド・オブ・バレット","レジェンド・オブ・バレット","【獲得ポイントが3位の時のみ発動可能】リーグポイント獲得量が50%増加する。
覚醒で効果時間が延長。","【発動条件・3位】獲得リーグポイント上昇","強さこそが物を言う、銃の世界。
沢山の出会いとバトルを経た先に
待ち受ける命を賭した戦いとは。
これは、君自身の選択の物語。","slog_299000960","slog_299000960","Chara_icon/299000960","Chara_icon/299000960","2021/06/08 7:00:00",True,"1", -218000080,999,"《あなたのために》虹架",5,3,1600,20000,100,1,0,0,0,"s218000080",,1,100,2,95,2,90,3,85,3,80,3,70,1,2,"ドロップ数","{0}個","skill_3_2line_moji86","un_2line_moji86",,"ディア・レイン","ディア・〇〇〇〇レイン","グリードを1体召喚。
覚醒でスキルポーションの
ドロップ数が増加。","グリードを召喚","学校とバイト、そして夢。やらな
きゃいけないことが沢山あるけれ
ど、皆がいるから頑張れる。今日
はお礼に、沢山ご奉仕するね。","slog_218000080","slog_218000080","Chara_icon/218000080","Chara_icon/218000080","2021/06/01 7:00:00",True,"1", -299000970,999,"《お姉ちゃんの休息》イーディス",5,3,1600,20000,100,1,0,0,0,"s299000970",,1,100,2,95,2,90,3,85,3,80,3,70,1,2,"ドロップ数","{0}個","skill_3_2line_moji86","un_2line_moji86",,"ディア・イーディス","ディア・〇〇〇〇イーディス","グリードを1体召喚。
覚醒でスキルポーションの
ドロップ数が増加。","グリードを召喚","今日は任務もお休み。だから、
一緒に過ごせる時間を大切にしよ
う。愛すべき日常も、永遠に続く
とは限らないから。","slog_299000970","slog_299000970","Chara_icon/299000970","Chara_icon/299000970","2021/07/01 7:00:00",True,"1", -299000980,999,"《今日は素直に》メディナ",5,3,1600,20000,100,1,0,0,0,"s299000980",,1,100,2,95,2,90,3,85,3,80,3,70,1,2,"ドロップ数","{0}個","skill_3_2line_moji86","un_2line_moji86",,"ディア・メディナ","ディア・〇〇〇〇メディナ","グリードを1体召喚。
覚醒でスキルポーションの
ドロップ数が増加。","グリードを召喚","普段は照れ臭くて伝えられないこ
とも、特別な今日は態度と言葉で
示してみたい。仲間以上に大切な
あなたへ。","slog_299000980","slog_299000980","Chara_icon/299000980","Chara_icon/299000980","2021/08/01 7:00:00",True,"1", -215000100,999,"《お出かけモード》ストレア",5,3,1600,20000,100,1,0,0,0,"s215000100",,1,100,2,95,2,90,3,85,3,80,3,70,1,2,"ドロップ数","{0}個","skill_3_2line_moji86","un_2line_moji86",,"ディア・ストレア","ディア・〇〇〇〇ストレア","グリードを1体召喚。
覚醒でスキルポーションの
ドロップ数が増加。","グリードを召喚","今日は一緒にお出かけの日!
とびきり可愛いお洋服で、今日は
あなたを独り占め。さぁ、どこへ
行ってみようか?","slog_215000100","slog_215000100","Chara_icon/215000100","Chara_icon/215000100","2021/09/01 7:00:00",True,"1", -299000990,999,"《自分の道を》紅葉",5,3,1600,20000,100,1,0,0,0,"s299000990",,1,100,2,95,2,90,3,85,3,80,3,70,1,2,"ドロップ数","{0}個","skill_3_2line_moji86","un_2line_moji86",,"ディア・クレハ","ディア・〇〇〇〇クレハ","グリードを1体召喚。
覚醒でスキルポーションの
ドロップ数が増加。","グリードを召喚","上を見上げてばかりの人生だった
けれど、もう迷わない。今日と言
う特別な日を、胸を張って生きて
いけるのは、君が一緒だから。","slog_299000990","slog_299000990","Chara_icon/299000990","Chara_icon/299000990","2021/10/01 7:00:00",True,"1", -217000070,999,"《今日は家族と》七色",5,3,1600,20000,100,1,0,0,0,"s217000070",,1,100,2,95,2,90,3,85,3,80,3,70,1,2,"ドロップ数","{0}個","skill_3_2line_moji86","un_2line_moji86",,"ディア・セブン","ディア・〇〇〇〇セブン","グリードを1体召喚。
覚醒でスキルポーションの
ドロップ数が増加。","グリードを召喚","大切な日は、大切な人と。多忙な
研究も今日はお休みにして、遠い
かの地へ行ってみようか。大切な
仲間と、家族の待つ場所へ。","slog_217000070","slog_217000070","Chara_icon/217000070","Chara_icon/217000070","2021/11/01 7:00:00",True,"1", -299001000,999,"《ずっと一緒に》プレミア&ティア",5,3,1600,20000,100,1,0,0,0,"s299001000",,1,100,2,95,2,90,3,85,3,80,3,70,1,2,"ドロップ数","{0}個","skill_3_2line_moji86","un_2line_moji86",,"ディア・プレミア・アンド・ティア","ディア・プレミア・アンド・ティア","グリードを1体召喚。
覚醒でスキルポーションの
ドロップ数が増加。","グリードを召喚","共に生まれた2人は、それぞれの
道で、心を育んでいった。2人の
大事な今日は、思い出を分け合っ
て、皆で一緒に過ごそうか。","slog_299001000","slog_299001000","Chara_icon/299001000","Chara_icon/299001000","2021/12/01 7:00:00",True,"1", -299001010,999,"《もちもちお年賀》テリア",5,3,1600,20000,100,1,0,0,0,"s299001010",,1,100,2,95,2,90,3,85,3,80,3,70,1,2,"ドロップ数","{0}個","skill_3_2line_moji86","un_2line_moji86",,"ディア・テリア","ディア・〇〇〇〇テリア","グリードを1体召喚。
覚醒でスキルポーションの
ドロップ数が増加。","グリードを召喚","βeater's cafeにも
お正月が到来!もちもち伸び~る
お雑煮片手に、本年もよろしく
お願いいたします!","slog_299001010","slog_299001010","Chara_icon/299001010","Chara_icon/299001010","2022/01/01 7:00:00",True,"1", -299001020,999,"《芽吹きの季節》小春",5,3,1600,20000,100,1,0,0,0,"s299001020",,1,100,2,95,2,90,3,85,3,80,3,70,1,2,"ドロップ数","{0}個","skill_3_2line_moji86","un_2line_moji86",,"ディア・コハル","ディア・〇〇〇〇コハル","グリードを1体召喚。
覚醒でスキルポーションの
ドロップ数が増加。","グリードを召喚","春目前の今日。穏やかに笑って
迎えられたのは、隣に君がいてく
れたから。この気持ちが芽吹くの
も、もう時間の問題かも。","slog_299001020","slog_299001020","Chara_icon/299001020","Chara_icon/299001020","2022/02/01 7:00:00",True,"1", -216000090,999,"《現実の宝物》琴音",5,3,1600,20000,100,1,0,0,0,"s216000090",,1,100,2,95,2,90,3,85,3,80,3,70,1,2,"ドロップ数","{0}個","skill_3_2line_moji86","un_2line_moji86",,"ディア・フィリア","ディア・〇〇〇〇フィリア","グリードを1体召喚。
覚醒でスキルポーションの
ドロップ数が増加。","グリードを召喚","君がくれた宝物は、ずっと心の
真ん中にある。現実世界で過ごす
特別な今日を、君と共に。一緒な
ら、宝物ももっと見つかるから。","slog_216000090","slog_216000090","Chara_icon/216000090","Chara_icon/216000090","2022/03/01 7:00:00",True,"1", -299001030,999,"《いつも通りの日々》雪嗣",5,3,1600,20000,100,1,0,0,0,"s299001030",,1,100,2,95,2,90,3,85,3,80,3,70,1,2,"ドロップ数","{0}個","skill_3_2line_moji86","un_2line_moji86",,"ディア・イツキ","ディア・〇〇〇〇イツキ","グリードを1体召喚。
覚醒でスキルポーションの
ドロップ数が増加。","グリードを召喚","どこか味気ないのは、隣に君がい
ないからだろうか。いつも通りの
日常の最中。特別な日に、特別な
存在である君を想って。","slog_299001030","slog_299001030","Chara_icon/299001030","Chara_icon/299001030","2022/03/30 7:00:00",True,"1", -299001040,999,"《お仕事の時間》翠子",5,3,1600,20000,100,1,0,0,0,"s299001040",,1,100,2,95,2,90,3,85,3,80,3,70,1,2,"ドロップ数","{0}個","skill_3_2line_moji86","un_2line_moji86",,"ディア・ツェリスカ","ディア・〇〇〇〇ツェリスカ","グリードを1体召喚。
覚醒でスキルポーションの
ドロップ数が増加。","グリードを召喚","特別な今日だからこそ、普段通り
仕事に向かう。誇りある大事な仕
事だから。これが終わったら、皆
でパーッと銃をぶっ放そう!","slog_299001040","slog_299001040","Chara_icon/299001040","Chara_icon/299001040","2022/05/01 7:00:00",True,"1", -204000270,999,"《極暑の涼銃》シノン",5,3,1600,20000,100,1,0,0,0,"s204000270",,65,210,68,204,71,201,73,198,75,196,77,194,10,20,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スプラッシュ・バレット","スプラッシュ・〇バレット","一定時間、前方にレーザー状の強力な水属性攻撃を照射し敵にダメージを与える。覚醒で攻撃力が上昇。","一定時間、レーザー状の強力な水属性攻撃を前方に照射","愛銃が水鉄砲に大変身?!…とい
うわけでなく海辺のマナーを守っ
て込める弾丸は海水に。武器が変
わろうと、腕前は変わらない。","slog_204000270","slog_204000270","Chara_icon/204000270","Chara_icon/204000270","2021/07/27 7:00:00",True,"1", -201000170,999,"《大波の黒弾》キリト",4,3,800,15000,100,1,0,0,0,"s201000170",,40,165,50,151,55,144,60,137,65,130,70,123,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スプラッシュ・リロード","スプラッシュ・〇リロード","一定時間、前方に水属性の弾を連射して敵にダメージを与える。覚醒で攻撃力が上昇。","一定時間、前方に水属性の弾を連射","銃と剣のプレイスタイルから一転
銃のみで戦うと決めたよう。引き
金を引かれる前に、素早く距離を
つめるのは彼しか出来ない芸当。","slog_201000170","slog_201000170","Chara_icon/201000170","Chara_icon/201000170","2021/07/27 7:00:00",True,"1", -203000270,999,"《嬉戯の煌花》リーファ",4,3,800,15000,100,1,0,0,0,"s203000270",,40,165,50,151,55,144,60,137,65,130,70,123,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スプラッシュ・ショット","スプラッシュ・〇ショット","一定時間、前方に水属性の弾を放射状に連射して敵にダメージを与える。覚醒で攻撃力が上昇。","一定時間、前方に水属性の弾を放射状に連射","剣道で鍛えた動体視力は伊達じゃ
ない。砂浜のガンナーとして妖精
の実力を見せる時。いざとなれば
間合いをつめて戦うそう。","slog_203000270","slog_203000270","Chara_icon/203000270","Chara_icon/203000270","2021/07/27 7:00:00",True,"1", -209000240,999,"《炎陽の閃水》ユウキ",4,3,800,15000,100,1,0,0,0,"s209000240",,40,165,50,151,55,144,60,137,65,130,70,123,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スプラッシュ・ストライカー","スプラッシュ・〇ストライカー","一定時間、前方にレーザー状の水属性攻撃を照射し敵にダメージを与える。覚醒で攻撃力が上昇。","一定時間、レーザー状の水属性攻撃を前方に照射","彼女の編み出した11連撃が水鉄
砲に適用され、11連射に?!
生まれ変わった愛剣と辻デュエル
を挑んで勝利しよう。","slog_209000240","slog_209000240","Chara_icon/209000240","Chara_icon/209000240","2021/07/27 7:00:00",True,"1", -206000230,999,"《キャンディポップ》シリカ",5,2,1600,20000,100,1,0,0,0,"s206000230",,10,120,11,116,12,112,13,108,14,104,15,100,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_SIL_SUP_024","レモネード・サマー","レモネード・〇〇サマー","自パーティのSTRを700%上昇させる。覚醒で効果時間が延長。","自パーティのSTR上昇","アイスをかじってひと休み。たく
さんのフレーバーから檸檬を選ぶ
と、甘くてすっぱい夏の味が口の
中に広がっていった。","slog_206000230","slog_206000230","Chara_icon/206000230","Chara_icon/206000230","2021/08/03 7:00:00",True,"1", -204000280,999,"《オーシャンケットシー》シノン",4,2,800,15000,100,1,0,0,0,"s204000280",,60,120,65,115,70,110,75,105,78,102,80,100,10,20,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_SIN_SUP_024","スウェイト・テール","スウェイト・〇〇テール","Mobに与えるダメージを100%上昇させ、自パーティの移動速度を上昇させる。覚醒で効果時間が延長。","Mobキラー+移動速度上昇","砂浜で皆と追いかけっこ。銃や弓
よりずっと単純だけど、大好きな
仲間たちとする遊びはなんでも
楽しくて揺れる尻尾が隠せない。","slog_204000280","slog_204000280","Chara_icon/204000280","Chara_icon/204000280","2021/08/03 7:00:00",True,"1", -211000100,999,"《サマーサンシャイン》アリス",4,2,800,15000,100,1,0,0,0,"s211000100",,20,100,22,97,24,94,26,91,28,88,30,85,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_ALI_SUP_024","ブリム・オブ・ハット","ブリム・オブ・〇〇ハット","自パーティに敵の注目を集め、リーグポイント獲得量を30%上昇させる。覚醒で効果時間が延長。","自パーティに敵の注目を集める+獲得リーグポイント上昇","パラソルの下、賑やかな仲間を眺
めながら南国のジュースに口を付
ける。火照る頬がバレないように
麦わら帽子を深く被った。","slog_211000100","slog_211000100","Chara_icon/211000100","Chara_icon/211000100","2021/08/03 7:00:00",True,"1", -290000140,999,"《サマープリティ》ユイ",4,2,800,15000,100,1,0,0,0,"s290000140",,10,40,12,38,14,36,16,34,18,32,20,30,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_YUI_SUP_024","スウィート・ビーチ","スウィート・〇〇ビーチ","自パーティのINTを150%上昇させ、ラストアタックボーナスを35%増加させる。覚醒で効果時間が延長。","INT上昇+ラストアタックで得られるボーナスポイント量上昇","可愛らしいフリルとリボンが彼女
の愛らしさを引き立てる。もう少
し休憩したら、今度は向こうの浜
辺に行こう。","slog_290000140","slog_290000140","Chara_icon/290000140","Chara_icon/290000140","2021/08/03 7:00:00",True,"1", -216000100,999,"《夏夜の彩り》フィリア",5,2,1600,20000,100,1,0,0,0,"s216000100","s216000100_passive",10,65,11,62,12,59,13,56,14,53,15,50,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"モーニング・ナイト","モーニング・〇〇ナイト","自パーティのINTが300%上昇。覚醒で効果時間が延長。【パッシブ】ラストアタック毎にボーナス増加。","自パーティのINT上昇","夜に咲く大輪の朝顔と花火たちが
あまりにも華やかだから、見失い
そうになる。大切な宝物は既に手
の中でそっと咲いているのだ。","slog_216000100","slog_216000100","Chara_icon/216000100","Chara_icon/216000100","2021/08/24 7:00:00",True,"1", -202000330,999,"《月下の憩い》アスナ",4,2,800,15000,100,1,0,0,0,"s202000330",,15,100,16,95,17,90,18,85,19,80,20,75,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_ASU_SUP_025","ノクス・チェリー","ノクス・チェリー","自パーティのSTRとINTを
200%上昇させる。覚醒で効果時間が延長。","自パーティのSTR・INT上昇","喧騒を避けて高台へ。先ほどまで
巡っていた屋台は遠く、静かな境
内には二人の声がよく通る。囁き
あって花火の輪郭をなぞった夏。","slog_202000330","slog_202000330","Chara_icon/202000330","Chara_icon/202000330","2021/08/24 7:00:00",True,"1", -204000290,999,"《納涼の弓姫》シノン",4,2,800,15000,100,1,0,0,0,"s204000290",,10,60,12,58,14,56,16,54,18,52,20,50,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86","CH_SIN_SUP_025","バタフライ・リボン","バタフライ・〇〇リボン","自パーティのSTRを150%上昇させ、ファーストアタックボーナスを35%増加させる。覚醒で効果時間が延長。","STR上昇+ファーストアタックで得られるボーナスポイント量上昇","妖精たちの間を縫って、もっと、
もっと前の席へ。逸る気持ちと足
取りを見失わないよう、揺れる帯
の蝶々を見つめた。","slog_204000290","slog_204000290","Chara_icon/204000290","Chara_icon/204000290","2021/08/24 7:00:00",True,"1", -209000250,999,"《ひと夏の思い出》ユウキ",4,2,800,15000,100,1,0,0,0,"s209000250",,15,50,17,47,19,44,21,41,23,38,25,35,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ピオニー・スパークル","ピオニー・〇〇〇スパークル","自パーティのINTを150%上昇させ、ラストアタックボーナスを30%増加させる。覚醒で効果時間が延長。","INT上昇+ラストストアタックで得られるボーナスポイント量上昇","打ち上げ花火の大きな音が手元の
牡丹を小さく揺らす。いつまでも
落ちないように、心臓の音を花火
に紛れさせた。","slog_209000250","slog_209000250","Chara_icon/209000250","Chara_icon/209000250","2021/08/24 7:00:00",True,"1", -202000340,999,"《スイカ・サースト》アスナ",5,3,1600,20000,100,1,0,0,0,"s202000340",,65,210,68,204,71,201,73,198,75,196,77,194,10,20,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スプリット・オブ・サースト","スプリット・オブ・サースト","一定時間、前方に突属性の強力な弾を連射して敵にダメージを与える。覚醒で攻撃力が上昇。","一定時間、前方に突属性の強力な弾を連射","スイカ割りならぬスイカ刺し?!
美しい剣技で中心を捉えた瞬間、
命中を喜ぶ笑顔が眩しい。それに
しても、どうやって食べよう?","slog_202000340","slog_202000340","Chara_icon/202000340","Chara_icon/202000340","2021/07/27 7:00:00",True,"1", -205000190,999,"《スイカ・ストライク》リズベット",5,3,1600,20000,100,1,0,0,0,"s205000190",,65,210,68,204,71,201,73,198,75,196,77,194,10,20,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スプリット・オブ・ストライク","スプリット・オブ・ストライク","一定時間、前方に打属性の強力な弾を連射して敵にダメージを与える。覚醒で攻撃力が上昇。","一定時間、前方に打属性の強力な弾を連射","自慢のメイスを振りかざし、まっ
すぐスイカに振り落とす。
大きいスイカは皆で、小さなスイ
カはピナ用に取り分けよう。","slog_205000190","slog_205000190","Chara_icon/205000190","Chara_icon/205000190","2021/07/27 7:00:00",True,"1", -208000040,999,"《不可避の魔球》エギル",5,3,1600,20000,100,1,0,0,0,"s208000040",,65,210,68,204,71,201,73,198,75,196,77,194,10,20,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"ソーサリー・スフィア","ソーサリー・〇〇スフィア","ビーチボールを飛ばして周囲の敵に強力なダメージを与える。飛ばすボールは段々大きくなる。覚醒で攻撃力が上昇。","ビーチボールによる強力な範囲攻撃","彼が持つとボールがなんだか小さ
く見える。鍛えられた大きな体の
後ろに、構えられた斧が光るたび
胸が高鳴るのは夏のせい…?","slog_208000040","slog_208000040","Chara_icon/208000040","Chara_icon/208000040","2021/07/27 7:00:00",True,"1", -211000110,999,"《スイカ・セーバー》アリス",5,3,1600,20000,100,1,0,0,0,"s211000110",,65,210,68,204,71,201,73,198,75,196,77,194,10,20,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スプリット・オブ・セーバー","スプリット・オブ・セーバー","一定時間、前方に斬属性の強力な弾を連射して敵にダメージを与える。覚醒で攻撃力が上昇。","一定時間、前方に斬属性の強力な弾を連射","まるで包丁で切り分けたような、
滑らかな断面。綺麗に等分された
スイカは、平等に皆のお腹の中へ
吸い込まれていった。","slog_211000110","slog_211000110","Chara_icon/211000110","Chara_icon/211000110","2021/07/27 7:00:00",True,"1", -220000070,999,"《空砲に駆けて》サチ",5,2,1600,20000,100,1,0,0,0,"s220000070",,10,65,11,61,12,57,13,54,14,52,15,50,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ファイア・サルート","ファイア・〇〇〇サルート","自パーティのSTRを210%上昇させる。覚醒で効果時間が延長。【キリトのみ効果が2倍】","【効果上昇・キリト】STR上昇","射的に輪投げ、金魚すくい…。夢
中になっていたら、いつの間にか
花火大会を告げる空砲が。全て打
ちあがる前に彼女の手を取った。","slog_220000070","slog_220000070","Chara_icon/220000070","Chara_icon/220000070","2021/07/27 7:00:00",True,"1", -203000280,999,"《湖上の大輪》リーファ",5,2,1600,20000,100,1,0,0,0,"s203000280",,60,140,65,136,70,132,75,128,80,124,85,120,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ラクス・ブロッサム","ラクス・〇〇〇〇ブロッサム","ファーストアタックとラストアタックで得られるボーナスポイントが25%増加する。覚醒で効果時間が延長。","ファースト&ラストアタックのボーナスポイント量上昇","湖面に映る仕掛け花火たちの間か
ら高く高く打ち上げられていく。
桟橋に手をかけて身を乗り出せば
空いっぱいに光が広がった。","slog_203000280","slog_203000280","Chara_icon/203000280","Chara_icon/203000280","2021/07/27 7:00:00",True,"1", -202000350,999,"《海中散策》アスナ",5,1,1600,20000,100,1,0,0,0,"s202000350",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"ウォークイン・マリーン","ウォークイン・〇マリーン","戦闘不能状態のメンバーを2人復帰させ、15秒間獲得リーグポイントを20%増加させる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+獲得リーグポイント上昇","色とりどりの魚たちを辿っていた
ら、随分と遠くまで来ていた。
もしかしたら、珊瑚が連なる彼ら
の家まで招待してくれたのかも。","slog_202000350","slog_202000350","Chara_icon/202000350","Chara_icon/202000350","2021/07/27 7:00:00",True,"1", -205000200,999,"《海上遊歩》リズベット",5,1,1600,20000,100,1,0,0,0,"s205000200",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"ウォークオン・マリーン","ウォークオン・〇マリーン","戦闘不能状態のメンバーを2人復帰させ、10秒間自パーティの移動速度を上昇させる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+移動速度上昇","波と潮風が自由気ままに彼女を揺
らす。元気いっぱいな皆のムード
メーカーも、今日はゆっくりと海
水浴を楽しんでいるようだ。","slog_205000200","slog_205000200","Chara_icon/205000200","Chara_icon/205000200","2021/07/27 7:00:00",True,"1", -299001050,999,"《海飛沫ガード》ロニエ",5,1,1600,20000,100,1,0,0,0,"s299001050",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"ディフェンス・スプリンクル","ディフェンス・〇スプリンクル","戦闘不能状態のメンバーを2人復帰させ、15秒間自パーティのINTを140%上昇させる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+自パーティのINT上昇","ちょっとしたじゃれ合いが、いつ
の間にか水の掛け合いに発展。
水越しにちらりと見える親友のい
たずらっぽい笑顔が眩しくて。","slog_299001050","slog_299001050","Chara_icon/299001050","Chara_icon/299001050","2021/07/27 7:00:00",True,"1", -299001060,999,"《海飛沫アタック》ティーゼ",5,1,1600,20000,100,1,0,0,0,"s299001060",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"オフェンス・スプリンクル","オフェンス・〇〇スプリンクル","戦闘不能状態のメンバーを2人復帰させ、15秒間自パーティのSTRを100%上昇させる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+自パーティのSTR上昇","先に仕掛けたのはどちらだったか
既にお互いの水着はびしょ濡れ。
親友に放った飛沫が太陽に反射し
て今日の思い出のように輝いた。","slog_299001060","slog_299001060","Chara_icon/299001060","Chara_icon/299001060","2021/07/27 7:00:00",True,"1", -201000180,999,"《ジェット・ヴォルフ》キリト",5,2,1600,20000,100,1,0,0,0,"s201000180","s201000180_passive",30,200,33,196,36,192,39,188,42,184,45,180,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ウルフ・ラン","ウルフ・ラン","自パーティの移動速度を上昇させる。覚醒で効果時間が延長。【パッシブ】ラストアタックを重ねる毎にボーナスが増加。","移動速度上昇","トリック・オア・トリート!
闇夜に紛れて現れるという一匹の
黒狼。きらりと悪戯っぽく光る大
きな瞳と目が合った。","slog_201000180","slog_201000180","Chara_icon/201000180","Chara_icon/201000180","2021/10/05 7:00:00",True,"1", -204000300,999,"《弓猫の魔法》シノン",4,3,800,15000,100,1,0,0,0,"s204000300",,40,165,50,151,55,144,60,137,65,130,70,123,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"ウィッチ・シューティング","ウィッチ・〇〇〇シューティング","一定時間、前方に矢を連射して敵にダメージと魅了を与える。覚醒で攻撃力が上昇。","一定時間、前方に魅了効果のある矢を連射","トリック・オア・トリート!
お菓子を探している間にカウント
ダウンが始まる。楽しそうな声色
と共に、悪戯が近づいてくる。","slog_204000300","slog_204000300","Chara_icon/204000300","Chara_icon/204000300","2021/10/05 7:00:00",True,"1", -209000260,999,"《トリックキャット》ユウキ",4,3,800,15000,100,1,0,0,0,"s209000260",,40,165,50,151,55,144,60,137,65,130,70,123,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"キャット・スタンピード","キャット・〇〇〇スタンピード","一定時間、前方に斬属性の弾を連射して敵にダメージを与える。覚醒で攻撃力が上昇。","一定時間、前方に斬属性の弾を連射","トリック・オア・トリート!
今日のために用意した甘いお菓子
は彼女の呪文で消えていく。お気
に召したものはどれだろう?","slog_209000260","slog_209000260","Chara_icon/209000260","Chara_icon/209000260","2021/10/05 7:00:00",True,"1", -216000110,999,"《キャットマジック》フィリア",4,3,800,15000,100,1,0,0,0,"s216000110",,40,165,50,151,55,144,60,137,65,130,70,123,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"パンプキン・カーニバル","パンプキン・〇〇カーニバル","無数のカボチャを降らせて打属性の広範囲攻撃を行い、敵にダメージを与える。
覚醒で攻撃力が上昇。","打属性の広範囲攻撃","トリック・オア・トリート!
お菓子はいつの間にか彼女の手元
に!魔法か宝探しの技術なのか、
それはハロウィンの秘密にして。","slog_216000110","slog_216000110","Chara_icon/216000110","Chara_icon/216000110","2021/10/05 7:00:00",True,"1", -203000290,999,"《黒煌の剣影》リーファ",5,2,1600,20000,100,1,0,0,0,"s203000290",,30,200,33,193,36,186,39,179,42,172,45,165,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"チャーミング・ファッション","チャーミング・〇ファッション","周囲のMobに魅了を付与する。さらに、リーグポイントの獲得量を40%増加させる。覚醒で効果時間が延長。","周囲のMobを魅了+獲得リーグポイント上昇","白い肌に黒いドレスがよく映え
る。裾を翻しながら、可憐に戦う
様を見てあるものは死神を思い、
あるものは戦乙女を見出した。","slog_203000290","slog_203000290","Chara_icon/203000290","Chara_icon/203000290","2021/10/26 7:00:00",True,"1", -205000210,999,"《漆黒の桃華》リズベット",4,2,800,15000,100,1,0,0,0,"s205000210",,20,100,22,97,24,94,26,91,28,88,30,85,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"プリティ・ファッション","プリティ・〇〇〇ファッション","周囲のMobに魅了を付与する。さらに、ファーストアタックボーナスを50%増加させる。覚醒で効果時間が延長。","周囲のMobを魅了+ファーストアタックで得られるボーナスポイント量上昇","大人になり切れない少女のあどけ
なさを隠すように桃色の薔薇と黒
ドレスが彼女を包む。すらりと伸
びる足から慌てて目を逸らした。","slog_205000210","slog_205000210","Chara_icon/205000210","Chara_icon/205000210","2021/10/26 7:00:00",True,"1", -215000110,999,"《ゴシックビウィッチ》ストレア",4,2,800,15000,100,1,0,0,0,"s215000110",,20,100,22,97,24,94,26,91,28,88,30,85,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"エレガント・ファッション","エレガント・〇〇ファッション","周囲のMobに魅了を付与する。さらに、ラストアタックボーナスを60%増加させる。覚醒で効果時間が延長。","周囲のMobを魅了+ラストアタックで得られるボーナスポイント量上昇","フリルとリボンの甘さすら艶やか
な色気に変えてしまう。日傘の下
の彼女を知りたくて、気が付くと
傘の柄に手をかけた。","slog_215000110","slog_215000110","Chara_icon/215000110","Chara_icon/215000110","2021/10/26 7:00:00",True,"1", -290000150,999,"《可憐な小華》ユイ",4,2,800,15000,100,1,0,0,0,"s290000150",,20,100,22,97,24,94,26,91,28,88,30,85,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"キュート・ファッション","キュート・〇〇〇ファッション","周囲のMobに魅了を付与する。さらに、リーグポイントの獲得量を30%増加させる。覚醒で効果時間が延長。","周囲のMobを魅了+獲得リーグポイント上昇","白色と桃色のフリルが重なる姿は
まるでイチゴのパフェのよう。
帽子が飛ばされないように、大好
きな両親を振り返った。","slog_290000150","slog_290000150","Chara_icon/290000150","Chara_icon/290000150","2021/10/26 7:00:00",True,"1", -202000360,999,"《聖夜の純愛》アスナ",5,3,1600,20000,100,1,0,0,0,"s202000360",,65,210,68,204,71,201,73,198,75,196,77,194,10,20,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"プレゼント・アバランチ","プレゼント・〇〇アバランチ","大量のプレゼントを前方に飛ばす強力な遠距離攻撃。覚醒で攻撃力が上昇。","プレゼントによる強力な遠距離攻撃","大切なあの人に渡すプレゼントは
素敵なラッピングで。自分のこと
も可愛い洋服で飾り付けたら、あ
とは彼を待つのみ。","slog_202000360","slog_202000360","Chara_icon/202000360","Chara_icon/202000360","2021/12/07 7:00:00",True,"1", -204000310,999,"《彩氷の福音》シノン",4,2,800,15000,100,1,0,0,0,"s204000310",,44,140,46,136,48,134,50,132,51,130,52,128,10,20,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ジングル・ギフト","ジングル・〇〇〇ギフト","ボスに与えるダメージを100%上昇させ、リーグポイントの獲得量も15%増加させる。覚醒で効果時間が延長。","ボスキラー+獲得リーグポイント上昇","いつもはあなたに恋するただの女
の子。けれど、今夜はあなたに喜
んで欲しいから、サンタになって
プレゼントを届けよう。","slog_204000310","slog_204000310","Chara_icon/204000310","Chara_icon/204000310","2021/12/07 7:00:00",True,"1", -209000270,999,"《冬宴の楽しみ》ユウキ",4,2,800,15000,100,1,0,0,0,"s209000270",,44,140,46,136,48,134,50,132,51,130,52,128,10,20,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ジングル・プレゼント","ジングル・〇〇〇プレゼント","自パーティのSTRを75%上昇させ、リーグポイントの獲得量も15%増加させる。覚醒で効果時間が延長。","STR上昇+獲得リーグポイント上昇","ツリーの下で待ち合わせ。来るま
で待とうと思ったけれど、大切な
人に、いの一番に渡したくて。姿
を見つけて駆けだした。","slog_209000270","slog_209000270","Chara_icon/209000270","Chara_icon/209000270","2021/12/07 7:00:00",True,"1", -216000120,999,"《魅惑の甘味》フィリア",4,2,800,15000,100,1,0,0,0,"s216000120",,44,140,46,136,48,134,50,132,51,130,52,128,10,20,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ジングル・スウィート","ジングル・〇〇〇スウィート","Mobに与えるダメージを100%上昇させ、リーグポイントの獲得量も15%増加させる。覚醒で効果時間が延長。","Mobキラー+獲得リーグポイント上昇","あなたと過ごすクリスマスは特別
だから、ケーキも同じくらい特別
にしたい。抑えられない気持ちの
分だけ段を増やした。","slog_216000120","slog_216000120","Chara_icon/216000120","Chara_icon/216000120","2021/12/07 7:00:00",True,"1", -220000080,999,"《星空の涙歌》サチ",5,3,1600,20000,100,1,0,0,0,"s220000080",,90,250,99,240,108,230,114,220,120,215,123,210,9,18,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"メモリー・オブ・ティアドロップ","メモリー・オブ・ティアドロップ","攻撃を受けるまで、一定時間毎に
リーグポイントを獲得する状態に
なる。
覚醒で効果時間が延長。","敵の攻撃を受けるまでリーグポイント自動獲得状態","最期に伝えたかった言葉は、頬を
伝う涙で掻き消されてしまった。
けれど、その気持ちが優しい彼に
届く日はそう遠くはないはずだ。","slog_220000080","slog_220000080","Chara_icon/220000080","Chara_icon/220000080","2021/09/28 7:00:00",True,"1", -202000370,999,"《ひとときの満悦》アスナ",4,2,800,15000,100,1,0,0,0,"s202000370",,50,180,55,172,60,168,65,164,70,160,75,156,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"メモリー・オブ・クリーム","メモリー・オブ・クリーム","自パーティのINTを75%上昇し、リーグポイントの獲得量も20%増加させる。
覚醒で効果時間が延長。","INT上昇+獲得リーグポイント上昇","目に映るもの触れるもの、周りに
本当のものは一つもないと思って
いた。それなのに、口にした味は
本当だと思えて…。","slog_202000370","slog_202000370","Chara_icon/202000370","Chara_icon/202000370","2021/09/28 7:00:00",True,"1", -205000220,999,"《名工の原石》リズベット",4,2,800,15000,100,1,0,0,0,"s205000220",,55,190,60,182,65,178,70,174,75,170,80,166,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"メモリー・オブ・フォーリング","メモリー・オブ・フォーリング","自パーティのINTを75%上昇し、リーグポイントの獲得量も20%増加させる。
覚醒で効果時間が延長。","INT上昇+獲得リーグポイント上昇","あなたと結んだ掌が、暖かすぎる
ものだから、きっとこれが恋だと
信じたくて。二人で落ちていけた
らと願った。","slog_205000220","slog_205000220","Chara_icon/205000220","Chara_icon/205000220","2021/09/28 7:00:00",True,"1", -206000240,999,"《絆を求める瞳》シリカ",4,2,800,15000,100,1,0,0,0,"s206000240",,45,170,50,162,55,158,60,154,65,150,70,146,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"メモリー・オブ・フラワー","メモリー・オブ・フラワー","自パーティのINTを75%上昇し、リーグポイントの獲得量も20%増加させる。
覚醒で効果時間が延長。","INT上昇+獲得リーグポイント上昇","大切な家族を失って目の前が真っ
暗になったとき、その闇に溶け込
むような優しさで彼は現れた。
戻ってきたら彼の話をしよう。","slog_206000240","slog_206000240","Chara_icon/206000240","Chara_icon/206000240","2021/09/28 7:00:00",True,"1", -298000130,999,"《兎の歓待》リズベット&シリカ",5,3,1600,20000,100,1,0,0,0,"s298000130",,2,220,2,212,3,204,3,196,4,188,4,180,1,2,"ドロップ数","{0}個","skill_3_2line_moji86","un_2line_moji86",,"ギャンブル・タイム","ギャンブル・〇〇タイム","全パーティのそばにランダムで敵Mobのミミックかグリードを4体召喚する。覚醒でスキルポーションのドロップ数が増加。","全パーティの近くにランダムで敵Mob【ミミック】かグリードを召喚","商売繁盛に欠かせないものは美味
しい肉まんと、それから可愛い看
板娘!ライバル店に負けないよう
バニー姿でおもてなししよう。","slog_298000130","slog_298000130","Chara_icon/298000130","Chara_icon/298000130","2021/09/28 7:00:00",True,"1", -215000120,999,"《博打ごっこ》ストレア",5,3,1600,20000,100,1,0,0,0,"s215000120",,65,210,68,204,71,201,73,198,75,196,77,194,10,20,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"サイクロン・ディール","サイクロン・〇〇ディール","一定時間、周囲に斬属性のトランプを連射して敵にダメージを与える。覚醒で攻撃力が上昇。","一定時間、周囲に斬属性のトランプを発射","挑戦者のチップ総どりを目指し
て、ディーラーを体験することに
した彼女。大胆な制服を着こなし
て、いざ勝負!","slog_215000120","slog_215000120","Chara_icon/215000120","Chara_icon/215000120","2021/09/28 7:00:00",True,"1", -202000380,999,"《深まる夜》アスナ",5,1,1600,20000,100,1,0,0,0,"s202000380",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・リリーフロック","スリープウィズ・リリーフロック","戦闘不能状態のメンバーを1人復帰させ、15秒間獲得リーグポイントを30%増加させる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー1人復帰+獲得リーグポイント上昇","昔のこと今日のこと、君のこと私
のこと…。夢を見る前に話したい
ことが尽きなくて、瞼を閉じるの
がこんなにも難しい。","slog_202000380","slog_202000380","Chara_icon/202000380","Chara_icon/202000380","2021/11/02 7:00:00",True,"1", -203000300,999,"《心弛の寝息》リーファ",5,1,1600,20000,100,1,0,0,0,"s203000300",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・シルフィード","スリープウィズ・シルフィード","戦闘不能状態のメンバーを1人復帰させ、15秒間自パーティの移動速度を上昇させる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー1人復帰+移動速度上昇","一つ屋根の下、それは昔と同じだ
けれど、共に寝るには理由が必要
になってしまった。この世界だけ
でいいから、兄の隣で眠りたい。","slog_203000300","slog_203000300","Chara_icon/203000300","Chara_icon/203000300","2021/09/28 7:00:00",True,"1", -204000320,999,"《安堵の就眠》シノン",5,1,1600,20000,100,1,0,0,0,"s204000320",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・スナイプ","スリープウィズ・スナイプ","戦闘不能状態のメンバーを1人復帰させ、15秒間ラストアタックボーナスを50%増加させる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー1人復帰+ラストアタックで得られるボーナスポイント量上昇","一人暮らしの彼女にとって、誰か
と共に寝ることは珍しくて暖かい
体験だった。冷たいベッドで寝る
よりも、よい夢見になりそうだ。","slog_204000320","slog_204000320","Chara_icon/204000320","Chara_icon/204000320","2021/09/28 7:00:00",True,"1", -205000230,999,"《消えゆく憂心》リズベット",5,1,1600,20000,100,1,0,0,0,"s205000230",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・スミス","スリープウィズ・スミス","戦闘不能状態のメンバーを1人復帰させ、45秒間自パーティがよろけ・ダウン無効状態になる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー1人復帰+よろけ・ダウン無効","彼と共に体を横にすると、いつの
間にか小さな不安は消えていく。
何があっても揺るがない彼が夢に
現れたら、気持ちを伝えよう。","slog_205000230","slog_205000230","Chara_icon/205000230","Chara_icon/205000230","2021/11/02 7:00:00",True,"1", -206000250,999,"《抱擁の慕情》シリカ",5,1,1600,20000,100,1,0,0,0,"s206000250",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・テイマー","スリープウィズ・テイマー","戦闘不能状態のメンバーを1人復帰させ、15秒間ファーストアタックボーナスを40%増加。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー1人復帰+ファーストアタックで得られるボーナスポイント量上昇","あの日も宿を共にしたけれど、今
の方が心の距離も近づいた。大き
くなった気持ちに釣り合うくらい
素敵なレディになれますように。","slog_206000250","slog_206000250","Chara_icon/206000250","Chara_icon/206000250","2021/11/02 7:00:00",True,"1", -210000030,999,"《たまゆらの逢瀬》アルゴ",5,1,1600,20000,100,1,0,0,0,"s210000030",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・ラット","スリープウィズ・ラット","戦闘不能状態のメンバーを2人復帰させ、20秒間敵に感知されなくなる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+ハイディング","情報屋として動く彼女が取れる時
間は、どうしたって限られてしま
う。多くない触れ合いだから、
せめて、手を握った。","slog_210000030","slog_210000030","Chara_icon/210000030","Chara_icon/210000030","2021/11/02 7:00:00",True,"1", -217000080,999,"《ご馳走ドリーム》セブン",5,1,1600,20000,100,1,0,0,0,"s217000080",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・ジーニアス","スリープウィズ・ジーニアス","戦闘不能状態のメンバーを2人復帰させ、15秒間周囲の敵のINTを大幅に低下させる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+周囲の敵のINTを低下","素敵な女性になるためにVRでの
食事は我慢中。でも、夢の中なら
いくら食べても大丈夫。…やけに
柔らかい食感だったけど。","slog_217000080","slog_217000080","Chara_icon/217000080","Chara_icon/217000080","2021/09/28 7:00:00",True,"1", -218000090,999,"《胸の旋律》レイン",5,1,1600,20000,100,1,0,0,0,"s218000090",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・レインボー","スリープウィズ・レインボー","戦闘不能状態のメンバーを2人復帰させ、20秒間攻撃時に50%の確率で追加攻撃を発生させる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+追加攻撃","恋愛をテーマにした作詞のために
恋人みたいに触れ合ってみたけれ
ど、訳のわからない気持ちを言葉
にするのはまだ恥ずかしくて…。","slog_218000090","slog_218000090","Chara_icon/218000090","Chara_icon/218000090","2021/09/28 7:00:00",True,"1", -299001070,999,"《和みの理趣》プレミア",5,1,1600,20000,100,1,0,0,0,"s299001070",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・ノーデータ","スリープウィズ・ノーデータ","戦闘不能状態のメンバーを2人復帰させ、45秒間耐久値が1%未満には減らなくなる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+耐久値が1%未満にならなくなる","知りたいことは多いから、あなた
から教えてほしくなる。胸の中へ
飛び込んだ理由が分からなくても
それでいいと言ってくれるから。","slog_299001070","slog_299001070","Chara_icon/299001070","Chara_icon/299001070","2021/09/28 7:00:00",True,"1", -299001080,999,"《慥かな腕枕》ティア",5,1,1600,20000,100,1,0,0,0,"s299001080",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・プレイヤー","スリープウィズ・プレイヤー","戦闘不能状態のメンバーを2人復帰させ、15秒間自パーティがダメージを受けなくなる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+ダメージを受けなくなる","ふかふかの枕も、隣に誰かがいる
のも、何だか少し落ち着かない。
けれど、あなたがいるのは不思議
と嫌ではなくて、むしろ…。","slog_299001080","slog_299001080","Chara_icon/299001080","Chara_icon/299001080","2021/09/28 7:00:00",True,"1", -299001090,999,"《溶融の招》キズメル",5,1,1600,20000,100,1,0,0,0,"s299001090",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・ダークエルフ","スリープウィズ・ダークエルフ","戦闘不能状態のメンバーを2人復帰させ、10秒間スキルEXPの獲得量を50%増加させる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+スキルEXPの獲得量上昇","人の彼に優しくできるのは、彼か
ら貰った優しさを返しているだけ
のこと。だから不安があるのなら
遠慮なく溶かしてあげたいのだ。
","slog_299001090","slog_299001090","Chara_icon/299001090","Chara_icon/299001090","2021/11/02 7:00:00",True,"1", -220000090,999,"《泡沫の安臥》サチ",5,1,1600,20000,100,1,0,0,0,"s220000090",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・メモリー","スリープウィズ・メモリー","戦闘不能状態のメンバーを2人復帰させ、15秒間スキルポーションの位置をミニマップに表示。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+スキルポーションサーチ","不安で仕方のない日々を少しでも
乗り越えられたのは彼が隣にいた
から。前とは違う彼の周りを見て
繋いだ手の中に願いを隠した。","slog_220000090","slog_220000090","Chara_icon/220000090","Chara_icon/220000090","2021/11/02 7:00:00",True,"1", -299001100,999,"《打ち明ける閨》エイジ",5,1,1600,20000,100,1,0,0,0,"s299001100",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・ノーチラス","スリープウィズ・ノーチラス","戦闘不能状態のメンバーを2人復帰させ、15秒間周囲の敵のSTRを大幅に低下させる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+周囲の敵のSTRを低下","フルダイブの世界で染みついた恐
怖は中々抜けないけれど、自分の
気持ちを吐露していれば、いつか
前向きになれる予感がして。
","slog_299001100","slog_299001100","Chara_icon/299001100","Chara_icon/299001100","2021/11/02 7:00:00",True,"1", -299001110,999,"《浮きたつ真意》ユナ",5,1,1600,20000,100,1,0,0,0,"s299001110",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・バード","スリープウィズ・バード","戦闘不能状態のメンバーを2人復帰させ、20秒間自パーティのSTR・INTを60%上昇。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+自パーティのSTR・INT上昇","フルダイブの世界は彼女にとって
は新鮮で毎日がドキドキの連続。
あなたに付いていけば、きっと
楽しいがもっと増えていく予感。","slog_299001110","slog_299001110","Chara_icon/299001110","Chara_icon/299001110","2021/11/02 7:00:00",True,"1", -299001160,999,"《前進》プログレッシブ",5,2,1600,20000,100,1,0,0,0,"s299001160",,30,200,33,193,36,186,39,179,42,172,45,165,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"スターレス・ナイト","スターレス・〇〇ナイト","自パーティのSTR・VIT・INT
を80%上昇させ、リーグポイントの獲得量も30%増加させる。覚醒で効果時間が延長。","STR・VIT・INT上昇+獲得リーグポイント上昇","別れよりも出会いに溢れていた
あの頃、長い道のりになる始まり
の物語。デスゲームとなった世界
で彼らはどう生きるのだろう。","slog_299001160","slog_299001160","Chara_icon/299001160","Chara_icon/299001160","2021/10/30 7:00:00",False,"1", -290000160,999,"《端麗巫女》ユイ",5,3,1600,20000,100,1,0,0,0,"s290000160",,30,250,34,248,38,246,40,244,42,242,44,240,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"カグラベル・ミラクル","カグラベル・〇〇ミラクル","レコードメダルを再配置し、
リーグポイント獲得量も
50%増加。
覚醒で効果時間が延長。","メダル再配置+リーグポイント獲得量上昇","澄んだ鈴の音が、振りかかる不幸
を遠ざけようとあたりに響く。
もうシステムコマンドを操れずと
も、仮想世界の神へ呼びかけた。","slog_290000160","slog_290000160","Chara_icon/290000160","Chara_icon/290000160","2021/12/14 7:00:00",True,"1", -202000400,999,"《神域の舞踊》アスナ",4,2,800,15000,100,1,0,0,0,"s202000400",,15,90,16,88,17,86,18,84,19,82,20,80,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"オラクル・ダンス","オラクル・〇〇〇ダンス","レコードメダルとグリードの位置をミニマップに表示し、リーグポイントの獲得量を20%増加させる。覚醒で効果時間が延長。","レコードメダル&グリードサーチ発動+リーグポイントの獲得量上昇","太陽を受けて煌めく栗色の髪が、
白い装束に影を落とす。その舞は
天女の如く美しく、雲中の通り道
など閉じてしまえと願った。","slog_202000400","slog_202000400","Chara_icon/202000400","Chara_icon/202000400","2021/12/14 7:00:00",True,"1", -206000270,999,"《可憐巫女》シリカ",4,3,800,15000,100,1,0,0,0,"s206000270",,25,180,30,170,33,160,36,155,38,150,40,145,5,10,"召喚増強レベル","{0}レベル","skill_3_2line_moji86","un_2line_moji86",,"シキガミ・サーヴァント","シキガミ・〇〇〇サーヴァント","偶像系Mobを3体召喚し、戦闘支援を受ける。
覚醒で召喚Mobのレベルが
増強。","お助けMob【偶像系】を召喚","清らかで穢れのない巫に相応しい
彼女が神楽鈴を鳴らすたび、心が
惹きつけられる。
空の上の神様と、同じように。","slog_206000270","slog_206000270","Chara_icon/206000270","Chara_icon/206000270","2021/12/14 7:00:00",True,"1", -215000150,999,"《玲瓏巫女》ストレア",4,2,800,15000,100,1,0,0,0,"s215000150",,30,100,32,96,34,92,36,88,38,84,40,80,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ゴッズ・ロード","ゴッズ・〇〇〇〇ロード","自パーティがよろけ・ダウン無効状態になり、
移動速度も上昇する。
覚醒で効果時間が延長。","よろけ・ダウン無効+移動速度上昇","姉たちの鳴らす鈴の音に続いて、
大幣をリズミカルに振るう。あな
たが健やかに過ごせるよう、電脳
世界のどこかへ祈りを捧げた。","slog_215000150","slog_215000150","Chara_icon/215000150","Chara_icon/215000150","2021/12/14 7:00:00",True,"1", -209000300,999,"《鏡花水月》ユウキ",5,2,1600,20000,100,1,0,0,0,"s209000300",,5,150,6,145,7,140,8,135,9,132,10,130,1,2,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ハネツキ・ビッグチャンス","ハネツキ・〇〇〇ビッグチャンス","ラストアタックで得られるボーナスポイントが600%増加する。覚醒で効果時間が延長。","ラストアタックで得られるボーナスポイント量上昇","羽根を落としたら負けるルールは
もちろん《ALO》でも同じ。
羽子板に打ち返された渾身の一打
に自慢の翅で追いつこう。","slog_209000300","slog_209000300","Chara_icon/209000300","Chara_icon/209000300","2021/12/28 7:00:00",True,"1", -201000190,999,"《萬福衝打》キリト",4,2,800,15000,100,1,0,0,0,"s201000190",,5,130,6,128,7,126,8,124,9,122,10,120,1,2,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"モチツキ・チャンス","モチツキ・〇〇〇チャンス","ファーストアタックとラストアタックで得られるボーナスポイントが250%増加する。覚醒で効果時間が延長。","ファースト&ラストアタックのボーナスポイント量上昇","つきたてのお餅を食べたいという
娘の願いに応えるべく張り切って
杵を振りかぶる。夫婦のやる気は
満ちて、息ピッタリの餅つきだ。","slog_201000190","slog_201000190","Chara_icon/201000190","Chara_icon/201000190","2021/12/28 7:00:00",True,"1", -203000320,999,"《錦上添花》直葉",4,2,800,15000,100,1,0,0,0,"s203000320",,5,130,6,128,7,126,8,124,9,122,10,120,1,2,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ラクガン・チャンス","ラクガン・〇〇〇チャンス","ファーストアタックで得られる
ボーナスポイントが300%
増加する。
覚醒で効果時間が延長。","ファーストアタックで得られるボーナスポイント量上昇","甘酒を呑めない代わりに、碗に
盛り付けられた落雁を頬張る。
優しい甘さが癖になって、つい
つい手が止まらない。","slog_203000320","slog_203000320","Chara_icon/203000320","Chara_icon/203000320","2021/12/28 7:00:00",True,"1", -204000340,999,"《雲中白鶴》シノン",4,2,800,15000,100,1,0,0,0,"s204000340",,5,130,6,128,7,126,8,124,9,122,10,120,1,2,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ハマヤ・チャンス","ハマヤ・〇〇〇〇チャンス","ラストアタックで得られるボーナスポイントが350%増加する。覚醒で効果時間が延長。","ラストアタックで得られるボーナスポイント量上昇","射手に相応しい縁起物の破魔矢は
禍を払い好機を射止めるという。
直感を信じて心の矢をつがえれば
あなたを射止められそう。","slog_204000340","slog_204000340","Chara_icon/204000340","Chara_icon/204000340","2021/12/28 7:00:00",True,"1", -206000280,999,"《火照り身少女》シリカ",5,3,1600,20000,100,1,0,0,0,"s206000280",,65,210,68,204,71,201,73,198,75,196,77,194,10,20,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"ベイシン・ガーディアンズ","ベイシン・〇〇〇ガーディアンズ","一定時間周囲に留まった後、前方に飛んで行く打属性の風呂桶を呼び出す。覚醒で攻撃力が上昇。","打属性の風呂桶による強力な攻撃","身も心も温まり、お湯から上がる
と予想外の人物が?!固まってし
まった彼女に慌てて弁明するが、
どこかから桶が飛んできて…。","slog_206000280","slog_206000280","Chara_icon/206000280","Chara_icon/206000280","2022/01/18 7:00:00",True,"1", -205000250,999,"《ほっと一息》リズベット",4,1,800,15000,100,1,0,0,0,"s205000250",,30,85,35,75,40,70,45,65,50,60,55,55,10,20,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"ホット・ヒート・エリア","ホット・ヒート・〇エリア","戦闘不能状態のメンバーを2人復帰させ、周囲の敵に混乱を付与。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+周囲の敵に混乱付与","鍛冶師としてハンマーを振るい、
店主として接客も怠らない。
日々の疲れを温泉で癒していると
心地よさに溶けてしまいそうだ。","slog_205000250","slog_205000250","Chara_icon/205000250","Chara_icon/205000250","2022/01/18 7:00:00",True,"1", -209000310,999,"《湯船の遊影》ユウキ",4,3,800,15000,100,1,0,0,0,"s209000310",,40,165,50,151,55,144,60,137,65,130,70,123,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"ショッキング・ホットスパ","ショッキング・〇ホットスパ","周囲に火属性の温泉で範囲攻撃を行い、同時に自パーティの耐久値と状態異常を回復する。覚醒で攻撃力が上昇。","火属性の範囲攻撃+耐久値と状態異常を回復","ひろ~い露天風呂に浮かれて、
思わず走り出してしまった彼女。
あなたの咎める声も聞かず、その
まま飛沫を上げて湯船にダイブ!","slog_209000310","slog_209000310","Chara_icon/209000310","Chara_icon/209000310","2022/01/18 7:00:00",True,"1", -217000100,999,"《保養の湯掛》セブン",4,2,800,15000,100,1,0,0,0,"s217000100",,50,120,54,116,58,112,62,108,66,104,70,100,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ロング・バスタイム","ロング・〇〇〇〇バスタイム","リーグポイント獲得量が
10%増加する。
覚醒で効果時間が延長。","獲得リーグポイント上昇","何事もメリハリが大切。研究に
息詰まったら温泉で気分転換を。
ロシアで育った彼女には、裸の
付き合いはまだ恥ずかしいよう。","slog_217000100","slog_217000100","Chara_icon/217000100","Chara_icon/217000100","2022/01/18 7:00:00",True,"1", -203000330,999,"《一途なラブハート》リーファ",5,2,1600,20000,100,3,0,0,0,"s203000330",,40,200,42,192,44,184,46,176,48,168,50,160,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ラブハート・メイクミラクル","ラブハート・〇〇メイクミラクル","【獲得ポイントが3位の時のみ発動可能】リーグポイント獲得量が100%増加する。
覚醒で効果時間が延長。","【発動条件・3位】獲得リーグポイント上昇","あたしの気持ちが、余すことなく
伝わりますように…。ハートに
込めた想いを味わってほしくて、
あなたの口に放り込んだ。","slog_203000330","slog_203000330","Chara_icon/203000330","Chara_icon/203000330","2022/02/08 7:00:00",True,"1", -202000410,999,"《愛のメッセージ》アスナ",4,3,800,15000,100,1,0,0,0,"s202000410",,40,165,50,151,55,144,60,137,65,130,70,123,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"ハート・フォートレス","ハート・〇〇〇〇フォートレス","触れた敵に魅了を付与するハートが周囲に出現。ハートは一定時間経過後に爆発し、ダメージを与える。覚醒で攻撃力が上昇。","魅了&範囲攻撃を行うハートが周囲に出現","チョコレートに込めた気持ちを
知ってほしいのはあなただけ。
リボンがほどかれたら想いを伝え
るから、受け取ってほしくて。","slog_202000410","slog_202000410","Chara_icon/202000410","Chara_icon/202000410","2022/02/08 7:00:00",True,"1", -204000350,999,"《愛情の弾丸》シノン",4,3,800,15000,100,1,0,0,0,"s204000350",,40,165,50,151,55,144,60,137,65,130,70,123,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"ハート・バレット","ハート・〇〇〇〇バレット","敵にダメージを与えるハートを前方に射出。着弾後、魅了を付与する爆発を起こす。覚醒で攻撃力が上昇。","前方にハート型の弾を飛ばして攻撃&広範囲に魅了を付与","早く気持ちが伝わるように、けれ
どタイミングは見極めて。今日こ
そあなたの胸を撃ち抜きたくて、
チョコレートに想いを込めた。","slog_204000350","slog_204000350","Chara_icon/204000350","Chara_icon/204000350","2022/02/08 7:00:00",True,"1", -216000150,999,"《甘い恋心》フィリア",4,2,800,15000,100,1,0,0,0,"s216000150",,16,120,18,110,20,100,22,90,24,85,25,80,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ハート・サーチャー","ハート・〇〇〇〇サーチャー","レコードメダルとグリードの位置
をミニマップに表示、自パーティ
のINTを50%上昇させる。
覚醒で効果時間が延長。","レコードメダル&グリードサーチ発動+自パーティのINT上昇","甘くとろけるチョコレートは、
言葉にできない想いのよう。何度
でも、あなたの笑顔を見たいから
アタシの気持ちをお裾分け!","slog_216000150","slog_216000150","Chara_icon/216000150","Chara_icon/216000150","2022/02/08 7:00:00",True,"1", -202000420,999,"《聖夜は白く》アスナ",5,3,1600,20000,100,1,0,0,0,"s202000420",,65,210,68,204,71,201,73,198,75,196,77,194,10,20,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"ジングル・ワルツ","ジングル・〇〇〇ワルツ","敵に接触するか一定時間経過で爆発するベルを周囲に出現させる。覚醒で攻撃力が上昇。","周囲に浮遊するベルによる強力な範囲攻撃","雪降る夜のホワイトクリスマスに
現れた真っ白なサンタさん。
あなたの聖夜が輝かしく、白く
あるようにと贈物を届けた。","slog_202000420","slog_202000420","Chara_icon/202000420","Chara_icon/202000420","2021/12/14 7:00:00",True,"1", -203000340,999,"《想望の家路》リーファ",5,3,1600,20000,100,1,0,0,0,"s203000340",,65,210,68,204,71,201,73,198,75,196,77,194,10,20,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"テディベア・パーティー","テディベア・〇〇パーティー","打属性のぬいぐるみを
大量に飛ばして周囲を攻撃する。
覚醒で攻撃力が上昇。","ぬいぐるみによる強力な打属性攻撃","暖炉にあたりながら、あなたの
遅い帰りを今かと待っている。
今日のため悩みぬいたプレゼント
を大事に抱えながら。","slog_203000340","slog_203000340","Chara_icon/203000340","Chara_icon/203000340","2021/12/14 7:00:00",True,"1", -204000360,999,"《宿り木の下で》シノン",5,3,1600,20000,100,1,0,0,0,"s204000360",,65,210,68,204,71,201,73,198,75,196,77,194,10,20,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"サプライズ・ギフト","サプライズ・〇〇ギフト","プレゼントを連射して前方を攻撃する。覚醒で攻撃力が上昇。","プレゼントの連射による強力な遠距離攻撃","細身の後ろに隠しきれない大きな
プレゼント。あなたに喜んでもら
いたくて、でも素直に伝えるのは
恥ずかしくて…。","slog_204000360","slog_204000360","Chara_icon/204000360","Chara_icon/204000360","2021/12/14 7:00:00",True,"1", -211000140,999,"《星に願いを》アリス",5,3,1600,20000,100,1,0,0,0,"s211000140",,65,210,68,204,71,201,73,198,75,196,77,194,10,20,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"ティンクル・ツリー","ティンクル・〇〇ツリー","敵に接触するか一定時間経過で
爆発するツリーを
前方に出現させる。
覚醒で攻撃力が上昇。","ツリーによる強力な範囲攻撃","彼女にとってクリスマスは未知の
文化。プレゼントを用意したり、
飾り付けをしたり、色んな初めて
をあなたと一緒に楽しみたい。","slog_211000140","slog_211000140","Chara_icon/211000140","Chara_icon/211000140","2021/12/14 7:00:00",True,"1", -212000090,999,"《駈走る雪空》ユージオ",5,3,1600,20000,100,1,0,0,0,"s212000090",,65,210,68,204,71,201,73,198,75,196,77,194,10,20,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"トイ・ファランクス","トイ・〇〇〇〇〇ファランクス","敵を追尾して爆発する玩具のロボを前方に並べて出現させる。覚醒で攻撃力が上昇。","玩具のロボによる強力な遠距離攻撃","街中の子供たちの夢が詰まった
麻袋を背中に家々を飛びまわって
いく。寒い夜、凍えることなく
過ごせるようにと願いながら。","slog_212000090","slog_212000090","Chara_icon/212000090","Chara_icon/212000090","2021/12/14 7:00:00",True,"1", -201000200,999,"《視線の先に》キリト",5,2,1600,20000,100,1,0,0,0,"s201000200",,30,100,35,90,39,80,42,70,45,65,48,60,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"アイズ・オブ・ブラッキー","アイズ・オブ・〇〇ブラッキー","自パーティのSTR・INTを
100%上昇させる。
覚醒で効果時間が延長。","自パーティのSTR・INT上昇","頼れる友人と、愛すべき恋人…。
仮想世界で見つけた守りたいもの
全てが、これから先も己の瞳に
映っていてほしいから。","slog_201000200","slog_201000200","Chara_icon/201000200","Chara_icon/201000200","2021/12/14 7:00:00",True,"1", -202000430,999,"《最前へ臨む朝》アスナ",5,2,1600,20000,100,1,0,0,0,"s202000430",,50,120,55,116,60,112,65,108,70,104,75,100,5,10,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"モーニング・ミラーイメージ","モーニング・〇〇ミラーイメージ","自パーティがよろけ・ダウン無効状態になり、スキルポーションの位置がミニマップに表示される。覚醒で効果時間が延長。","よろけ・ダウン無効+スキルポーションサーチ","仮想世界に生きる全員の願いが
華奢な双肩にのしかかる。
誰一人欠けることなく、その理想
を現実にするための朝を迎えた。","slog_202000430","slog_202000430","Chara_icon/202000430","Chara_icon/202000430","2021/12/14 7:00:00",True,"1", -211000150,999,"《夜会を抜け出して》アリス",5,2,1600,20000,100,1,0,0,0,"s211000150",,15,75,16,73,17,71,18,69,19,67,20,65,1,2,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"シークレット・シーカー","シークレット・〇シーカー","敵に感知されなくなり、ファーストアタックボーナスが80%増加する。覚醒で効果時間が延長。","ハイディング+ファーストアタックで得られるボーナスポイント量上昇","華々しさに酔った頬の熱を冷ます
ためバルコニーで夜風を浴びる。
賑やかな空気を背中に感じながら
あなたと共に静かに語り合った。","slog_211000150","slog_211000150","Chara_icon/211000150","Chara_icon/211000150","2021/12/14 7:00:00",True,"1", -299001170,999,"《あの日、金木犀の下で》イーディス",5,2,1600,20000,100,1,0,0,0,"s299001170",,65,210,68,204,71,201,73,198,75,196,77,194,10,20,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"メモラブル・ペタルス","メモラブル・〇〇ペタルス","自パーティのSTRを110%上昇させる。覚醒で効果時間が延長。【アリスのみ効果が3倍】","【効果上昇・アリス】STR上昇","甘く香る花の香りに包まれると、
あたたかくて、どこか切ない。
けれど、あなたが隣にいれば、
その切なさも心地よくて。","slog_299001170","slog_299001170","Chara_icon/299001170","Chara_icon/299001170","2021/12/14 7:00:00",True,"1", -299001180,999,"《永遠の美》アドミニストレータ",5,2,1600,20000,100,1,0,0,0,"s299001180",,15,65,17,62,19,59,21,56,23,53,25,50,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"ビューティー・オーラ","ビューティー・〇オーラ","自パーティの攻撃に
魅了を付与し、
INTを200%上昇させる。
覚醒で効果時間が延長。","自パーティの攻撃に魅了付与+INT上昇","美しい肢体が水面を揺らすたびに
思わず感嘆の息が漏れる。優美な
光景に頭がぼうっとして、薔薇の
香りに誘われるまま傍へ寄った。","slog_299001180","slog_299001180","Chara_icon/299001180","Chara_icon/299001180","2021/12/14 7:00:00",True,"1", -209000280,999,"《寧静の眠り》ユウキ",5,1,1600,20000,100,1,0,0,0,"s209000280",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・ロザリウム","スリープウィズ・ロザリウム","戦闘不能状態のメンバーを2人復帰させ、20秒間自パーティのSTRを50%上昇&闇属性付与。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+自パーティのSTR上昇&闇属性付与","もしも兄がいたらと夢想する。
あなたのように、どんな状況でも
後ろから守ってくれる絶対的な
安心を与えてくれるのだろうか。","slog_209000280","slog_209000280","Chara_icon/209000280","Chara_icon/209000280","2022/01/25 7:00:00",True,"1", -211000120,999,"《高まる体温》アリス",5,1,1600,20000,100,1,0,0,0,"s211000120",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・オスマンサス","スリープウィズ・オスマンサス","戦闘不能状態のメンバーを1人復帰させ、45秒間グリードからの注目を集める。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー1人復帰+グリードからの注目","何度あなたの隣で寝ようとも鼓動
は早く頬も熱くなるばかりで、平
常心ではいられない。私と同じく
らい、私を意識してほしいのに。","slog_211000120","slog_211000120","Chara_icon/211000120","Chara_icon/211000120","2022/01/25 7:00:00",True,"1", -212000070,999,"《分け合う心》ユージオ",5,1,1600,20000,100,1,0,0,0,"s212000070",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・ブルーローズ","スリープウィズ・ブルーローズ","戦闘不能状態のメンバーを1人復帰させ、周囲の敵にダメージと氷咲を付与する。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー1人復帰+周囲の敵にダメージ&氷咲付与","二人で掛けるには小さい布団を
寒さを凌ぐため、あなたと分け
合う。こうして枕を並べれば、
いつか隣に並べる気がして。","slog_212000070","slog_212000070","Chara_icon/212000070","Chara_icon/212000070","2022/01/25 7:00:00",True,"1", -215000130,999,"《癒しの懐抱》ストレア",5,1,1600,20000,100,1,0,0,0,"s215000130",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・ライラック","スリープウィズ・ライラック","戦闘不能状態のメンバーを2人復帰させ、20秒間自パーティのSTRを50%上昇&土属性付与。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+自パーティのSTR上昇&土属性付与","彼女のハグはあなたを癒すため。
そして、あなたから元気を貰う
ため。ずっと大好きだから、
一生そばにいてね。","slog_215000130","slog_215000130","Chara_icon/215000130","Chara_icon/215000130","2022/01/25 7:00:00",True,"1", -216000130,999,"《臥房の什宝》フィリア",5,1,1600,20000,100,1,0,0,0,"s216000130",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・シーカー","スリープウィズ・シーカー","戦闘不能状態のメンバーを2人復帰させ、7秒間レコードメダルとグリードの位置をサーチする。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+レコードメダル&グリードサーチ","トレジャーハンターの彼女が見つ
けたとびっきりのお宝は、孤独な
未来を変えてくれたあなた。これ
からは、一緒に未来を作りたい。","slog_216000130","slog_216000130","Chara_icon/216000130","Chara_icon/216000130","2022/01/25 7:00:00",True,"1", -299001130,999,"《夢の浮橋》イツキ",5,1,1600,20000,100,1,0,0,0,"s299001130",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・シリウス","スリープウィズ・シリウス","戦闘不能状態のメンバーを2人復帰させ、20秒間獲得リーグポイントを15%増加させる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+獲得リーグポイント上昇","いつも見る夢には弾かれたように
自分の姿が見当たらない。けれど
あなたを撃つ自身が夢に現れて。
夢のままであれと、神に祈った。","slog_299001130","slog_299001130","Chara_icon/299001130","Chara_icon/299001130","2022/01/25 7:00:00",True,"1", -299001140,999,"《浮雲の温度》ツェリスカ",5,1,1600,20000,100,1,0,0,0,"s299001140",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・ピーコック","スリープウィズ・ピーコック","戦闘不能状態のメンバーを2人復帰させ、20秒間ラストアタックボーナスを25%増加させる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+ラストアタックで得られるボーナスポイント量上昇","現実でも仮想世界でも、人の
温もりを感じながら就く眠りは
心地よい。そう感じるのは、隣
にいるのがあなただからだ。","slog_299001140","slog_299001140","Chara_icon/299001140","Chara_icon/299001140","2022/01/25 7:00:00",True,"1", -299001150,999,"《色葉散らず》クレハ",5,1,1600,20000,100,1,0,0,0,"s299001150",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・シロッコ","スリープウィズ・シロッコ","戦闘不能状態のメンバーを2人復帰させ、20秒間ファーストアタックボーナスを20%増加。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+ファーストアタックで得られるボーナスポイント量上昇","幼馴染として知る過去と今の彼女
が変わっていても、根底にある
優しさは同じだと感じたから、
足蹴にされても隣で眠りたい。","slog_299001150","slog_299001150","Chara_icon/299001150","Chara_icon/299001150","2022/01/25 7:00:00",True,"1", -291000060,999,"《絆の軌跡》リコ",5,2,1600,20000,100,1,0,0,34,"s291000060",,10,65,11,62,12,59,13,56,14,53,15,50,2,4,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"リコメンド・マイハート","リコメンド・〇〇マイハート","自パーティのINTが300%上昇。覚醒で効果時間が延長。【パッシブ】パニッシャー出現率が上昇。重複で効果増。","自パーティのINT上昇","あなたと歩んだ3年間。それは、
あなたや友と育んだ愛の軌跡。
これからもあなたの側で、
その笑顔のお役に立てたら。","slog_291000060","slog_291000061","Chara_icon/291000060","Chara_icon/291000061","2022/03/08 7:00:00",True,"1", -297000150,999,"《奇跡の出会い》オールキャスト",5,3,1600,20000,100,1,0,0,0,"s297000150",,1,380,1,360,2,340,2,320,3,310,3,300,1,2,"ドロップ数","{0}個","skill_3_2line_moji86","un_2line_moji86",,"サンキューパーティー","サンキュー〇〇〇パーティー","3秒間だけグリードを9体召喚する。覚醒でスキルポーションのドロップ数が増加。","グリードを召喚","この仮想世界で、三度四季が
巡った。仲間と重ねた春夏秋冬を
彼らは決して忘れない。どうか
あなたの心にも残りますように。","slog_297000150","slog_297000151","Chara_icon/297000150","Chara_icon/297000151","2022/03/08 7:00:00",True,"1", -209000320,999,"《幻想の桜姫》ユウキ",5,3,1600,20000,100,1,0,0,0,"s209000320",,90,180,96,176,102,172,108,168,114,164,120,160,10,20,"効果時間","{0}秒","skill_3_2line_moji86","un_2line_moji86",,"サクラ・フロントライン","サクラ・〇〇〇〇フロントライン","自パーティに敵の注目を集め、
攻撃を受けるまで一定時間毎に
リーグポイントを獲得する状態に
なる。覚醒で効果時間が延長。","敵の注目を集める+攻撃を受けるまでリーグポイント自動獲得状態","彼女が剣を一振りすれば、桜の花
が舞い踊る。その美しい剣技は、
桜のように讃えられ、多くの人の
心の中で咲き続けていく。","slog_209000320","slog_209000320","Chara_icon/209000320","Chara_icon/209000320","2022/03/08 7:00:00",True,"1", -202000440,999,"《桜花女王》アスナ",4,3,800,15000,100,1,0,0,0,"s202000440",,40,165,50,151,55,144,60,137,65,130,70,123,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"ブロッサム・スピアーズ","ブロッサム・〇〇スピアーズ","一定時間、前方に突属性の
追尾弾を連射して敵にダメージを
与える。覚醒で攻撃力が上昇。","一定時間、前方に突属性の追尾弾を連射","妖精たちの女王が、麗らかな春の
訪れを告げる。その姿は花明かり
のように華やかに輝いて、人々の
心を照らすだろう。","slog_202000440","slog_202000440","Chara_icon/202000440","Chara_icon/202000440","2022/03/08 7:00:00",True,"1", -203000350,999,"《桜光の姫騎士》リーファ",4,3,800,15000,100,1,0,0,0,"s203000350",,40,165,50,151,55,144,60,137,65,130,70,123,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"ブロッサム・スラッシャーズ","ブロッサム・〇〇スラッシャーズ","乱れ飛ぶ衝撃波によって周囲の敵に斬属性のダメージを与える。覚醒で攻撃力が上昇。","斬属性の衝撃波による範囲攻撃","妖精のお花見は場所を選ばない。
突然の花嵐に見舞われたら、
それは空から桜を楽しむ妖精の
羽ばたきのせいかもしれない。","slog_203000350","slog_203000350","Chara_icon/203000350","Chara_icon/203000350","2022/03/08 7:00:00",True,"1", -204000370,999,"《桜麗の弓士》シノン",4,3,800,15000,100,1,0,0,0,"s204000370",,40,165,50,151,55,144,60,137,65,130,70,123,5,10,"攻撃力上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"ブロッサム・アローズ","ブロッサム・〇〇アローズ","乱れ飛ぶ矢によって周囲の敵に突属性のダメージを与える。覚醒で攻撃力が上昇。","突属性の矢による範囲攻撃","彼女が放つ一矢が、あなたに桜の
便りを届ける。外に出てみれば、
昨日より色づいた景色が広がって
暖かい風が頬を撫でた。","slog_204000370","slog_204000370","Chara_icon/204000370","Chara_icon/204000370","2022/03/08 7:00:00",True,"1", -202000390,999,"《目配せのしとね》アスナ",5,1,1600,20000,100,1,0,0,0,"s202000390",,5,95,10,85,15,80,20,75,25,70,30,65,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・ベテルギウス","スリープウィズ・ベテルギウス","戦闘不能状態のメンバーを2人復帰させ、15秒間獲得リーグポイントを30%増加させる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+獲得リーグポイント上昇","眠りにつく前は、二人きりの
穏やかな時間。あなたを独り占め
できる今だけは、その闇のような
瞳に私だけを映してほしくて。","slog_202000390","slog_202000390","Chara_icon/202000390","Chara_icon/202000390","2022/04/26 7:00:00",True,"1", -203000310,999,"《不壊の縁故》リーファ",5,1,1600,20000,100,1,0,0,0,"s203000310",,5,95,10,85,15,80,20,75,25,70,30,65,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・シルフ","スリープウィズ・シルフ","戦闘不能状態のメンバーを2人復帰させ、15秒間自パーティの移動速度を上昇させる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+移動速度上昇","どれだけ時が経とうとも、兄妹の
絆は不変のまま。けれど、枕を
共にするほどの距離が、いつまで
も続かないと知っているから。","slog_203000310","slog_203000310","Chara_icon/203000310","Chara_icon/203000310","2022/04/26 7:00:00",True,"1", -204000330,999,"《解きほぐす内心》シノン",5,1,1600,20000,100,1,0,0,0,"s204000330",,5,95,10,85,15,80,20,75,25,70,30,65,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・ティアマト","スリープウィズ・ティアマト","戦闘不能状態のメンバーを2人復帰させ、15秒間ラストアタックボーナスを50%増加させる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+ラストアタックで得られるボーナスポイント量上昇","いつも誰かを想って立ち上がる
あなたの、心ごとほぐせたら。
せめて夢の中では憂うことのない
ように、掴む手に力を込めた。","slog_204000330","slog_204000330","Chara_icon/204000330","Chara_icon/204000330","2022/04/26 7:00:00",True,"1", -205000240,999,"《安眠の廉》リズベット",5,1,1600,20000,100,1,0,0,0,"s205000240",,5,95,10,85,15,80,20,75,25,70,30,65,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・ヴァルキリー","スリープウィズ・ヴァルキリー","戦闘不能状態のメンバーを2人復帰させ、45秒間自パーティがよろけ・ダウン無効状態になる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+よろけ・ダウン無効","培った鍛冶の経験を寝具に活かし
寝心地の良さを追求する彼女。
けれど、どんな眠りも信頼する
彼の隣には敵わないと思えて。","slog_205000240","slog_205000240","Chara_icon/205000240","Chara_icon/205000240","2022/03/08 7:00:00",True,"1", -206000260,999,"《守られる吉夢》シリカ",5,1,1600,20000,100,1,0,0,0,"s206000260",,5,95,10,85,15,80,20,75,25,70,30,65,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・シュトルム","スリープウィズ・シュトルム","戦闘不能状態のメンバーを2人復帰させ、15秒間ファーストアタックボーナスを40%増加。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+ファーストアタックで得られるボーナスポイント量上昇","夢の中、弱い自分の影に囚われ
る。もう少し、一人で真っ直ぐ
立てるようになるまでは、彼の
側で優しい夢を見たい。","slog_206000260","slog_206000260","Chara_icon/206000260","Chara_icon/206000260","2022/03/08 7:00:00",True,"1", -209000290,999,"《東雲の契り》ユウキ",5,1,1600,20000,100,1,0,0,0,"s209000290",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・クサナギ","スリープウィズ・クサナギ","戦闘不能状態のメンバーを1人復帰させ、30秒間自パーティのSTRを50%上昇&闇属性付与。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー1人復帰+自パーティのSTR上昇&闇属性付与","体を休めることも戦法のうち。
全力で仮想世界を駆けた今日を
思いながら心地よい眠りにつく。
また明日の約束を果たすために。","slog_209000290","slog_209000290","Chara_icon/209000290","Chara_icon/209000290","2022/04/26 7:00:00",True,"1", -210000040,999,"《密かな処方》アルゴ",5,1,1600,20000,100,1,0,0,0,"s210000040",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・ハニーコム","スリープウィズ・ハニーコム","戦闘不能状態のメンバーを1人復帰させ、30秒間敵に感知されなくなる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー1人復帰+ハイディング","怪しげな薬の効能を確かめるべく
ひと息で全て呷ったものの、結果
は分からずじまい。本当の効能は
彼女の心の中に秘められたまま。","slog_210000040","slog_210000040","Chara_icon/210000040","Chara_icon/210000040","2022/04/26 7:00:00",True,"1", -211000130,999,"《二人寝は繁く》アリス",5,1,1600,20000,100,1,0,0,0,"s211000130",,5,95,10,85,15,80,20,75,25,70,30,65,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・ロイヤルガード","スリープウィズ・ロイヤルガード","戦闘不能状態のメンバーを2人復帰させ、45秒間グリードからの注目を集める。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+グリードからの注目","己の使命を果たすため、元の世界
の手がかりをあなたと探す日々。
けれど、帰ってしまえば、
独り寝は難しい気がして。","slog_211000130","slog_211000130","Chara_icon/211000130","Chara_icon/211000130","2022/04/26 7:00:00",True,"1", -212000080,999,"《追懐の夜語り》ユージオ",5,1,1600,20000,100,1,0,0,0,"s212000080",,5,95,10,85,15,80,20,75,25,70,30,65,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・ジェントル","スリープウィズ・ジェントル","戦闘不能状態のメンバーを2人復帰させ、周囲の敵にダメージと氷咲を付与する。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー2人復帰+周囲の敵にダメージ&氷咲付与","一日の出来事を君と振り返る。
なんてことない時間のはずなのに
胸が締め付けられるほど、
どこか懐かしさがあって。","slog_212000080","slog_212000080","Chara_icon/212000080","Chara_icon/212000080","2022/04/26 7:00:00",True,"1", -215000140,999,"《開かれる愁眉》ストレア",5,1,1600,20000,100,1,0,0,0,"s215000140",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・バンカーコメット","スリープウィズ・バンカーコメット","戦闘不能状態のメンバーを1人復帰させ、30秒間自パーティのSTRを50%上昇&土属性付与。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー1人復帰+自パーティのSTR上昇&土属性付与","キミのことを癒してあげたい、
キミのことを、もっと知りたい。
この気持ちはプログラムを超えた
純粋な想いだと知ってほしい。","slog_215000140","slog_215000140","Chara_icon/215000140","Chara_icon/215000140","2022/03/08 7:00:00",True,"1", -216000140,999,"《安らぎの香》フィリア",5,1,1600,20000,100,1,0,0,0,"s216000140",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・クワイエット","スリープウィズ・クワイエット","戦闘不能状態のメンバーを1人復帰させ、10秒間レコードメダルとグリードの位置をサーチする。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー1人復帰+レコードメダル&グリードサーチ","疲れを癒すアロマの香りが部屋に
漂う。あなたが安心して眠れる
のは、この優しい香りのおかげ?
それとも、わたしの隣だから…?","slog_216000140","slog_216000140","Chara_icon/216000140","Chara_icon/216000140","2022/03/08 7:00:00",True,"1", -217000090,999,"《聞き澄ます愛吟》セブン",5,1,1600,20000,100,1,0,0,0,"s217000090",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・ライト","スリープウィズ・ライト","戦闘不能状態のメンバーを1人復帰させ、22秒間周囲の敵のINTを大幅に低下させる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー1人復帰+周囲の敵のINTを低下","家族みたいな大切なあなただから
聞かせる特別な子守歌。
いつかあなたの声で、その歌を
聞いて眠りにつきたい。","slog_217000090","slog_217000090","Chara_icon/217000090","Chara_icon/217000090","2022/03/08 7:00:00",True,"1", -218000100,999,"《傾慕の子守歌》レイン",5,1,1600,20000,100,1,0,0,0,"s218000100",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・スパイラル","スリープウィズ・スパイラル","戦闘不能状態のメンバーを1人復帰させ、30秒間攻撃時に50%の確率で追加攻撃を発生させる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー1人復帰+追加攻撃","あなたが元気になりますように。
そんな想いを込めて作った曲を
眠れないあなたのために歌う。
夢の中でもあなたに会えたらと。","slog_218000100","slog_218000100","Chara_icon/218000100","Chara_icon/218000100","2022/03/08 7:00:00",True,"1", -299001120,999,"《受ける薫陶》プレミア",5,1,1600,20000,100,1,0,0,0,"s299001120",,5,85,10,75,15,70,20,65,25,60,30,55,5,10,"耐久値回復量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"スリープウィズ・ナイトスカイ","スリープウィズ・ナイトスカイ","戦闘不能状態のメンバーを1人復帰させ、55秒間耐久値が1%未満には減らなくなる。覚醒で復帰時の回復量が上昇。","戦闘不能状態のメンバー1人復帰+耐久値が1%未満にならなくなる","どうしてあなたと一緒にいると、
頬の温度が上がるのか。不思議
だらけのこの世界を、これから
一緒に知っていきたい。","slog_299001120","slog_299001120","Chara_icon/299001120","Chara_icon/299001120","2022/03/08 7:00:00",True,"1", -291000070,999,"《雅懐の一葉》リコ",5,2,1600,20000,100,1,0,0,0,"s291000070",,500,60,600,56,650,52,700,48,750,44,800,40,100,199,"STR上昇量","{0}%","skill_3_2line_moji86","un_2line_moji86",,"オーバードライブ・オーダー","オーバードライブ〇〇・オーダー","自パーティのSTRを
5秒間上昇させる。
覚醒でSTR上昇量が増加。","自パーティのSTR上昇","あなたと出会い、言葉を交わし、
彼女は生まれた。胸の内で熱を
持ち続けるこの気持ちを、人は
愛と呼ぶのかもしれない。","slog_291000070","slog_291000071","Chara_icon/291000070","Chara_icon/291000071","2022/03/08 7:00:00",True,"1", diff --git a/titles/sao/data/Title.csv b/titles/sao/data/Title.csv deleted file mode 100644 index 73b4dfd..0000000 --- a/titles/sao/data/Title.csv +++ /dev/null @@ -1,269 +0,0 @@ -TitleId,DisplayName,Requirement,Value1,Value2,Rank,ImageFilePath, -1,"Unknown調査員",0,0,0,1,"DEG_1", -10001,"ビギニング",1,10,0,2,"DEG_10001", -10002,"ミドル・ホルダー",1,50,0,3,"DEG_10002", -10003,"ハイエスト・ホルダー",1,100,0,4,"DEG_10003", -10004,"アッパー・ランカー",1,200,0,5,"DEG_10004", -10005,"グラインド・マスター",1,300,0,5,"DEG_10005", -20001,"謎の少女;リコ",2,10018,0,3,"DEG_20001", -20002,"記憶の欠片",2,20014,0,3,"DEG_20002", -20004,"--error--error--",15,30017,0,3,"DEG_20004", -20005,"Master of Re:co",15,40023,0,3,"DEG_20005", -20006,"新しき友人",15,50019,0,4,"DEG_20006", -30001,"挑戦者",3,10,0,3,"DEG_30001", -30002,"下剋上",3,50,0,3,"DEG_30002", -30003,"達成者",3,100,0,3,"DEG_30003", -40001,"ブラッキー",4,1,50,3,"DEG_40001", -40002,"黒の剣士",4,1,100,5,"DEG_40002", -40003,"バーサクヒーラー",4,2,50,3,"DEG_40003", -40004,"閃光",4,2,100,5,"DEG_40004", -40005,"妖精剣士",4,3,50,3,"DEG_40005", -40006,"スピードホリック",4,3,100,5,"DEG_40006", -40007,"クールビューティ",4,4,50,3,"DEG_40007", -40008,"冥界の女神",4,4,100,5,"DEG_40008", -40009,"戦う鍛冶屋さん",4,5,50,3,"DEG_40009", -40010,"マスターメイサー",4,5,100,5,"DEG_40010", -40011,"中層のアイドル",4,6,50,3,"DEG_40011", -40012,"ドラゴンテイマー",4,6,100,5,"DEG_40012", -40013,"武士道を歩む者",4,7,50,3,"DEG_40013", -40014,"風林火山",4,7,100,5,"DEG_40014", -40015,"商人プレイヤー",4,8,50,3,"DEG_40015", -40016,"剛腕マスター",4,8,100,5,"DEG_40016", -40017,"スリーピング・ナイツ",4,9,50,3,"DEG_40017", -40018,"絶剣",4,9,100,5,"DEG_40018", -40019,"影の功労者",4,10,50,3,"DEG_40019", -40020,"鼠の情報屋",4,10,100,5,"DEG_40020", -40021,"整合騎士 第三十位",4,11,50,3,"DEG_40021", -40022,"金木犀の騎士",4,11,100,5,"DEG_40022", -40023,"上級修剣士",4,12,50,3,"DEG_40023", -40024,"青薔薇の騎士",4,12,100,5,"DEG_40024", -40025,"謎多き追跡者",4,18,50,3,"DEG_40025", -40026,"虹架ける歌声",4,18,100,5,"DEG_40026", -50001,"コレクション初級",5,10,0,2,"DEG_50001", -50002,"リソース収集家",5,50,0,3,"DEG_50002", -50003,"お宝ハンティング",5,100,0,4,"DEG_50003", -60001,"モンスター記録者",6,10,0,2,"DEG_60001", -60002,"モンスター博士",6,50,0,3,"DEG_60002", -60003,"モンスター・マスター",6,100,0,4,"DEG_60003", -70001,"パワーアップ初級",7,10,0,2,"DEG_70001", -70002,"リソース・トレーナー",7,50,0,3,"DEG_70002", -70003,"錬金術師",7,100,0,4,"DEG_70003", -80001,"調査報告・初級",8,10,0,2,"DEG_80001", -80002,"ベテラン調査員",8,50,0,3,"DEG_80002", -80003,"プロフェッショナル",8,100,0,4,"DEG_80003", -90001,"掃除屋",9,100,0,2,"DEG_90001", -90002,"ザ・スローター",9,500,0,3,"DEG_90002", -90003,"バウンティハンター",9,1000,0,4,"DEG_90003", -100001,"千里の道も一歩から",10,10,0,2,"DEG_100001", -100002,"雨垂れ石を穿つ",10,20,0,3,"DEG_100002", -100003,"質実剛健",10,30,0,4,"DEG_100003", -110003,"日々精進",11,30,0,0,"DEG_110003", -120001,"ビーター",12,0,0,3,"DEG_120001", -130001,"蒼穹を駆ける英傑:銅",13,4600,0,3,"DEG_130001", -130002,"蒼穹を駆ける英傑:銀",13,9700,0,4,"DEG_130002", -130003,"蒼穹を駆ける英傑:金",13,21600,0,5,"DEG_130003", -130011,"万天に通ずる鼠:銅",13,8400,0,3,"DEG_130011", -130012,"万天に通ずる鼠:銀",13,31500,0,4,"DEG_130012", -130013,"万天に通ずる鼠:金",13,63000,0,5,"DEG_130013", -130021,"心通う邂逅:銅",13,4100,0,3,"DEG_130021", -130022,"心通う邂逅:銀",13,47800,0,4,"DEG_130022", -130023,"心通う邂逅:金",13,143300,0,5,"DEG_130023", -130031,"かの剣技の継承者:銅",13,4100,0,3,"DEG_130031", -130032,"かの剣技の継承者:銀",13,47800,0,4,"DEG_130032", -130033,"かの剣技の継承者:金",13,143300,0,5,"DEG_130033", -130041,"咲き誇る技巧:銅",13,4100,0,3,"DEG_130041", -130042,"咲き誇る技巧:銀",13,52000,0,4,"DEG_130042", -130043,"咲き誇る技巧:金",13,143300,0,5,"DEG_130043", -130051,"まごころショコラティエ(銅)",13,1800,0,3,"DEG_130051", -130052,"まごころショコラティエ(銀)",13,58900,0,4,"DEG_130052", -130053,"まごころショコラティエ(金)",13,165400,0,5,"DEG_130053", -130061,"異界の女神(銅)",13,1800,0,3,"DEG_130061", -130062,"異界の女神(銀)",13,59300,0,4,"DEG_130062", -130063,"異界の女神(金)",13,166500,0,5,"DEG_130063", -130071,"クロス・イメージ(銅)",13,1800,0,3,"DEG_130071", -130072,"クロス・イメージ(銀)",13,59800,0,4,"DEG_130072", -130073,"クロス・イメージ(金)",13,167700,0,5,"DEG_130073", -130081,"灼熱!アタックガール(銅)",13,1800,0,3,"DEG_130081", -130082,"灼熱!アタックガール(銀)",13,59800,0,4,"DEG_130082", -130083,"灼熱!アタックガール(金)",13,167700,0,5,"DEG_130083", -130091,"0と1の狭間で(銅)",13,1800,0,3,"DEG_130091", -130092,"0と1の狭間で(銀)",13,59800,0,4,"DEG_130092", -130093,"0と1の狭間で(金)",13,167700,0,5,"DEG_130093", -130101,"変わらないもの(銅)",13,1800,0,3,"DEG_130101", -130102,"変わらないもの(銀)",13,59800,0,4,"DEG_130102", -130103,"変わらないもの(金)",13,167700,0,5,"DEG_130103", -130111,"サンタさん大作戦!(銅)",13,1800,0,3,"DEG_130111", -130112,"サンタさん大作戦!(銀)",13,59800,0,4,"DEG_130112", -130113,"サンタさん大作戦!(金)",13,167700,0,5,"DEG_130113", -130121,"いとしのショコラティエ(銅)",13,1800,0,3,"DEG_130121", -130122,"いとしのショコラティエ(銀)",13,59800,0,4,"DEG_130122", -130123,"いとしのショコラティエ(金)",13,167700,0,5,"DEG_130123", -130131,"ふたとせ廻りて(銅)",13,1800,0,3,"DEG_130131", -130132,"ふたとせ廻りて(銀)",13,59800,0,4,"DEG_130132", -130133,"ふたとせ廻りて(金)",13,167700,0,5,"DEG_130133", -130141,"私の現実(銅)",13,1800,0,3,"DEG_130141", -130142,"私の現実(銀)",13,59800,0,4,"DEG_130142", -130143,"私の現実(金)",13,167700,0,5,"DEG_130143", -130151,"にじさす未来(銅)",13,1800,0,3,"DEG_130151", -130152,"にじさす未来(銀)",13,59800,0,4,"DEG_130152", -130153,"にじさす未来(金)",13,167700,0,5,"DEG_130153", -130161,"大海の中の絆(銅)",13,1800,0,3,"DEG_130161", -130162,"大海の中の絆(銀)",13,59800,0,4,"DEG_130162", -130163,"大海の中の絆(金)",13,167700,0,5,"DEG_130163", -130171,"夢の逸楽(銅)",13,1800,0,3,"DEG_130171", -130172,"夢の逸楽(銀)",13,59800,0,4,"DEG_130172", -130173,"夢の逸楽(金)",13,167700,0,5,"DEG_130173", -130181,"忍ぶ外套(銅)",13,1800,0,3,"DEG_130181", -130182,"忍ぶ外套(銀)",13,59800,0,4,"DEG_130182", -130183,"忍ぶ外套(金)",13,167700,0,5,"DEG_130183", -130191,"駈走る雪原(銅)",13,1800,0,3,"DEG_130191", -130192,"駈走る雪原(銀)",13,59800,0,4,"DEG_130192", -130193,"駈走る雪原(金)",13,167700,0,5,"DEG_130193", -130201,"友愛はほろ苦く(銅)",13,1800,0,3,"DEG_130201", -130202,"友愛はほろ苦く(銀)",13,59800,0,4,"DEG_130202", -130203,"友愛はほろ苦く(金)",13,167700,0,5,"DEG_130203", -130211,"絆の軌跡(銅)",13,1800,0,3,"DEG_130211", -130212,"絆の軌跡(銀)",13,59800,0,4,"DEG_130212", -130213,"絆の軌跡(金)",13,167700,0,5,"DEG_130213", -130221,"一杯の息抜き(銅)",13,1800,0,3,"DEG_130221", -130222,"一杯の息抜き(銀)",13,59800,0,4,"DEG_130222", -130223,"一杯の息抜き(金)",13,167700,0,5,"DEG_130223", -140001,"スタンダードリーグ",14,2,5,1,"DEG_140001", -140002,"クラスフィフス",14,2,50,1,"DEG_140002", -140003,"クラスフォース",14,2,80,2,"DEG_140003", -140004,"クラスサード",14,2,130,3,"DEG_140004", -140005,"クラスセカンド",14,2,200,4,"DEG_140005", -140006,"クラスファースト",14,2,300,5,"DEG_140006", -140011,"スタンダードリーグ",14,3,5,1,"DEG_140011", -140012,"クラスフィフス",14,3,50,1,"DEG_140012", -140013,"クラスフォース",14,3,80,2,"DEG_140013", -140014,"クラスサード",14,3,130,3,"DEG_140014", -140015,"クラスセカンド",14,3,200,4,"DEG_140015", -140016,"クラスファースト",14,3,300,5,"DEG_140016", -140021,"スタンダードリーグ",14,4,5,1,"DEG_140021", -140022,"クラスフィフス",14,4,30,1,"DEG_140022", -140023,"クラスフォース",14,4,40,2,"DEG_140023", -140024,"クラスサード",14,4,55,3,"DEG_140024", -140025,"クラスセカンド",14,4,75,4,"DEG_140025", -140026,"クラスファースト",14,4,100,5,"DEG_140026", -140031,"スタンダードリーグ",14,5,5,1,"DEG_140031", -140032,"クラスフィフス",14,5,30,1,"DEG_140032", -140033,"クラスフォース",14,5,40,2,"DEG_140033", -140034,"クラスサード",14,5,55,3,"DEG_140034", -140035,"クラスセカンド",14,5,75,4,"DEG_140035", -140036,"クラスファースト",14,5,100,5,"DEG_140036", -140041,"スタンダードリーグ",14,6,5,1,"DEG_140041", -140042,"クラスフィフス",14,6,30,1,"DEG_140042", -140043,"クラスフォース",14,6,40,2,"DEG_140043", -140044,"クラスサード",14,6,55,3,"DEG_140044", -140045,"クラスセカンド",14,6,75,4,"DEG_140045", -140046,"クラスファースト",14,6,100,5,"DEG_140046", -150041,"スタンダードリーグ",14,7,5,1,"DEG_150041", -150042,"クラスフィフス",14,7,30,1,"DEG_150042", -150043,"クラスフォース",14,7,40,2,"DEG_150043", -150044,"クラスサード",14,7,55,3,"DEG_150044", -150045,"クラスセカンド",14,7,75,4,"DEG_150045", -150046,"クラスファースト",14,7,100,5,"DEG_150046", -140061,"スタンダードリーグ",14,8,5,1,"DEG_140061", -140062,"クラスフィフス",14,8,30,1,"DEG_140062", -140063,"クラスフォース",14,8,40,2,"DEG_140063", -140064,"クラスサード",14,8,55,3,"DEG_140064", -140065,"クラスセカンド",14,8,75,4,"DEG_140065", -140066,"クラスファースト",14,8,100,5,"DEG_140066", -140071,"スタンダードリーグ",14,9,5,1,"DEG_140071", -140072,"クラスフィフス",14,9,30,1,"DEG_140072", -140073,"クラスフォース",14,9,40,2,"DEG_140073", -140074,"クラスサード",14,9,55,3,"DEG_140074", -140075,"クラスセカンド",14,9,75,4,"DEG_140075", -140076,"クラスファースト",14,9,100,5,"DEG_140076", -140081,"スタンダードリーグ",14,10,5,1,"DEG_140081", -140082,"クラスフィフス",14,10,30,1,"DEG_140082", -140083,"クラスフォース",14,10,40,2,"DEG_140083", -140084,"クラスサード",14,10,55,3,"DEG_140084", -140085,"クラスセカンド",14,10,75,4,"DEG_140085", -140086,"クラスファースト",14,10,100,5,"DEG_140086", -140091,"スタンダードリーグ",14,11,5,1,"DEG_140091", -140092,"クラスフィフス",14,11,30,1,"DEG_140092", -140093,"クラスフォース",14,11,40,2,"DEG_140093", -140094,"クラスサード",14,11,55,3,"DEG_140094", -140095,"クラスセカンド",14,11,75,4,"DEG_140095", -140096,"クラスファースト",14,11,100,5,"DEG_140096", -140101,"スタンダードリーグ",14,12,5,1,"DEG_140101", -140102,"クラスフィフス",14,12,30,1,"DEG_140102", -140103,"クラスフォース",14,12,40,2,"DEG_140103", -140104,"クラスサード",14,12,55,3,"DEG_140104", -140105,"クラスセカンド",14,12,75,4,"DEG_140105", -140106,"クラスファースト",14,12,100,5,"DEG_140106", -140111,"スタンダードリーグ",14,13,5,1,"DEG_140111", -140112,"クラスフィフス",14,13,30,1,"DEG_140112", -140113,"クラスフォース",14,13,40,2,"DEG_140113", -140114,"クラスサード",14,13,55,3,"DEG_140114", -140115,"クラスセカンド",14,13,75,4,"DEG_140115", -140116,"クラスファースト",14,13,100,5,"DEG_140116", -140121,"スタンダードリーグ",14,14,5,1,"DEG_140121", -140122,"クラスフィフス",14,14,30,1,"DEG_140122", -140123,"クラスフォース",14,14,40,2,"DEG_140123", -140124,"クラスサード",14,14,55,3,"DEG_140124", -140125,"クラスセカンド",14,14,75,4,"DEG_140125", -140126,"クラスファースト",14,14,100,5,"DEG_140126", -140131,"スタンダードリーグ",14,15,5,1,"DEG_140131", -140132,"クラスフィフス",14,15,30,1,"DEG_140132", -140133,"クラスフォース",14,15,40,2,"DEG_140133", -140134,"クラスサード",14,15,55,3,"DEG_140134", -140135,"クラスセカンド",14,15,75,4,"DEG_140135", -140136,"クラスファースト",14,15,100,5,"DEG_140136", -140141,"スタンダードリーグ",14,16,5,1,"DEG_140141", -140142,"クラスフィフス",14,16,30,1,"DEG_140142", -140143,"クラスフォース",14,16,40,2,"DEG_140143", -140144,"クラスサード",14,16,55,3,"DEG_140144", -140145,"クラスセカンド",14,16,75,4,"DEG_140145", -140146,"クラスファースト",14,16,100,5,"DEG_140146", -140151,"スタンダードリーグ",14,17,5,1,"DEG_140151", -140152,"クラスフィフス",14,17,30,1,"DEG_140152", -140153,"クラスフォース",14,17,40,2,"DEG_140153", -140154,"クラスサード",14,17,55,3,"DEG_140154", -140155,"クラスセカンド",14,17,75,4,"DEG_140155", -140156,"クラスファースト",14,17,100,5,"DEG_140156", -160001,"月間称号《物思い耽る午後》",99,0,0,3,"DEG_160001", -160002,"月間称号《物思い耽る午後》 覚醒",99,0,0,5,"DEG_160002", -160003,"夏の特別称号《潮風の悪戯》",99,0,0,3,"DEG_160003", -160004,"夏の特別称号《潮風の悪戯》 覚醒",99,0,0,5,"DEG_160004", -160005,"月間称号《悪戯な視線》",99,0,0,3,"DEG_160005", -160006,"月間称号《悪戯な視線》 覚醒",99,0,0,5,"DEG_160006", -160007,"月間称号《花舞う昼下がり》",99,0,0,3,"DEG_160007", -160008,"月間称号《花舞う昼下がり》 覚醒",99,0,0,5,"DEG_160008", -160009,"月間称号《おやつ・タイム!》",99,0,0,3,"DEG_160009", -160010,"月間称号《おやつ・タイム!》 覚醒",99,0,0,5,"DEG_160010", -160011,"月間称号《妖精の戯れ》",99,0,0,3,"DEG_160011", -160012,"月間称号《妖精の戯れ》 覚醒",99,0,0,5,"DEG_160012", -160013,"月間称号《穏やかな一時》",99,0,0,3,"DEG_160013", -160014,"月間称号《穏やかな一時》 覚醒",99,0,0,5,"DEG_160014", -160015,"特別称号《プレゼント大作戦!》",99,0,0,3,"DEG_160015", -160016,"特別称号《プレゼント大作戦!》 覚醒",99,0,0,5,"DEG_160016", -160017,"月間称号《さかさま花世界》",99,0,0,3,"DEG_160017", -160018,"月間称号《さかさま花世界》 覚醒",99,0,0,5,"DEG_160018", -160019,"月間称号《休日の談笑》",99,0,0,3,"DEG_160019", -160020,"月間称号《休日の談笑》 覚醒",99,0,0,5,"DEG_160020", -160023,"月間称号《晴天の君》",99,0,0,3,"DEG_160023", -160024,"月間称号《晴天の君》 覚醒",99,0,0,5,"DEG_160024", -160021,"月間称号《安らぎの我が家》",99,0,0,3,"DEG_160021", -160022,"月間称号《安らぎの我が家》 覚醒",99,0,0,5,"DEG_160022", -160025,"月間称号《幸せの青い鳥》",99,0,0,3,"DEG_160025", -160026,"月間称号《幸せの青い鳥》 覚醒",99,0,0,5,"DEG_160026", -160027,"月間称号《秘密の甘邸》",99,0,0,3,"DEG_160027", -160028,"月間称号《秘密の甘邸》 覚醒",99,0,0,5,"DEG_160028", -160029,"特別称号《ふたとせ集ひて》",99,0,0,3,"DEG_160029", -160030,"特別称号《ふたとせ集ひて》覚醒",99,0,0,5,"DEG_160030", -160031,"月間称号《一輪の幼花》",99,0,0,3,"DEG_160031", -160032,"月間称号《一輪の幼花》 覚醒",99,0,0,5,"DEG_160032", -160033,"月間称号《寛ぎの我が家》",99,0,0,3,"DEG_160033", -160034,"月間称号《寛ぎの我が家》 覚醒",99,0,0,5,"DEG_160034", -160035,"月間称号《小径の花環》",99,0,0,3,"DEG_160035", -160036,"月間称号《小径の花環》 覚醒",99,0,0,5,"DEG_160036", -160037,"月間称号《虹架かる団らん》",99,0,0,3,"DEG_160037", -160038,"月間称号《虹架かる団らん》 覚醒",99,0,0,5,"DEG_160038", -160039,"特別称号《贈る一番星》",99,0,0,3,"DEG_160039", -160040,"特別称号《贈る一番星》 覚醒",99,0,0,5,"DEG_160040", -160041,"月間称号《約束の花束》",99,0,0,3,"DEG_160041", -160042,"月間称号《約束の花束》 覚醒",99,0,0,5,"DEG_160042", -990001,"新世界の伝道師",99,4,0,5,"DEG_990001", diff --git a/titles/sao/database.py b/titles/sao/database.py index db866a4..b7026fb 100644 --- a/titles/sao/database.py +++ b/titles/sao/database.py @@ -1,5 +1,5 @@ -from core.config import CoreConfig from core.data import Data +from core.config import CoreConfig from .schema import * @@ -10,4 +10,4 @@ class SaoData(Data): self.item = SaoItemData(cfg, self.session) self.profile = SaoProfileData(cfg, self.session) - self.static = SaoStaticData(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 index e8039a8..4d38136 100644 --- a/titles/sao/handlers/__init__.py +++ b/titles/sao/handlers/__init__.py @@ -1,2 +1,2 @@ from .base import * -from .helpers import * +from .helpers import * \ No newline at end of file diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index 9b29ab0..4178640 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -1,13 +1,10 @@ -import csv import struct -from csv import * 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: @@ -21,13 +18,11 @@ class SaoRequestHeader: 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 @@ -37,44 +32,31 @@ class SaoResponseHeader: 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, - ) - + 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) + 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) @@ -84,73 +66,60 @@ class SaoGetMaintResponse(SaoBaseResponse): 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_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, - ) - ) + 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( @@ -159,87 +128,78 @@ class SaoMasterDataVersionCheckResponse(SaoBaseResponse): "data_version" / Int32ub, ) - resp_data = resp_struct.build( - dict( - result=self.result, - update_flag=self.update_flag, - data_version=self.data_version, - ) - ) + 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.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_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], - ) - ) + 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_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], - ) - ) + 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) @@ -266,10 +226,7 @@ class SaoGetAuthCardDataRequest(SaoBaseRequest): self.chip_id = chip_id[0] off += chip_id[1] - -class SaoGetAuthCardDataResponse( - SaoBaseResponse -): # GssSite.dll / GssSiteSystem / GameConnectProt / public class get_auth_card_data_R : GameConnect.GssProtocolBase +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) @@ -277,9 +234,9 @@ class SaoGetAuthCardDataResponse( self.unused_card_flag = "" self.first_play_flag = 0 self.tutorial_complete_flag = 1 - self.nick_name = profile_data["nick_name"] # nick_name field #4 - self.personal_id = str(profile_data["user"]) - + self.nick_name = 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( @@ -291,79 +248,71 @@ class SaoGetAuthCardDataResponse( "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)], + "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], - ) - ) + 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)" - ) + 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, - ) - ) + 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( @@ -372,29 +321,25 @@ class SaoGetQuestSceneMultiPlayPhotonServerResponse(SaoBaseResponse): "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], - ) - ) + 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 - + self.ticket_id = "9" #up to 18 + def make(self) -> bytes: # create a resp struct resp_struct = Struct( @@ -404,19 +349,16 @@ class SaoTicketResponse(SaoBaseResponse): "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], - ) - ) + 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) @@ -450,27 +392,26 @@ class SaoCommonLoginRequest(SaoBaseRequest): self.free_ticket_distribution_target_flag = decode_byte(data, off) off += BYTE_OFF - class SaoCommonLoginResponse(SaoBaseResponse): def __init__(self, cmd, profile_data) -> None: super().__init__(cmd) self.result = 1 - self.user_id = str(profile_data["user"]) + 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_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 @@ -478,95 +419,84 @@ class SaoCommonLoginResponse(SaoBaseResponse): "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, - ) - ) + 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 - + 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)], + "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 - ], - ) - ) + 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.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)], @@ -577,65 +507,56 @@ class SaoGetUserBasicDataResponse(SaoBaseResponse): "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_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))], + "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))], + "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)], + "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], - ) - ) + 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 = [] @@ -647,22 +568,23 @@ class SaoGetHeroLogUserDataListResponse(SaoBaseResponse): 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=",") + 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 + 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 < int(data[e + 1][1]): + + for e in range(0,len(data)): + if exp>=int(data[e][1]) and exp bytes: - # new stuff + #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 + "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 + "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 + "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, @@ -761,21 +671,15 @@ class SaoGetHeroLogUserDataListResponse(SaoBaseResponse): # 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), + "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=[], - ) - ) - ) + 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( @@ -811,32 +715,29 @@ class SaoGetHeroLogUserDataListResponse(SaoBaseResponse): 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 - ) + 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 = [] @@ -845,24 +746,25 @@ class SaoGetEquipmentUserDataListResponse(SaoBaseResponse): 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=",") + 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 + 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 < int(data[e + 1][1]): + + 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 + "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 + "awakening_stage" / Int16ub, #short + "awakening_exp" / Int32ub, #int "property1_property_id" / Int32ub, "property1_value1" / Int32ub, "property1_value2" / Int32ub, @@ -937,23 +836,15 @@ class SaoGetEquipmentUserDataListResponse(SaoBaseResponse): # 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 - ), + "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=[], - ) - ) - ) + 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( @@ -961,9 +852,7 @@ class SaoGetEquipmentUserDataListResponse(SaoBaseResponse): 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 - ], + 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], @@ -985,27 +874,24 @@ class SaoGetEquipmentUserDataListResponse(SaoBaseResponse): 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 - ) + 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) @@ -1017,21 +903,18 @@ class SaoGetItemUserDataListResponse(SaoBaseResponse): 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 - + 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 + #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 + "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)], @@ -1040,21 +923,15 @@ class SaoGetItemUserDataListResponse(SaoBaseResponse): # 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), + "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=[], - ) - ) - ) + 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( @@ -1064,8 +941,9 @@ class SaoGetItemUserDataListResponse(SaoBaseResponse): 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) @@ -1075,38 +953,36 @@ class SaoGetItemUserDataListResponse(SaoBaseResponse): 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.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 - + 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 + "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 + "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 @@ -1116,23 +992,15 @@ class SaoGetSupportLogUserDataListResponse(SaoBaseResponse): # 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 - ), + "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=[], - ) - ) - ) + 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( @@ -1147,59 +1015,50 @@ class SaoGetSupportLogUserDataListResponse(SaoBaseResponse): 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["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 - + 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 + "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), + "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=[], - ) - ) - ) + 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( @@ -1207,7 +1066,7 @@ class SaoGetTitleUserDataListResponse(SaoBaseResponse): 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) @@ -1218,13 +1077,11 @@ class SaoGetTitleUserDataListResponse(SaoBaseResponse): 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) @@ -1232,34 +1089,16 @@ class SaoGetEpisodeAppendDataListResponse(SaoBaseResponse): 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.user_id_list = [str(profile_data["user"]), str(profile_data["user"]), str(profile_data["user"]), str(profile_data["user"]), str(profile_data["user"])] self.episode_append_id_list = [10001, 10002, 10003, 10004, 10005] - self.own_num_list = [3, 3, 3, 3, 3] - + self.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 + "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, ) @@ -1267,46 +1106,31 @@ class SaoGetEpisodeAppendDataListResponse(SaoBaseResponse): # 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), + "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=[], - ) - ) - ) + 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) - ): + 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.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 - ) + 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) @@ -1314,23 +1138,21 @@ class SaoGetEpisodeAppendDataListResponse(SaoBaseResponse): 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 +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.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.party_team_data_list_size = 3 # Number of arrays self.user_party_team_id_1 = "0" self.arrangement_num_1 = 0 @@ -1364,296 +1186,264 @@ class SaoGetPartyDataListResponse(SaoBaseResponse): # Default party 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 + "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 + "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 + "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)], + "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_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 + "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)], + "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_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 + "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)], + "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, - ) - ) + 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_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 + "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_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 + "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, - ) - ) + 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) @@ -1662,7 +1452,7 @@ class SaoGetResourcePathInfoResponse(SaoBaseResponse): 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( @@ -1677,24 +1467,21 @@ class SaoGetResourcePathInfoResponse(SaoBaseResponse): "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], - ) - ) + 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) @@ -1721,67 +1508,59 @@ class SaoEpisodePlayStartRequest(SaoBaseRequest): 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.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 + + "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], - ) - ) + 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) @@ -1808,14 +1587,13 @@ class SaoEpisodePlayEndRequest(SaoBaseRequest): 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) @@ -1837,62 +1615,69 @@ class SaoEpisodePlayEndResponse(SaoBaseResponse): 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_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, - ) - ) + 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) @@ -1919,17 +1704,16 @@ class SaoTrialTowerPlayStartRequest(SaoBaseRequest): 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) @@ -1956,22 +1740,21 @@ class SaoTrialTowerPlayEndRequest(SaoBaseRequest): 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.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 @@ -1987,9 +1770,7 @@ class SaoTrialTowerPlayEndResponse(SaoBaseResponse): 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_type = 0 # dummy values from 2,101000000,1 from RewardTable.csv self.common_reward_id = 0 self.common_reward_num = 0 @@ -2001,79 +1782,93 @@ class SaoTrialTowerPlayEndResponse(SaoBaseResponse): 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 + + "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_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 + + #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, - ) - ) + 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) @@ -2092,7 +1887,6 @@ class SaoEpisodePlayEndUnanalyzedLogFixedRequest(SaoBaseRequest): 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) @@ -2107,30 +1901,22 @@ class SaoEpisodePlayEndUnanalyzedLogFixedResponse(SaoBaseResponse): 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(",") + 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"]) - ) + 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 - + 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 + #new stuff common_reward_data_struct = Struct( "common_reward_type" / Int16ub, "common_reward_id" / Int32ub, @@ -2139,35 +1925,22 @@ class SaoEpisodePlayEndUnanalyzedLogFixedResponse(SaoBaseResponse): 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), + "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, - ), + "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=[], - ) - ) - ) + 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( @@ -2176,27 +1949,17 @@ class SaoEpisodePlayEndUnanalyzedLogFixedResponse(SaoBaseResponse): 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, - ) - ) - + 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 - ) + 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" - ] - ) + 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) @@ -2204,13 +1967,11 @@ class SaoEpisodePlayEndUnanalyzedLogFixedResponse(SaoBaseResponse): 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) @@ -2225,7 +1986,7 @@ class SaoGetQuestSceneUserDataListResponse(SaoBaseResponse): # quest_scene_best_score_user_data self.clear_time = [] self.combo_num = [] - self.total_damage = [] # string + self.total_damage = [] #string self.concurrent_destroying_num = [] for i in range(len(quest_data)): @@ -2235,117 +1996,85 @@ class SaoGetQuestSceneUserDataListResponse(SaoBaseResponse): 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.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.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 + 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 + #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 + "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_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, - ), + "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 - ), + "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=[], - ) - ) - ) + 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], - ) - ) - + 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 - ) + 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" - ] - ) + 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) @@ -2353,12 +2082,10 @@ class SaoGetQuestSceneUserDataListResponse(SaoBaseResponse): 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) @@ -2366,80 +2093,75 @@ class SaoCheckYuiMedalGetConditionResponse(SaoBaseResponse): 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 + "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, - ) - ) + 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.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 + "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_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], - ) - ) + 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) @@ -2447,29 +2169,27 @@ class SaoCheckProfileCardUsedRewardResponse(SaoBaseResponse): 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 + "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, - ) - ) + 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) @@ -2486,10 +2206,8 @@ class SaoSynthesizeEnhancementHeroLogRequest(SaoBaseRequest): 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_list: List[MaterialCommonRewardUserData] = [] + self.material_common_reward_user_data_count = decode_int(data, off) off += INT_OFF @@ -2498,82 +2216,81 @@ class SaoSynthesizeEnhancementHeroLogRequest(SaoBaseRequest): 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=",") + 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 + 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 < int(data[e + 1][1]): + + for e in range(0,len(data)): + if exp>=int(data[e][1]) and exp bytes: - # new stuff + #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 + "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 + "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 + "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, @@ -2596,21 +2313,15 @@ class SaoSynthesizeEnhancementHeroLogResponse(SaoBaseResponse): # 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), + "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=[], - ) - ) - ) + 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, @@ -2645,13 +2356,12 @@ class SaoSynthesizeEnhancementHeroLogResponse(SaoBaseResponse): 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 - ) + 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) @@ -2659,7 +2369,6 @@ class SaoSynthesizeEnhancementHeroLogResponse(SaoBaseResponse): 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) @@ -2671,15 +2380,13 @@ class SaoSynthesizeEnhancementEquipmentRequest(SaoBaseRequest): 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_list: List[MaterialCommonRewardUserData] = [] + self.material_common_reward_user_data_count = decode_int(data, off) off += INT_OFF @@ -2688,7 +2395,6 @@ class SaoSynthesizeEnhancementEquipmentRequest(SaoBaseRequest): 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) @@ -2696,61 +2402,62 @@ class SaoSynthesizeEnhancementEquipmentResponse(SaoBaseResponse): 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=",") + 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 + 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 < int(data[e + 1][1]): + + 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 + "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 + "awakening_stage" / Int16ub, #short + "awakening_exp" / Int32ub, #int "property1_property_id" / Int32ub, "property1_value1" / Int32ub, "property1_value2" / Int32ub, @@ -2773,23 +2480,15 @@ class SaoSynthesizeEnhancementEquipmentResponse(SaoBaseResponse): # 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 - ), + "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=[], - ) - ) - ) + 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, @@ -2818,13 +2517,12 @@ class SaoSynthesizeEnhancementEquipmentResponse(SaoBaseResponse): 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 - ) + 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) @@ -2832,129 +2530,122 @@ class SaoSynthesizeEnhancementEquipmentResponse(SaoBaseResponse): 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.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.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 + "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, - ) - ) + 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.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.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 + "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 + "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, - ) - ) + 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.ranking_user_data_size = 1 # number of arrays self.rank = 1 self.user_id = "1" @@ -2968,13 +2659,14 @@ class SaoGetDefragMatchLeaguePointRankingListResponse(SaoBaseResponse): 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 + "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 @@ -2983,52 +2675,49 @@ class SaoGetDefragMatchLeaguePointRankingListResponse(SaoBaseResponse): "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 + "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, - ) - ) + 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.ranking_user_data_size = 1 # number of arrays self.rank = 1 self.user_id = "1" @@ -3042,13 +2731,14 @@ class SaoGetDefragMatchLeagueScoreRankingListResponse(SaoBaseResponse): 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 + "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 @@ -3057,47 +2747,44 @@ class SaoGetDefragMatchLeagueScoreRankingListResponse(SaoBaseResponse): "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 + "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, - ) - ) + 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) @@ -3105,7 +2792,7 @@ class SaoBnidSerialCodeCheckResponse(SaoBaseResponse): self.result = 1 self.bnid_item_id = "130050" self.use_status = 0 - + def make(self) -> bytes: # create a resp struct resp_struct = Struct( @@ -3115,177 +2802,176 @@ class SaoBnidSerialCodeCheckResponse(SaoBaseResponse): "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, - ) - ) + 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.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.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.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_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.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_equipment_id = 0 #int + self.sub_equipment_enhancement_value = 0 #short + self.sub_equipment_awakening_stage = 0 #short - self.sub_equipment_property1_property_id = 0 # int - self.sub_equipment_property1_value1 = 0 # int - self.sub_equipment_property1_value2 = 0 # int - self.sub_equipment_property2_property_id = 0 # int - self.sub_equipment_property2_value1 = 0 # int - self.sub_equipment_property2_value2 = 0 # int - self.sub_equipment_property3_property_id = 0 # int - self.sub_equipment_property3_value1 = 0 # int - self.sub_equipment_property3_value2 = 0 # int - self.sub_equipment_property4_property_id = 0 # int - self.sub_equipment_property4_value1 = 0 # int - self.sub_equipment_property4_value2 = 0 # int - - self.holographic_flag = 1 # byte + self.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 + #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 + "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), + "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=[], - ) - ) - ) + 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, @@ -3298,9 +2984,11 @@ class SaoScanQrQuestProfileCardResponse(SaoBaseResponse): 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, @@ -3313,9 +3001,11 @@ class SaoScanQrQuestProfileCardResponse(SaoBaseResponse): 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, @@ -3328,9 +3018,10 @@ class SaoScanQrQuestProfileCardResponse(SaoBaseResponse): 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) @@ -3340,8 +3031,7 @@ class SaoScanQrQuestProfileCardResponse(SaoBaseResponse): 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) @@ -3349,21 +3039,20 @@ class SaoConsumeCreditGuestRequest(SaoBaseRequest): 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) @@ -3389,7 +3078,6 @@ class SaoChangePartyRequest(SaoBaseRequest): 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) @@ -3408,7 +3096,6 @@ class TrialTowerPlayEndUnanalyzedLogFixed(SaoBaseRequest): self.rarity_up_exec_flag = decode_byte(data, off) off += BYTE_OFF - class GetShopResourceSalesDataListRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) @@ -3417,21 +3104,19 @@ class GetShopResourceSalesDataListRequest(SaoBaseRequest): 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.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) @@ -3440,21 +3125,19 @@ class GetYuiMedalShopUserDataListRequest(SaoBaseRequest): 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.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) @@ -3463,200 +3146,87 @@ class GetGashaMedalShopUserDataListRequest(SaoBaseRequest): 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.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.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.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.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.result = 1 # byte self.data_list: List[ResEarnCampaignShop] = [] def make(self) -> bytes: ret = encode_byte(self.result) ret += encode_arr_cls(self.data_list) - + self.header.length = len(ret) return super().make() + ret - - -class SaoGiveFreeTicketRequest(SaoBaseRequest): - def __init__(self, header: SaoRequestHeader, data: bytes) -> None: - super().__init__(header, data) - off = 0 - self.ticket_id, new_off = decode_str(data, off) - off += new_off - - self.user_id, new_off = decode_str(data, off) - off += new_off - - self.give_num = decode_byte(data, off) - off += BYTE_OFF - - -class SaoGiveFreeTicketResponse(SaoBaseResponse): - def __init__(self, cmd_id: int) -> None: - super().__init__(cmd_id) - self.result = 1 # byte - - def make(self) -> bytes: - ret = encode_byte(self.result) - return super().make() + ret - - -class SaoLogoutTicketUnpurchasedRequest(SaoBaseRequest): - def __init__(self, header: SaoRequestHeader, data: bytes) -> None: - super().__init__(header, data) - off = 0 - self.ticket_id, new_off = decode_str(data, off) - off += new_off - - self.user_id, new_off = decode_str(data, off) - off += new_off - - self.cabinet_type = decode_byte(data, off) - off += BYTE_OFF - - -class SaoLogoutTicketUnpurchasedResponse(SaoBaseResponse): - def __init__(self, cmd_id: int) -> None: - super().__init__(cmd_id) - self.result = 1 # byte - - def make(self) -> bytes: - ret = encode_byte(self.result) - return super().make() + ret - - -class SaoGetQuestHierarchyProgressDegreesRankingListRequest(SaoBaseRequest): - def __init__(self, header: SaoRequestHeader, data: bytes) -> None: - super().__init__(header, data) - off = 0 - self.store_id, new_off = decode_str(data, off) - off += new_off - - self.get_rank_start_num = decode_short(data, off) - off += SHORT_OFF - - self.get_rank_end_num = decode_short(data, off) - off += SHORT_OFF - - -class SaoGetQuestHierarchyProgressDegreesRankingListResponse(SaoBaseResponse): - def __init__(self, cmd_id: int) -> None: - super().__init__(cmd_id) - self.result = 1 # byte - self.quest_hierarchy_progress_degrees_ranking_data_list: List[ - QuestHierarchyProgressDegreesRankingData - ] = [] - - def make(self) -> bytes: - ret = encode_byte(self.result) - ret += encode_arr_cls(self.quest_hierarchy_progress_degrees_ranking_data_list) - return super().make() + ret - - -class SaoGetQuestPopularHeroLogRankingListRequest(SaoBaseRequest): - def __init__(self, header: SaoRequestHeader, data: bytes) -> None: - super().__init__(header, data) - off = 0 - self.store_id, new_off = decode_str(data, off) - off += new_off - - self.get_rank_start_num = decode_short(data, off) - off += SHORT_OFF - - self.get_rank_end_num = decode_short(data, off) - off += SHORT_OFF - - -class SaoGetQuestPopularHeroLogRankingListResponse(SaoBaseResponse): - def __init__(self, cmd_id: int) -> None: - super().__init__(cmd_id) - self.result = 1 # byte - self.quest_popular_hero_log_ranking_data_list: List[ - PopularHeroLogRankingData - ] = [] - - def make(self) -> bytes: - ret = encode_byte(self.result) - ret += encode_arr_cls(self.quest_popular_hero_log_ranking_data_list) - return super().make() + ret diff --git a/titles/sao/handlers/helpers.py b/titles/sao/handlers/helpers.py index 450cd36..6e11146 100644 --- a/titles/sao/handlers/helpers.py +++ b/titles/sao/handlers/helpers.py @@ -1,7 +1,7 @@ -import logging +from typing import Tuple, List, Optional import struct +import logging from datetime import datetime -from typing import List, Optional, Tuple BIGINT_OFF = 16 LONG_OFF = 8 @@ -11,124 +11,100 @@ 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") + 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}" - ) + 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" - ) + 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!") + 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]: +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!") + 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) @@ -145,50 +121,46 @@ def encode_arr_num(data: List[int], element_size: int) -> bytes: for x in data: ret += encode_bigint(x) else: - logging.getLogger("sao").error(f"Unknown element size {element_size}") + 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) @@ -199,7 +171,6 @@ class MaterialCommonRewardUserData(BaseHelper): 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 @@ -239,7 +210,6 @@ class PartyTeamData(BaseHelper): self._sz = sz - class PartyData(BaseHelper): def __init__(self, data: bytes, offset: int) -> None: sz = 0 @@ -261,7 +231,6 @@ class PartyData(BaseHelper): self._sz = sz - class PlayStartRequestData(BaseHelper): def __init__(self, data: bytes, offset: int) -> None: sz = 0 @@ -282,14 +251,12 @@ class PlayStartRequestData(BaseHelper): 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) @@ -299,28 +266,24 @@ class BaseGetData(BaseHelper): 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) @@ -329,7 +292,6 @@ class EventItemData(BaseHelper): 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) @@ -338,7 +300,6 @@ class DiscoveryEnemyData(BaseHelper): self._sz = INT_OFF + SHORT_OFF - class DestroyBossData(BaseHelper): def __init__(self, data: bytes, offset: int) -> None: self.boss_type = decode_byte(data, offset) @@ -349,7 +310,6 @@ class DestroyBossData(BaseHelper): 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) @@ -360,7 +320,6 @@ class MissionData(BaseHelper): self._sz = INT_OFF + SHORT_OFF + BYTE_OFF - class ScoreData(BaseHelper): def __init__(self, data: bytes, offset: int) -> None: super().__init__(data, offset) @@ -404,7 +363,6 @@ class ScoreData(BaseHelper): 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 @@ -450,9 +408,7 @@ class PlayEndRequestData(BaseHelper): 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 - ] = [] + 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() @@ -505,7 +461,6 @@ class PlayEndRequestData(BaseHelper): self._sz = sz - class EntryUserData(BaseHelper): def __init__(self, data: bytes, offset: int) -> None: super().__init__(data, offset) @@ -520,7 +475,6 @@ class EntryUserData(BaseHelper): 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) @@ -540,7 +494,6 @@ class MultiPlayStartRequestData(BaseHelper): 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) @@ -551,7 +504,6 @@ class MultiPlayEndRequestData(BaseHelper): 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) @@ -559,108 +511,97 @@ class SalesResourceData(BaseHelper): 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 - + 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] - ) + + 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": + 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.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) @@ -670,69 +611,59 @@ class ShopResourceSalesData(BaseHelper): 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": + 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": + 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] @@ -740,7 +671,7 @@ class YuiMedalShopData(BaseHelper): 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) @@ -765,27 +696,25 @@ class YuiMedalShopData(BaseHelper): 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": + 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) @@ -807,7 +736,6 @@ class YuiMedalShopData(BaseHelper): ret += encode_byte(self.sort) return ret - class YuiMedalShopItemData(BaseHelper): def __init__(self, data: bytes, offset: int) -> None: super().__init__(data, offset) @@ -823,45 +751,37 @@ class YuiMedalShopItemData(BaseHelper): 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": + 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 @@ -870,7 +790,7 @@ class YuiMedalShopItemData(BaseHelper): 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) @@ -878,25 +798,24 @@ class YuiMedalShopItemData(BaseHelper): 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) @@ -925,21 +844,19 @@ class ResEarnCampaignShop(BaseHelper): 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": + 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) @@ -954,7 +871,6 @@ class ResEarnCampaignShop(BaseHelper): 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) @@ -975,20 +891,13 @@ class GashaMedalShop(BaseHelper): 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": + 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 @@ -996,7 +905,7 @@ class GashaMedalShop(BaseHelper): 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) @@ -1006,82 +915,3 @@ class GashaMedalShop(BaseHelper): ret += encode_str(fmt_dt(self.sales_start_date)) ret += encode_str(fmt_dt(self.sales_end_date)) return ret - - -class QuestHierarchyProgressDegreesRankingData(BaseHelper): - def __init__(self, data: bytes, offset: int) -> None: - super().__init__(data, offset) - self.rank = decode_int(data, offset + self._sz) - self._sz += INT_OFF - self.trial_tower_id = decode_int(data, offset + self._sz) - self._sz += INT_OFF - - user_id = decode_str(data, offset + self._sz) - self.user_id = user_id[0] - self._sz += user_id[1] - - nick_name = decode_str(data, offset + self._sz) - self.nick_name = nick_name[0] - self._sz += nick_name[1] - - self.setting_title_id = decode_int(data, offset + self._sz) - self._sz += INT_OFF - self.favorite_hero_log_id = decode_int(data, offset + self._sz) - self._sz += INT_OFF - self.favorite_hero_log_awakening_stage = decode_short(data, offset + self._sz) - self._sz += SHORT_OFF - self.favorite_support_log_id = decode_int(data, offset + self._sz) - self._sz += INT_OFF - self.favorite_support_log_awakening_stage = decode_short( - data, offset + self._sz - ) - self._sz += SHORT_OFF - - clear_time = decode_str(data, offset + self._sz) - self.clear_time = clear_time[0] - self._sz += clear_time[1] - - @classmethod - def from_args(cls) -> "QuestHierarchyProgressDegreesRankingData": - ret = cls(b"\x00" * 36, 0) - return ret - - def make(self) -> bytes: - ret = encode_int(self.rank) - ret += encode_int(self.trial_tower_id) - ret += encode_str(self.user_id) - ret += encode_str(self.nick_name) - ret += encode_int(self.setting_title_id) - ret += encode_int(self.favorite_hero_log_id) - ret += encode_short(self.favorite_hero_log_awakening_stage) - ret += encode_int(self.favorite_support_log_id) - ret += encode_short(self.favorite_support_log_awakening_stage) - ret += encode_str(self.clear_time) - return ret - - -class PopularHeroLogRankingData(BaseHelper): - def __init__(self, data: bytes, offset: int) -> None: - super().__init__(data, offset) - self.rank = decode_int(data, offset + self._sz) - self._sz += INT_OFF - self.hero_log_id = decode_int(data, offset + self._sz) - self._sz += INT_OFF - self.used_num = decode_int(data, offset + self._sz) - self._sz += INT_OFF - - @classmethod - def from_args( - cls, ranking: int, hero_id: int, used_num: int - ) -> "PopularHeroLogRankingData": - ret = cls(b"\x00" * 12, 0) - cls.ranking = ranking - cls.hero_log_id = hero_id - cls.used_num = used_num - return ret - - def make(self) -> bytes: - ret = encode_int(self.rank) - ret += encode_int(self.hero_log_id) - ret += encode_int(self.used_num) - return ret diff --git a/titles/sao/index.py b/titles/sao/index.py index cebd634..78d641e 100644 --- a/titles/sao/index.py +++ b/titles/sao/index.py @@ -1,21 +1,18 @@ -import logging -import secrets -from hashlib import md5 +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 typing import List, Tuple +from Crypto.Cipher import Blowfish +from hashlib import md5 +import secrets -import coloredlogs -import yaml from core import CoreConfig, Utils from core.title import BaseServlet -from Crypto.Cipher import Blowfish -from starlette.requests import Request -from starlette.responses import Response -from starlette.routing import Route -from titles.sao.base import SaoBase from titles.sao.config import SaoConfig from titles.sao.const import SaoConstants +from titles.sao.base import SaoBase from titles.sao.handlers.base import * @@ -54,106 +51,94 @@ class SaoServlet(BaseServlet): 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_routes(self) -> List[Route]: - return [ - Route( - "/{datecode:int}/proto/if/{category:str}/{endpoint:str}", - self.render_POST, - methods=["POST"], - ) - ] - + 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: + 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]: + + def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: port_ssl = Utils.get_title_port_ssl(self.core_cfg) port_normal = Utils.get_title_port(self.core_cfg) proto = "http" - port = f":{port_normal}" if port_normal != 80 else "" - - if self.game_cfg.server.use_https: + 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 port_ssl != 443 else "" + port = f":{port_ssl}" if not self.core_cfg.server.is_using_proxy and port_ssl != 443 else "" - return (f"{proto}://{self.core_cfg.server.hostname}{port}/", "") + 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 (False, "") - return (True, SaoConstants.GAME_CDS, SaoConstants.NETID_PREFIX) + return (True, "SAO1") - async def render_POST(self, request: Request) -> bytes: - endpoint = request.path_params.get("endpoint", "") + 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 = await request.body() + req_raw = request.content.read() if len(req_raw) < 40: self.logger.warn(f"Malformed request to {endpoint} - {req_raw.hex()}") - return Response() + 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 Response() - + 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 - ) + 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 = await handler(req_header, req_data) + 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 Response() - + return b"" + self.logger.debug(f"Response: {resp.hex()}") if self.game_cfg.crypt.enable: @@ -161,16 +146,12 @@ class SaoServlet(BaseServlet): 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 - ) + + 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?? + tmp = struct.pack("!I", crypt_data_len) # does it want the length of the encrypted response?? resp = resp[:20] + tmp + iv + data_crypt self.logger.debug(f"Encrypted Response: {resp.hex()}") - return Response(resp, media_type="text/html; charset=utf-8") + return resp \ No newline at end of file diff --git a/titles/sao/read.py b/titles/sao/read.py index 2526603..dafb450 100644 --- a/titles/sao/read.py +++ b/titles/sao/read.py @@ -1,11 +1,12 @@ +from typing import Optional, Dict, List +from os import walk, path +import urllib import csv -from os import path -from typing import Optional -from core.config import CoreConfig from read import BaseReader -from titles.sao.const import SaoConstants +from core.config import CoreConfig from titles.sao.database import SaoData +from titles.sao.const import SaoConstants class SaoReader(BaseReader): @@ -28,14 +29,17 @@ class SaoReader(BaseReader): self.logger.error(f"Invalid project SAO version {version}") exit(1) - async def read(self) -> None: - if path.exists(self.bin_dir): - await self.read_csv(f"{self.bin_dir}") + def read(self) -> None: + pull_bin_ram = True - else: - self.logger.warn("Directory not found, nothing to import") + 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 - async def read_csv(self, bin_dir: str) -> None: + 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") @@ -50,16 +54,20 @@ class SaoReader(BaseReader): enabled = True self.logger.info(f"Added quest {questSceneId} | Name: {name}") - + try: - await self.data.static.put_quest( - questSceneId, 0, sortNo, name, enabled + 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" @@ -76,9 +84,9 @@ class SaoReader(BaseReader): enabled = True self.logger.info(f"Added hero {heroLogId} | Name: {name}") - + try: - await self.data.static.put_hero( + self.data.static.put_hero( 0, heroLogId, name, @@ -87,13 +95,13 @@ class SaoReader(BaseReader): skillTableSubId, awakeningExp, flavorText, - enabled, + 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" @@ -109,9 +117,9 @@ class SaoReader(BaseReader): enabled = True self.logger.info(f"Added equipment {equipmentId} | Name: {name}") - + try: - await self.data.static.put_equipment( + self.data.static.put_equipment( 0, equipmentId, name, @@ -119,7 +127,7 @@ class SaoReader(BaseReader): weaponTypeId, rarity, flavorText, - enabled, + enabled ) except Exception as err: self.logger.error(err) @@ -140,16 +148,22 @@ class SaoReader(BaseReader): enabled = True self.logger.info(f"Added item {itemId} | Name: {name}") - + try: - await self.data.static.put_item( - 0, itemId, name, itemTypeId, rarity, flavorText, enabled + 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" @@ -165,9 +179,9 @@ class SaoReader(BaseReader): enabled = True self.logger.info(f"Added support log {supportLogId} | Name: {name}") - + try: - await self.data.static.put_support_log( + self.data.static.put_support_log( 0, supportLogId, charaId, @@ -175,13 +189,13 @@ class SaoReader(BaseReader): rarity, salePrice, skillName, - enabled, + 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" @@ -196,23 +210,21 @@ class SaoReader(BaseReader): enabled = True self.logger.info(f"Added title {titleId} | Name: {displayName}") - + if len(titleId) > 5: try: - await self.data.static.put_title( + self.data.static.put_title( 0, titleId, displayName, requirement, rank, imageFilePath, - enabled, + enabled ) except Exception as err: self.logger.error(err) - elif ( - len(titleId) < 6 - ): # current server code cannot have multiple lengths for the id + 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") @@ -227,13 +239,14 @@ class SaoReader(BaseReader): commonRewardId = row["CommonRewardId"] enabled = True - self.logger.info( - f"Added rare drop {questRareDropId} | Reward: {commonRewardId}" - ) - + self.logger.info(f"Added rare drop {questRareDropId} | Reward: {commonRewardId}") + try: - await self.data.static.put_rare_drop( - 0, questRareDropId, commonRewardId, enabled + self.data.static.put_rare_drop( + 0, + questRareDropId, + commonRewardId, + enabled ) except Exception as err: self.logger.error(err) diff --git a/titles/sao/schema/__init__.py b/titles/sao/schema/__init__.py index 5c4fb6e..3e75fc0 100644 --- a/titles/sao/schema/__init__.py +++ b/titles/sao/schema/__init__.py @@ -1,4 +1,3 @@ -# ruff: noqa: F401 -from .item import SaoItemData 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 index fafca9a..11adf27 100644 --- a/titles/sao/schema/item.py +++ b/titles/sao/schema/item.py @@ -1,12 +1,12 @@ -from typing import Dict, List, Optional +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 sqlalchemy import Column, Table, UniqueConstraint, and_ -from sqlalchemy.dialects.mysql import insert -from sqlalchemy.engine import Row -from sqlalchemy.schema import ForeignKey -from sqlalchemy.sql import func -from sqlalchemy.types import JSON, TIMESTAMP, Boolean, Integer equipment_data = Table( "sao_equipment_data", @@ -118,9 +118,7 @@ sessions = Table( 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" - ), + UniqueConstraint("user", "user_party_team_id", "play_date", name="sao_play_sessions_uk"), mysql_charset="utf8mb4", ) @@ -140,51 +138,41 @@ end_sessions = Table( mysql_charset="utf8mb4", ) - class SaoItemData(BaseData): - async 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]: + 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, - ) + quest_drop_boost_apply_flag=quest_drop_boost_apply_flag + ) conflict = sql.on_duplicate_key_update(user=user_id) - result = await self.execute(conflict) + 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 - async def create_end_session( - self, user_id: int, quest_id: int, play_result_flag: bool, reward_data: JSON - ) -> Optional[int]: + 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 = await self.execute(conflict) + 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 - async def put_item(self, user_id: int, item_id: int) -> Optional[int]: + def put_item(self, user_id: int, item_id: int) -> Optional[int]: sql = insert(item_data).values( user=user_id, item_id=item_id, @@ -194,7 +182,7 @@ class SaoItemData(BaseData): item_id=item_id, ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.error( f"{__name__} failed to insert item! user: {user_id}, item_id: {item_id}" @@ -202,17 +190,8 @@ class SaoItemData(BaseData): return None return result.lastrowid - - async def put_equipment_data( - self, - user_id: int, - equipment_id: int, - enhancement_value: int, - enhancement_exp: int, - awakening_exp: int, - awakening_stage: int, - possible_awakening_flag: int, - ) -> Optional[int]: + + 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, @@ -231,7 +210,7 @@ class SaoItemData(BaseData): possible_awakening_flag=possible_awakening_flag, ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.error( f"{__name__} failed to insert equipment! user: {user_id}, equipment_id: {equipment_id}" @@ -240,20 +219,7 @@ class SaoItemData(BaseData): return result.lastrowid - async def put_hero_log( - self, - user_id: int, - user_hero_log_id: int, - log_level: int, - log_exp: int, - main_weapon: int, - sub_equipment: int, - skill_slot1_skill_id: int, - skill_slot2_skill_id: int, - skill_slot3_skill_id: int, - skill_slot4_skill_id: int, - skill_slot5_skill_id: int, - ) -> Optional[int]: + 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, @@ -280,7 +246,7 @@ class SaoItemData(BaseData): skill_slot5_skill_id=skill_slot5_skill_id, ) - result = await self.execute(conflict) + 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}" @@ -289,14 +255,7 @@ class SaoItemData(BaseData): return result.lastrowid - async def put_hero_party( - self, - user_id: int, - user_party_team_id: int, - user_hero_log_id_1: int, - user_hero_log_id_2: int, - user_hero_log_id_3: int, - ) -> Optional[int]: + 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, @@ -311,7 +270,7 @@ class SaoItemData(BaseData): user_hero_log_id_3=user_hero_log_id_3, ) - result = await self.execute(conflict) + 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}" @@ -320,16 +279,7 @@ class SaoItemData(BaseData): return result.lastrowid - async def put_player_quest( - self, - user_id: int, - episode_id: int, - quest_clear_flag: bool, - clear_time: int, - combo_num: int, - total_damage: int, - concurrent_destroying_num: int, - ) -> Optional[int]: + 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, @@ -337,7 +287,7 @@ class SaoItemData(BaseData): clear_time=clear_time, combo_num=combo_num, total_damage=total_damage, - concurrent_destroying_num=concurrent_destroying_num, + concurrent_destroying_num=concurrent_destroying_num ) conflict = sql.on_duplicate_key_update( @@ -345,10 +295,10 @@ class SaoItemData(BaseData): clear_time=clear_time, combo_num=combo_num, total_damage=total_damage, - concurrent_destroying_num=concurrent_destroying_num, + concurrent_destroying_num=concurrent_destroying_num ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.error( f"{__name__} failed to insert quest! user: {user_id}, episode_id: {episode_id}" @@ -357,20 +307,17 @@ class SaoItemData(BaseData): return result.lastrowid - async def get_user_equipment( - self, user_id: int, equipment_id: int - ) -> Optional[Dict]: - sql = equipment_data.select( - equipment_data.c.user == user_id - and equipment_data.c.equipment_id == equipment_id - ) - - result = await self.execute(sql) + 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() - - async def get_user_equipments(self, user_id: int) -> Optional[List[Row]]: + + def get_user_equipments( + self, user_id: int + ) -> Optional[List[Row]]: """ A catch-all equipments lookup given a profile """ @@ -380,12 +327,14 @@ class SaoItemData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_user_items(self, user_id: int) -> Optional[List[Row]]: + def get_user_items( + self, user_id: int + ) -> Optional[List[Row]]: """ A catch-all items lookup given a profile """ @@ -395,12 +344,12 @@ class SaoItemData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_hero_log( + def get_hero_log( self, user_id: int, user_hero_log_id: int = None ) -> Optional[List[Row]]: """ @@ -409,18 +358,18 @@ class SaoItemData(BaseData): sql = hero_log_data.select( and_( hero_log_data.c.user == user_id, - hero_log_data.c.user_hero_log_id == user_hero_log_id - if user_hero_log_id is not None - else True, + hero_log_data.c.user_hero_log_id == user_hero_log_id if user_hero_log_id is not None else True, ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_hero_logs(self, user_id: int) -> Optional[List[Row]]: + def get_hero_logs( + self, user_id: int + ) -> Optional[List[Row]]: """ A catch-all hero lookup given a profile """ @@ -430,29 +379,27 @@ class SaoItemData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_hero_party( + 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, + hero_party.c.user_party_team_id == user_party_team_id if user_party_team_id is not None else True, ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_quest_log( + def get_quest_log( self, user_id: int, episode_id: int = None ) -> Optional[List[Row]]: """ @@ -465,12 +412,14 @@ class SaoItemData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_quest_logs(self, user_id: int) -> Optional[List[Row]]: + def get_quest_logs( + self, user_id: int + ) -> Optional[List[Row]]: """ A catch-all quest lookup given a profile """ @@ -480,73 +429,78 @@ class SaoItemData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_session(self, user_id: int = None) -> Optional[List[Row]]: + 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()) + ).order_by( + sessions.c.play_date.asc() + ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_end_session(self, user_id: int = None) -> Optional[List[Row]]: + def get_end_session( + self, user_id: int = None + ) -> Optional[List[Row]]: sql = end_sessions.select( and_( end_sessions.c.user == user_id, ) - ).order_by(end_sessions.c.play_date.desc()) + ).order_by( + end_sessions.c.play_date.asc() + ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def remove_hero_log(self, user_id: int, user_hero_log_id: int) -> None: + def remove_hero_log(self, user_id: int, user_hero_log_id: int) -> None: sql = hero_log_data.delete( - and_( + and_( hero_log_data.c.user == user_id, hero_log_data.c.user_hero_log_id == user_hero_log_id, ) ) - result = await self.execute(sql) + 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 - async def remove_equipment(self, user_id: int, equipment_id: int) -> 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, - ) + and_(equipment_data.c.user == user_id, equipment_data.c.equipment_id == equipment_id) ) - result = await self.execute(sql) + 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 - async def remove_item(self, user_id: int, item_id: int) -> 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 = await self.execute(sql) + 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 + return None \ No newline at end of file diff --git a/titles/sao/schema/profile.py b/titles/sao/schema/profile.py index 1b263f4..d7320cc 100644 --- a/titles/sao/schema/profile.py +++ b/titles/sao/schema/profile.py @@ -1,11 +1,13 @@ -from typing import Optional +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 sqlalchemy import Column, Table -from sqlalchemy.dialects.mysql import insert -from sqlalchemy.engine import Row -from sqlalchemy.schema import ForeignKey -from sqlalchemy.types import Integer, String +from ..const import SaoConstants profile = Table( "sao_profile", @@ -22,35 +24,23 @@ profile = Table( Column("rank_num", Integer, server_default="1"), Column("rank_exp", Integer, server_default="0"), Column("own_col", Integer, server_default="0"), - Column("own_vp", Integer, server_default="300"), + Column("own_vp", Integer, server_default="0"), Column("own_yui_medal", Integer, server_default="0"), Column("setting_title_id", Integer, server_default="20005"), ) - class SaoProfileData(BaseData): - async def create_profile(self, user_id: int) -> Optional[int]: + 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 = await self.execute(conflict) + 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 - async def put_profile( - self, - user_id: int, - user_type: int, - nick_name: str, - rank_num: int, - rank_exp: int, - own_col: int, - own_vp: int, - own_yui_medal: int, - setting_title_id: int, - ) -> Optional[int]: + 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, @@ -60,7 +50,7 @@ class SaoProfileData(BaseData): own_col=own_col, own_vp=own_vp, own_yui_medal=own_yui_medal, - setting_title_id=setting_title_id, + setting_title_id=setting_title_id ) conflict = sql.on_duplicate_key_update( @@ -69,19 +59,21 @@ class SaoProfileData(BaseData): own_col=own_col, own_vp=own_vp, own_yui_medal=own_yui_medal, - setting_title_id=setting_title_id, + setting_title_id=setting_title_id ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: - self.logger.error(f"{__name__} failed to insert profile! user: {user_id}") + self.logger.error( + f"{__name__} failed to insert profile! user: {user_id}" + ) return None return result.lastrowid - async def get_profile(self, user_id: int) -> Optional[Row]: + def get_profile(self, user_id: int) -> Optional[Row]: sql = profile.select(profile.c.user == user_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None - return result.fetchone() + return result.fetchone() \ No newline at end of file diff --git a/titles/sao/schema/static.py b/titles/sao/schema/static.py index e8965ca..ce9a6a9 100644 --- a/titles/sao/schema/static.py +++ b/titles/sao/schema/static.py @@ -1,9 +1,13 @@ 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 -from sqlalchemy import Column, Table, UniqueConstraint -from sqlalchemy.dialects.mysql import insert -from sqlalchemy.types import Boolean, Integer, String quest = Table( "sao_static_quest", @@ -14,7 +18,9 @@ quest = Table( Column("sortNo", Integer), Column("name", String(255)), Column("enabled", Boolean), - UniqueConstraint("version", "questSceneId", name="sao_static_quest_uk"), + UniqueConstraint( + "version", "questSceneId", name="sao_static_quest_uk" + ), mysql_charset="utf8mb4", ) @@ -31,7 +37,9 @@ hero = Table( Column("awakeningExp", Integer), Column("flavorText", String(255)), Column("enabled", Boolean), - UniqueConstraint("version", "heroLogId", name="sao_static_hero_list_uk"), + UniqueConstraint( + "version", "heroLogId", name="sao_static_hero_list_uk" + ), mysql_charset="utf8mb4", ) @@ -47,7 +55,9 @@ equipment = Table( Column("rarity", Integer), Column("flavorText", String(255)), Column("enabled", Boolean), - UniqueConstraint("version", "equipmentId", name="sao_static_equipment_list_uk"), + UniqueConstraint( + "version", "equipmentId", name="sao_static_equipment_list_uk" + ), mysql_charset="utf8mb4", ) @@ -62,7 +72,9 @@ item = Table( Column("rarity", Integer), Column("flavorText", String(255)), Column("enabled", Boolean), - UniqueConstraint("version", "itemId", name="sao_static_item_list_uk"), + UniqueConstraint( + "version", "itemId", name="sao_static_item_list_uk" + ), mysql_charset="utf8mb4", ) @@ -78,7 +90,9 @@ support = Table( Column("salePrice", Integer), Column("skillName", String(255)), Column("enabled", Boolean), - UniqueConstraint("version", "supportLogId", name="sao_static_support_log_list_uk"), + UniqueConstraint( + "version", "supportLogId", name="sao_static_support_log_list_uk" + ), mysql_charset="utf8mb4", ) @@ -91,10 +105,7 @@ rare_drop = Table( Column("commonRewardId", Integer), Column("enabled", Boolean), UniqueConstraint( - "version", - "questRareDropId", - "commonRewardId", - name="sao_static_rare_drop_list_uk", + "version", "questRareDropId", "commonRewardId", name="sao_static_rare_drop_list_uk" ), mysql_charset="utf8mb4", ) @@ -110,44 +121,32 @@ title = Table( Column("rank", Integer), Column("imageFilePath", String(255)), Column("enabled", Boolean), - UniqueConstraint("version", "titleId", name="sao_static_title_list_uk"), + UniqueConstraint( + "version", "titleId", name="sao_static_title_list_uk" + ), mysql_charset="utf8mb4", ) - class SaoStaticData(BaseData): - async def put_quest( - self, questSceneId: int, version: int, sortNo: int, name: str, enabled: bool - ) -> Optional[int]: + 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, - enabled=enabled, + tutorial=tutorial, ) conflict = sql.on_duplicate_key_update( name=name, questSceneId=questSceneId, version=version ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: return None return result.lastrowid - - async def put_hero( - self, - version: int, - heroLogId: int, - name: str, - nickname: str, - rarity: int, - skillTableSubId: int, - awakeningExp: int, - flavorText: str, - enabled: bool, - ) -> Optional[int]: + + 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, @@ -157,27 +156,19 @@ class SaoStaticData(BaseData): skillTableSubId=skillTableSubId, awakeningExp=awakeningExp, flavorText=flavorText, - enabled=enabled, + enabled=enabled ) - conflict = sql.on_duplicate_key_update(name=name, heroLogId=heroLogId) + conflict = sql.on_duplicate_key_update( + name=name, heroLogId=heroLogId + ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: return None return result.lastrowid - - async def put_equipment( - self, - version: int, - equipmentId: int, - name: str, - equipmentType: int, - weaponTypeId: int, - rarity: int, - flavorText: str, - enabled: bool, - ) -> Optional[int]: + + 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, @@ -186,26 +177,19 @@ class SaoStaticData(BaseData): weaponTypeId=weaponTypeId, rarity=rarity, flavorText=flavorText, - enabled=enabled, + enabled=enabled ) - conflict = sql.on_duplicate_key_update(name=name, equipmentId=equipmentId) + conflict = sql.on_duplicate_key_update( + name=name, equipmentId=equipmentId + ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: return None return result.lastrowid - - async def put_item( - self, - version: int, - itemId: int, - name: str, - itemTypeId: int, - rarity: int, - flavorText: str, - enabled: bool, - ) -> Optional[int]: + + 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, @@ -213,27 +197,19 @@ class SaoStaticData(BaseData): itemTypeId=itemTypeId, rarity=rarity, flavorText=flavorText, - enabled=enabled, + enabled=enabled ) - conflict = sql.on_duplicate_key_update(name=name, itemId=itemId) + conflict = sql.on_duplicate_key_update( + name=name, itemId=itemId + ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: return None return result.lastrowid - - async def put_support_log( - self, - version: int, - supportLogId: int, - charaId: int, - name: str, - rarity: int, - salePrice: int, - skillName: str, - enabled: bool, - ) -> Optional[int]: + + 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, @@ -242,19 +218,19 @@ class SaoStaticData(BaseData): rarity=rarity, salePrice=salePrice, skillName=skillName, - enabled=enabled, + enabled=enabled ) - conflict = sql.on_duplicate_key_update(name=name, supportLogId=supportLogId) + conflict = sql.on_duplicate_key_update( + name=name, supportLogId=supportLogId + ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: return None return result.lastrowid - async def put_rare_drop( - self, version: int, questRareDropId: int, commonRewardId: int, enabled: bool - ) -> Optional[int]: + def put_rare_drop( self, version: int, questRareDropId: int, commonRewardId: int, enabled: bool ) -> Optional[int]: sql = insert(rare_drop).values( version=version, questRareDropId=questRareDropId, @@ -263,26 +239,15 @@ class SaoStaticData(BaseData): ) conflict = sql.on_duplicate_key_update( - questRareDropId=questRareDropId, - commonRewardId=commonRewardId, - version=version, + questRareDropId=questRareDropId, commonRewardId=commonRewardId, version=version ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: return None return result.lastrowid - - async def put_title( - self, - version: int, - titleId: int, - displayName: str, - requirement: int, - rank: int, - imageFilePath: str, - enabled: bool, - ) -> Optional[int]: + + 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, @@ -290,116 +255,114 @@ class SaoStaticData(BaseData): requirement=requirement, rank=rank, imageFilePath=imageFilePath, - enabled=enabled, + enabled=enabled ) - conflict = sql.on_duplicate_key_update(displayName=displayName, titleId=titleId) + conflict = sql.on_duplicate_key_update( + displayName=displayName, titleId=titleId + ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: return None return result.lastrowid - async def get_quests_id(self, sortNo: int) -> Optional[Dict]: + def get_quests_id(self, sortNo: int) -> Optional[Dict]: sql = quest.select(quest.c.sortNo == sortNo) - - result = await self.execute(sql) + + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_quests_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]: - sql = quest.select( - quest.c.version == version and quest.c.enabled == enabled - ).order_by(quest.c.questSceneId.asc()) + def get_quests_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]: + sql = quest.select(quest.c.version == version and quest.c.enabled == enabled).order_by( + quest.c.questSceneId.asc() + ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return [list[2] for list in result.fetchall()] - - async def get_hero_id(self, heroLogId: int) -> Optional[Dict]: + + def get_hero_id(self, heroLogId: int) -> Optional[Dict]: sql = hero.select(hero.c.heroLogId == heroLogId) - - result = await self.execute(sql) + + 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() + ) - async def get_hero_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]: - sql = hero.select( - hero.c.version == version and hero.c.enabled == enabled - ).order_by(hero.c.heroLogId.asc()) - - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return [list[2] for list in result.fetchall()] - - async def get_equipment_id(self, equipmentId: int) -> Optional[Dict]: + + def get_equipment_id(self, equipmentId: int) -> Optional[Dict]: sql = equipment.select(equipment.c.equipmentId == equipmentId) - - result = await self.execute(sql) + + 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() + ) - async def get_equipment_ids( - self, version: int, enabled: bool - ) -> Optional[List[Dict]]: - sql = equipment.select( - equipment.c.version == version and equipment.c.enabled == enabled - ).order_by(equipment.c.equipmentId.asc()) - - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return [list[2] for list in result.fetchall()] - async def get_item_id(self, itemId: int) -> Optional[Dict]: + def get_item_id(self, itemId: int) -> Optional[Dict]: sql = item.select(item.c.itemId == itemId) - - result = await self.execute(sql) + + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_rare_drop_id(self, questRareDropId: int) -> Optional[Dict]: + def get_rare_drop_id(self, questRareDropId: int) -> Optional[Dict]: sql = rare_drop.select(rare_drop.c.questRareDropId == questRareDropId) - - result = await self.execute(sql) + + 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() + ) - async 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 = await self.execute(sql) + 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() + ) - async def get_support_log_ids( - self, version: int, enabled: bool - ) -> Optional[List[Dict]]: - sql = support.select( - support.c.version == version and support.c.enabled == enabled - ).order_by(support.c.supportLogId.asc()) - - result = await self.execute(sql) + 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() + ) - async 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 = await self.execute(sql) + result = self.execute(sql) if result is None: return None - return [list[2] for list in result.fetchall()] + 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 3103bba..a3bf96b 100644 --- a/titles/wacca/__init__.py +++ b/titles/wacca/__init__.py @@ -1,11 +1,12 @@ from titles.wacca.const import WaccaConstants -from titles.wacca.database import WaccaData -from titles.wacca.frontend import WaccaFrontend 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 frontend = WaccaFrontend game_codes = [WaccaConstants.GAME_CODE] +current_schema_version = 5 diff --git a/titles/wacca/base.py b/titles/wacca/base.py index b9815fb..dd3fe24 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -1,15 +1,15 @@ +from typing import Any, List, Dict import logging -from datetime import datetime, timedelta -from math import floor -from typing import Any, Dict, List - import inflection +from math import floor +from datetime import datetime, timedelta from core.config import CoreConfig -from core.const import AllnetCountryCode 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: @@ -83,24 +83,24 @@ class WaccaBase: else: self.region_id = WaccaConstants.Region[prefecture_name] - async def handle_housing_get_request(self, data: Dict) -> Dict: + def handle_housing_get_request(self, data: Dict) -> Dict: req = BaseRequest(data) housing_id = 1337 self.logger.info(f"{req.chipId} -> {housing_id}") resp = HousingGetResponse(housing_id) return resp.make() - async def handle_advertise_GetRanking_request(self, data: Dict) -> Dict: + def handle_advertise_GetRanking_request(self, data: Dict) -> Dict: req = AdvertiseGetRankingRequest(data) return AdvertiseGetRankingResponse().make() - async def handle_housing_start_request(self, data: Dict) -> Dict: + def handle_housing_start_request(self, data: Dict) -> Dict: req = HousingStartRequestV1(data) allnet_region_id = None - machine = await self.data.arcade.get_machine(req.chipId) + machine = self.data.arcade.get_machine(req.chipId) if machine is not None: - arcade = await self.data.arcade.get_arcade(machine["arcade"]) + arcade = self.data.arcade.get_arcade(machine["arcade"]) allnet_region_id = arcade["region_id"] if req.appVersion.country == AllnetCountryCode.JAPAN.value: @@ -126,20 +126,20 @@ class WaccaBase: resp = HousingStartResponseV1(region_id) return resp.make() - async def handle_advertise_GetNews_request(self, data: Dict) -> Dict: + def handle_advertise_GetNews_request(self, data: Dict) -> Dict: resp = GetNewsResponseV1() return resp.make() - async def handle_user_status_logout_request(self, data: Dict) -> Dict: + def handle_user_status_logout_request(self, data: Dict) -> Dict: req = UserStatusLogoutRequest(data) self.logger.info(f"Log out user {req.userId} from {req.chipId}") return BaseResponse().make() - async def handle_user_status_get_request(self, data: Dict) -> Dict: + def handle_user_status_get_request(self, data: Dict) -> Dict: req = UserStatusGetRequest(data) resp = UserStatusGetV1Response() - profile = await self.data.profile.get_profile(aime_id=req.aimeId) + 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 @@ -159,14 +159,14 @@ class WaccaBase: resp.userStatus.wp = profile["wp"] resp.userStatus.useCount = profile["login_count"] - set_title_id = await self.data.profile.get_options( + 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 = await self.data.profile.get_options( + set_icon_id = self.data.profile.get_options( WaccaConstants.OPTIONS["set_title_id"], profile["user"] ) if set_icon_id is None: @@ -181,7 +181,7 @@ class WaccaBase: return resp.make() - async def handle_user_status_login_request(self, data: Dict) -> Dict: + def handle_user_status_login_request(self, data: Dict) -> Dict: req = UserStatusLoginRequest(data) resp = UserStatusLoginResponseV1() is_consec_day = True @@ -191,7 +191,7 @@ class WaccaBase: resp.lastLoginDate = 0 else: - profile = await self.data.profile.get_profile(req.userId) + profile = self.data.profile.get_profile(req.userId) if profile is None: self.logger.warning( f"Unknown user id {req.userId} attempted login from {req.chipId}" @@ -215,7 +215,7 @@ class WaccaBase: if midnight_today_ts - last_login_time > 86400: is_consec_day = False - await self.data.profile.session_login( + self.data.profile.session_login( req.userId, resp.firstLoginDaily, is_consec_day ) @@ -227,64 +227,52 @@ class WaccaBase: return resp.make() - async def handle_user_status_create_request(self, data: Dict) -> Dict: + def handle_user_status_create_request(self, data: Dict) -> Dict: req = UserStatusCreateRequest(data) - profileId = await self.data.profile.create_profile( + profileId = self.data.profile.create_profile( req.aimeId, req.username, self.version ) if profileId is None: return BaseResponse().make() - + if profileId == 0: # We've already made this profile, just return success - new_user = await self.data.profile.get_profile(aime_id=req.aimeId) - profileId = new_user["id"] + new_user = self.data.profile.get_profile(aime_id=req.aimeId) + profileId = new_user['id'] # Insert starting items - await self.data.item.put_item( - req.aimeId, WaccaConstants.ITEM_TYPES["title"], 104001 - ) - await self.data.item.put_item( - req.aimeId, WaccaConstants.ITEM_TYPES["title"], 104002 - ) - await self.data.item.put_item( - req.aimeId, WaccaConstants.ITEM_TYPES["title"], 104003 - ) - await self.data.item.put_item( - req.aimeId, WaccaConstants.ITEM_TYPES["title"], 104005 - ) + 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) - await self.data.item.put_item( - req.aimeId, WaccaConstants.ITEM_TYPES["icon"], 102001 - ) - await self.data.item.put_item( - req.aimeId, WaccaConstants.ITEM_TYPES["icon"], 102002 - ) + self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["icon"], 102001) + self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["icon"], 102002) - await self.data.item.put_item( + self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["note_color"], 103001 ) - await self.data.item.put_item( + self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["note_color"], 203001 ) - await self.data.item.put_item( + self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["note_sound"], 105001 ) - await self.data.item.put_item( + self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210001 ) return UserStatusCreateResponseV2(profileId, req.username).make() - async def handle_user_status_getDetail_request(self, data: Dict) -> Dict: + def handle_user_status_getDetail_request(self, data: Dict) -> Dict: req = UserStatusGetDetailRequest(data) resp = UserStatusGetDetailResponseV1() - profile = await self.data.profile.get_profile(req.userId) + profile = self.data.profile.get_profile(req.userId) if profile is None: self.logger.warning(f"Unknown profile {req.userId}") return resp.make() @@ -292,12 +280,12 @@ class WaccaBase: self.logger.info(f"Get detail for profile {req.userId}") user_id = profile["user"] - profile_scores = await self.data.score.get_best_scores(user_id) - profile_items = await self.data.item.get_items(user_id) - profile_song_unlocks = await self.data.item.get_song_unlocks(user_id) - profile_options = await self.data.profile.get_options(user_id) - profile_trophies = await self.data.item.get_trophies(user_id) - profile_tickets = await self.data.item.get_tickets(user_id) + profile_scores = self.data.score.get_best_scores(user_id) + profile_items = self.data.item.get_items(user_id) + profile_song_unlocks = self.data.item.get_song_unlocks(user_id) + profile_options = self.data.profile.get_options(user_id) + profile_trophies = self.data.item.get_trophies(user_id) + profile_tickets = self.data.item.get_tickets(user_id) resp.songUpdateTime = int(profile["last_login_date"].timestamp()) resp.songPlayStatus = [profile["last_song_id"], 1] @@ -445,11 +433,11 @@ class WaccaBase: return resp.make() - async def handle_user_trial_get_request(self, data: Dict) -> Dict: + def handle_user_trial_get_request(self, data: Dict) -> Dict: req = UserTrialGetRequest(data) resp = UserTrialGetResponse() - user_id = await self.data.profile.profile_to_aime_user(req.profileId) + 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}" @@ -457,7 +445,7 @@ class WaccaBase: return resp.make() self.logger.info(f"Get trial info for user {req.profileId}") - stages = await self.data.score.get_stageup(user_id, self.version) + stages = self.data.score.get_stageup(user_id, self.version) if stages is None: stages = [] @@ -487,7 +475,7 @@ class WaccaBase: return resp.make() - async def handle_user_trial_update_request(self, data: Dict) -> Dict: + def handle_user_trial_update_request(self, data: Dict) -> Dict: req = UserTrialUpdateRequest(data) total_score = 0 @@ -497,15 +485,15 @@ class WaccaBase: while len(req.songScores) < 3: req.songScores.append(0) - profile = await self.data.profile.get_profile(req.profileId) + profile = self.data.profile.get_profile(req.profileId) user_id = profile["user"] - old_stage = await self.data.score.get_stageup_stage( + old_stage = self.data.score.get_stageup_stage( user_id, self.version, req.stageId ) if old_stage is None: - await self.data.score.put_stageup( + self.data.score.put_stageup( user_id, self.version, req.stageId, @@ -531,7 +519,7 @@ class WaccaBase: best_score2 = old_stage["song2_score"] best_score3 = old_stage["song3_score"] - await self.data.score.put_stageup( + self.data.score.put_stageup( user_id, self.version, req.stageId, @@ -549,17 +537,17 @@ class WaccaBase: req.stageLevel == profile["dan_level"] and req.clearType.value > profile["dan_type"] ): - await self.data.profile.update_profile_dan( + self.data.profile.update_profile_dan( req.profileId, req.stageLevel, req.clearType.value ) - await self.util_put_items(req.profileId, user_id, req.itemsObtained) + 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 = await self.data.profile.get_options( + current_icon = self.data.profile.get_options( user_id, WaccaConstants.OPTIONS["set_icon_id"] ) - current_nav = await self.data.profile.get_options( + current_nav = self.data.profile.get_options( user_id, WaccaConstants.OPTIONS["set_nav_id"] ) @@ -572,18 +560,18 @@ class WaccaBase: else: current_nav = current_nav["value"] - await self.data.item.put_item( + self.data.item.put_item( user_id, WaccaConstants.ITEM_TYPES["icon"], current_icon ) - await self.data.item.put_item( + self.data.item.put_item( user_id, WaccaConstants.ITEM_TYPES["navigator"], current_nav ) - await self.data.profile.update_profile_playtype( + self.data.profile.update_profile_playtype( req.profileId, 4, data["appVersion"][:7] ) return BaseResponse().make() - async def handle_user_sugoroku_update_request(self, data: Dict) -> Dict: + def handle_user_sugoroku_update_request(self, data: Dict) -> Dict: ver_split = data["appVersion"].split(".") resp = BaseResponse() @@ -595,16 +583,16 @@ class WaccaBase: req = UserSugarokuUpdateRequestV2(data) mission_flg = req.mission_flag - user_id = await self.data.profile.profile_to_aime_user(req.profileId) + 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}" ) return resp.make() - await self.util_put_items(req.profileId, user_id, req.itemsObtainted) + self.util_put_items(req.profileId, user_id, req.itemsObtainted) - await self.data.profile.update_gate( + self.data.profile.update_gate( user_id, req.gateId, req.page, @@ -615,25 +603,23 @@ class WaccaBase: ) return resp.make() - async def handle_user_info_getMyroom_request(self, data: Dict) -> Dict: + def handle_user_info_getMyroom_request(self, data: Dict) -> Dict: return UserInfogetMyroomResponseV1().make() - async def handle_user_music_unlock_request(self, data: Dict) -> Dict: + def handle_user_music_unlock_request(self, data: Dict) -> Dict: req = UserMusicUnlockRequest(data) - profile = await self.data.profile.get_profile(req.profileId) + profile = self.data.profile.get_profile(req.profileId) if profile is None: return BaseResponse().make() user_id = profile["user"] current_wp = profile["wp"] - tickets = await self.data.item.get_tickets(user_id) + tickets = self.data.item.get_tickets(user_id) new_tickets: List[TicketItem] = [] for ticket in tickets: - new_tickets.append( - TicketItem(ticket["id"], ticket["ticket_id"], 9999999999) - ) + new_tickets.append(TicketItem(ticket["id"], ticket["ticket_id"], 9999999999)) for item in req.itemsUsed: if ( @@ -642,7 +628,7 @@ class WaccaBase: ): if current_wp >= item.quantity: current_wp -= item.quantity - await self.data.profile.spend_wp(req.profileId, item.quantity) + self.data.profile.spend_wp(req.profileId, item.quantity) else: return BaseResponse().make() @@ -655,21 +641,21 @@ class WaccaBase: self.logger.debug( f"Remove ticket ID {new_tickets[x].userTicketId} type {new_tickets[x].ticketId} from {user_id}" ) - await self.data.item.spend_ticket(new_tickets[x].userTicketId) + 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 = await self.data.score.get_best_score( + old_score = self.data.score.get_best_score( user_id, req.songId, req.difficulty ) if not old_score: - await self.data.score.put_best_score( + self.data.score.put_best_score( user_id, req.songId, req.difficulty, 0, [0] * 5, [0] * 13, 0, 0 ) - await self.data.item.unlock_song( + self.data.item.unlock_song( user_id, req.songId, req.difficulty @@ -686,12 +672,12 @@ class WaccaBase: return UserMusicUnlockResponse(current_wp, new_tickets).make() - async def handle_user_info_getRanking_request(self, data: Dict) -> Dict: + 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() - async def handle_user_music_update_request(self, data: Dict) -> Dict: + def handle_user_music_update_request(self, data: Dict) -> Dict: ver_split = data["appVersion"].split(".") if int(ver_split[0]) >= 3: resp = UserMusicUpdateResponseV3() @@ -712,7 +698,7 @@ class WaccaBase: ) return resp.make() - profile = await self.data.profile.get_profile(req.profileId) + profile = self.data.profile.get_profile(req.profileId) if profile is None: self.logger.warning( @@ -721,7 +707,7 @@ class WaccaBase: return resp.make() user_id = profile["user"] - await self.util_put_items(req.profileId, user_id, req.itemsObtained) + self.util_put_items(req.profileId, user_id, req.itemsObtained) playlog_clear_status = ( req.songDetail.flagCleared @@ -730,7 +716,7 @@ class WaccaBase: + req.songDetail.flagAllMarvelous ) - await self.data.score.put_playlog( + self.data.score.put_playlog( user_id, req.songDetail.songId, req.songDetail.difficulty, @@ -747,7 +733,7 @@ class WaccaBase: self.season, ) - old_score = await self.data.score.get_best_score( + old_score = self.data.score.get_best_score( user_id, req.songDetail.songId, req.songDetail.difficulty ) @@ -763,7 +749,7 @@ class WaccaBase: grades[req.songDetail.grade.value - 1] = 1 - await self.data.score.put_best_score( + self.data.score.put_best_score( user_id, req.songDetail.songId, req.songDetail.difficulty, @@ -819,7 +805,7 @@ class WaccaBase: old_score["rating"], ) - await self.data.score.put_best_score( + self.data.score.put_best_score( user_id, req.songDetail.songId, req.songDetail.difficulty, @@ -845,40 +831,40 @@ class WaccaBase: return resp.make() # TODO: Coop and vs data - async def handle_user_music_updateCoop_request(self, data: Dict) -> Dict: + def handle_user_music_updateCoop_request(self, data: Dict) -> Dict: coop_info = data["params"][4] return self.handle_user_music_update_request(data) - async def handle_user_music_updateVersus_request(self, data: Dict) -> Dict: + def handle_user_music_updateVersus_request(self, data: Dict) -> Dict: vs_info = data["params"][4] return self.handle_user_music_update_request(data) - async def handle_user_music_updateTrial_request(self, data: Dict) -> Dict: + def handle_user_music_updateTrial_request(self, data: Dict) -> Dict: return self.handle_user_music_update_request(data) - async def handle_user_mission_update_request(self, data: Dict) -> Dict: + def handle_user_mission_update_request(self, data: Dict) -> Dict: req = UserMissionUpdateRequest(data) page_status = req.params[1][1] - profile = await self.data.profile.get_profile(req.profileId) + profile = self.data.profile.get_profile(req.profileId) if profile is None: return BaseResponse().make() if len(req.itemsObtained) > 0: - await self.util_put_items(req.profileId, profile["user"], req.itemsObtained) + self.util_put_items(req.profileId, profile["user"], req.itemsObtained) - await self.data.profile.update_bingo( + self.data.profile.update_bingo( profile["user"], req.bingoDetail.pageNumber, page_status ) - await self.data.profile.update_tutorial_flags(req.profileId, req.params[3]) + self.data.profile.update_tutorial_flags(req.profileId, req.params[3]) return BaseResponse().make() - async def handle_user_goods_purchase_request(self, data: Dict) -> Dict: + def handle_user_goods_purchase_request(self, data: Dict) -> Dict: req = UserGoodsPurchaseRequest(data) resp = UserGoodsPurchaseResponse() - profile = await self.data.profile.get_profile(req.profileId) + profile = self.data.profile.get_profile(req.profileId) if profile is None: return BaseResponse().make() @@ -890,20 +876,20 @@ class WaccaBase: and not self.game_config.mods.infinite_wp ): resp.currentWp -= req.cost - await self.data.profile.spend_wp(req.profileId, 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}" ) - await self.util_put_items(req.profileId, user_id, [req.itemObtained]) + 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 = await self.data.item.get_tickets(user_id) + tickets = self.data.item.get_tickets(user_id) for ticket in tickets: resp.tickets.append( @@ -919,16 +905,16 @@ class WaccaBase: return resp.make() - async def handle_competition_status_login_request(self, data: Dict) -> Dict: + def handle_competition_status_login_request(self, data: Dict) -> Dict: return BaseResponse().make() - async def handle_competition_status_update_request(self, data: Dict) -> Dict: + def handle_competition_status_update_request(self, data: Dict) -> Dict: return BaseResponse().make() - async def handle_user_rating_update_request(self, data: Dict) -> Dict: + def handle_user_rating_update_request(self, data: Dict) -> Dict: req = UserRatingUpdateRequest(data) - user_id = await self.data.profile.profile_to_aime_user(req.profileId) + user_id = self.data.profile.profile_to_aime_user(req.profileId) if user_id is None: self.logger.error( @@ -937,33 +923,33 @@ class WaccaBase: return BaseResponse().make() for song in req.songs: - await self.data.score.update_song_rating( + self.data.score.update_song_rating( user_id, song.songId, song.difficulty, song.rating ) - await self.data.profile.update_user_rating(req.profileId, req.totalRating) + self.data.profile.update_user_rating(req.profileId, req.totalRating) return BaseResponse().make() - async def handle_user_status_update_request(self, data: Dict) -> Dict: + def handle_user_status_update_request(self, data: Dict) -> Dict: req = UserStatusUpdateRequestV1(data) - user_id = await self.data.profile.profile_to_aime_user(req.profileId) + 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}" ) return BaseResponse().make() - await self.util_put_items(req.profileId, user_id, req.itemsRecieved) - await self.data.profile.update_profile_playtype( + self.util_put_items(req.profileId, user_id, req.itemsRecieved) + self.data.profile.update_profile_playtype( req.profileId, req.playType.value, data["appVersion"][:7] ) - current_icon = await self.data.profile.get_options( + current_icon = self.data.profile.get_options( user_id, WaccaConstants.OPTIONS["set_icon_id"] ) - current_nav = await self.data.profile.get_options( + current_nav = self.data.profile.get_options( user_id, WaccaConstants.OPTIONS["set_nav_id"] ) @@ -976,38 +962,38 @@ class WaccaBase: else: current_nav = current_nav["value"] - await self.data.item.put_item( + self.data.item.put_item( user_id, WaccaConstants.ITEM_TYPES["icon"], current_icon ) - await self.data.item.put_item( + self.data.item.put_item( user_id, WaccaConstants.ITEM_TYPES["navigator"], current_nav ) return BaseResponse().make() - async def handle_user_info_update_request(self, data: Dict) -> Dict: + def handle_user_info_update_request(self, data: Dict) -> Dict: req = UserInfoUpdateRequest(data) - user_id = await self.data.profile.profile_to_aime_user(req.profileId) + user_id = self.data.profile.profile_to_aime_user(req.profileId) for opt in req.optsUpdated: - await self.data.profile.update_option(user_id, opt.optId, opt.optVal) + self.data.profile.update_option(user_id, opt.optId, opt.optVal) for update in req.datesUpdated: pass for fav in req.favoritesAdded: - await self.data.profile.add_favorite_song(user_id, fav) + self.data.profile.add_favorite_song(user_id, fav) for unfav in req.favoritesRemoved: - await self.data.profile.remove_favorite_song(user_id, unfav) + self.data.profile.remove_favorite_song(user_id, unfav) return BaseResponse().make() - async def handle_user_vip_get_request(self, data: Dict) -> Dict: + def handle_user_vip_get_request(self, data: Dict) -> Dict: req = UserVipGetRequest(data) resp = UserVipGetResponse() - profile = await self.data.profile.get_profile(req.profileId) + profile = self.data.profile.get_profile(req.profileId) if profile is None: self.logger.warning( f"handle_user_vip_get_request no profile with ID {req.profileId}" @@ -1035,10 +1021,10 @@ class WaccaBase: return resp.make() - async def handle_user_vip_start_request(self, data: Dict) -> Dict: + def handle_user_vip_start_request(self, data: Dict) -> Dict: req = UserVipStartRequest(data) - profile = await self.data.profile.get_profile(req.profileId) + profile = self.data.profile.get_profile(req.profileId) if profile is None: return BaseResponse().make() @@ -1054,10 +1040,10 @@ class WaccaBase: ).make() vip_exp_time = self.srvtime + timedelta(days=req.days) - await self.data.profile.update_vip_time(req.profileId, vip_exp_time) + self.data.profile.update_vip_time(req.profileId, vip_exp_time) return UserVipStartResponse(int(vip_exp_time.timestamp())).make() - async def util_put_items( + 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: @@ -1066,10 +1052,10 @@ class WaccaBase: if items_obtained: for item in items_obtained: if item.itemType == WaccaConstants.ITEM_TYPES["xp"]: - await self.data.profile.add_xp(profile_id, item.quantity) + self.data.profile.add_xp(profile_id, item.quantity) elif item.itemType == WaccaConstants.ITEM_TYPES["wp"]: - await self.data.profile.add_wp(profile_id, item.quantity) + self.data.profile.add_wp(profile_id, item.quantity) elif ( item.itemType @@ -1077,11 +1063,11 @@ class WaccaBase: or item.itemType == WaccaConstants.ITEM_TYPES["music_unlock"] ): if item.quantity > WaccaConstants.Difficulty.HARD.value: - old_score = await self.data.score.get_best_score( + old_score = self.data.score.get_best_score( user_id, item.itemId, item.quantity ) if not old_score: - await self.data.score.put_best_score( + self.data.score.put_best_score( user_id, item.itemId, item.quantity, @@ -1094,20 +1080,18 @@ class WaccaBase: if item.quantity == 0: item.quantity = WaccaConstants.Difficulty.HARD.value - await self.data.item.unlock_song( - user_id, item.itemId, item.quantity - ) + self.data.item.unlock_song(user_id, item.itemId, item.quantity) elif item.itemType == WaccaConstants.ITEM_TYPES["ticket"]: - await self.data.item.add_ticket(user_id, item.itemId) + self.data.item.add_ticket(user_id, item.itemId) elif item.itemType == WaccaConstants.ITEM_TYPES["trophy"]: - await self.data.item.update_trophy( + self.data.item.update_trophy( user_id, item.itemId, self.season, item.quantity, 0 ) else: - await self.data.item.put_item(user_id, item.itemType, item.itemId) + self.data.item.put_item(user_id, item.itemType, item.itemId) def util_calc_song_rating(self, score: int, difficulty: float) -> int: if score >= 990000: diff --git a/titles/wacca/config.py b/titles/wacca/config.py index 2e1f9b0..e96c3f4 100644 --- a/titles/wacca/config.py +++ b/titles/wacca/config.py @@ -1,5 +1,4 @@ -from typing import List - +from typing import Dict, List from core.config import CoreConfig diff --git a/titles/wacca/const.py b/titles/wacca/const.py index 4be113c..b25d3ac 100644 --- a/titles/wacca/const.py +++ b/titles/wacca/const.py @@ -1,6 +1,8 @@ from enum import Enum from typing import Optional +from core.const import AllnetJapanRegionId + class WaccaConstants: CONFIG_NAME = "wacca.yaml" diff --git a/titles/wacca/database.py b/titles/wacca/database.py index b5569cb..133e22f 100644 --- a/titles/wacca/database.py +++ b/titles/wacca/database.py @@ -1,5 +1,5 @@ -from core.config import CoreConfig from core.data import Data +from core.config import CoreConfig from titles.wacca.schema import * diff --git a/titles/wacca/frontend.py b/titles/wacca/frontend.py index 61a774b..cc40644 100644 --- a/titles/wacca/frontend.py +++ b/titles/wacca/frontend.py @@ -1,16 +1,14 @@ -from os import path -from typing import List - -import jinja2 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 core.frontend import FE_Base, UserSession -from starlette.requests import Request -from starlette.responses import Response -from starlette.routing import Route +from titles.wacca.database import WaccaData from titles.wacca.config import WaccaConfig from titles.wacca.const import WaccaConstants -from titles.wacca.database import WaccaData class WaccaFrontend(FE_Base): @@ -26,22 +24,15 @@ class WaccaFrontend(FE_Base): ) self.nav_name = "Wacca" - def get_routes(self) -> List[Route]: - return [Route("/", self.render_GET, methods=["GET"])] - - async def render_GET(self, request: Request) -> bytes: + def render_GET(self, request: Request) -> bytes: template = self.environment.get_template( - "titles/wacca/templates/wacca_index.jinja" - ) - usr_sesh = self.validate_session(request) - if not usr_sesh: - usr_sesh = UserSession() - - return Response( - template.render( - title=f"{self.core_config.server.name} | {self.nav_name}", - game_list=self.environment.globals["game_list"], - sesh=vars(usr_sesh), - ), - media_type="text/html; charset=utf-8", + "titles/wacca/frontend/wacca_index.jinja" ) + sesh: Session = request.getSession() + usr_sesh = IUserSession(sesh) + + return template.render( + title=f"{self.core_config.server.name} | {self.nav_name}", + game_list=self.environment.globals["game_list"], + sesh=vars(usr_sesh) + ).encode("utf-16") diff --git a/titles/wacca/handlers/__init__.py b/titles/wacca/handlers/__init__.py index f9223ec..f084682 100644 --- a/titles/wacca/handlers/__init__.py +++ b/titles/wacca/handlers/__init__.py @@ -1,5 +1,5 @@ -from titles.wacca.handlers.advertise import * from titles.wacca.handlers.base import * +from titles.wacca.handlers.advertise import * from titles.wacca.handlers.housing import * from titles.wacca.handlers.user_info import * from titles.wacca.handlers.user_misc import * diff --git a/titles/wacca/handlers/advertise.py b/titles/wacca/handlers/advertise.py index b321068..47c8406 100644 --- a/titles/wacca/handlers/advertise.py +++ b/titles/wacca/handlers/advertise.py @@ -1,6 +1,6 @@ -from typing import Dict, List +from typing import List, Dict -from titles.wacca.handlers.base import BaseRequest, BaseResponse +from titles.wacca.handlers.base import BaseResponse, BaseRequest from titles.wacca.handlers.helpers import Notice diff --git a/titles/wacca/handlers/base.py b/titles/wacca/handlers/base.py index bdcdf78..abfed5f 100644 --- a/titles/wacca/handlers/base.py +++ b/titles/wacca/handlers/base.py @@ -1,7 +1,6 @@ -from datetime import datetime from typing import Dict, List - from titles.wacca.handlers.helpers import Version +from datetime import datetime class BaseRequest: diff --git a/titles/wacca/handlers/helpers.py b/titles/wacca/handlers/helpers.py index e24b39b..354bdaf 100644 --- a/titles/wacca/handlers/helpers.py +++ b/titles/wacca/handlers/helpers.py @@ -1,5 +1,5 @@ +from typing import List, Optional, Any from enum import Enum -from typing import Any, List, Optional from titles.wacca.const import WaccaConstants @@ -96,7 +96,7 @@ class Version(ShortVersion): self.role = role def __str__(self) -> str: - return f"{self.major}.{self.minor:02}.{self.patch:02}.{self.country}.{self.build:05}.{self.role}" + return f"{self.major}.{self.minor}.{self.patch}.{self.country}.{self.role}.{self.build}" class HousingInfo: diff --git a/titles/wacca/handlers/housing.py b/titles/wacca/handlers/housing.py index 1a6bef2..e11c61a 100644 --- a/titles/wacca/handlers/housing.py +++ b/titles/wacca/handlers/housing.py @@ -1,8 +1,8 @@ -from typing import Dict, List +from typing import List, Dict -from titles.wacca.const import WaccaConstants from titles.wacca.handlers.base import BaseRequest, BaseResponse from titles.wacca.handlers.helpers import HousingInfo +from titles.wacca.const import WaccaConstants # ---housing/get---- diff --git a/titles/wacca/handlers/user_info.py b/titles/wacca/handlers/user_info.py index 900e4ff..b70ac35 100644 --- a/titles/wacca/handlers/user_info.py +++ b/titles/wacca/handlers/user_info.py @@ -1,7 +1,7 @@ -from typing import Dict, List +from typing import List, Dict from titles.wacca.handlers.base import BaseRequest, BaseResponse -from titles.wacca.handlers.helpers import DateUpdate, UserOption +from titles.wacca.handlers.helpers import UserOption, DateUpdate # ---user/info/update--- diff --git a/titles/wacca/handlers/user_misc.py b/titles/wacca/handlers/user_misc.py index 4f6850a..eb03802 100644 --- a/titles/wacca/handlers/user_misc.py +++ b/titles/wacca/handlers/user_misc.py @@ -1,15 +1,9 @@ -from typing import Dict, List +from typing import List, Dict from titles.wacca.handlers.base import BaseRequest, BaseResponse -from titles.wacca.handlers.helpers import ( - BingoDetail, - BingoPageStatus, - GateTutorialFlag, - GenericItemRecv, - PurchaseType, - SongRatingUpdate, - TicketItem, -) +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--- diff --git a/titles/wacca/handlers/user_music.py b/titles/wacca/handlers/user_music.py index 1e2cd23..26c2167 100644 --- a/titles/wacca/handlers/user_music.py +++ b/titles/wacca/handlers/user_music.py @@ -1,17 +1,18 @@ -from typing import Dict, List +from typing import List, Dict from titles.wacca.handlers.base import BaseRequest, BaseResponse from titles.wacca.handlers.helpers import ( GenericItemRecv, - MusicUpdateDetailV1, - MusicUpdateDetailV2, - MusicUpdateDetailV3, - SeasonalInfoV1, - SeasonalInfoV2, - SongUpdateDetailV1, 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--- @@ -92,9 +93,7 @@ class UserMusicUnlockRequest(BaseRequest): class UserMusicUnlockResponse(BaseResponse): - def __init__( - self, current_wp: int = 0, tickets_remaining: List[TicketItem] = [] - ) -> None: + def __init__(self, current_wp: int = 0, tickets_remaining: List[TicketItem] = []) -> None: super().__init__() self.wp = current_wp self.tickets = tickets_remaining diff --git a/titles/wacca/handlers/user_status.py b/titles/wacca/handlers/user_status.py index e2c50f2..6eef16a 100644 --- a/titles/wacca/handlers/user_status.py +++ b/titles/wacca/handlers/user_status.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Optional +from typing import List, Dict, Optional from titles.wacca.handlers.base import BaseRequest, BaseResponse from titles.wacca.handlers.helpers import * @@ -267,7 +267,9 @@ class UserStatusLoginResponseV3(UserStatusLoginResponseV2): 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...) + self.unk: List = ( + [] + ) # Ticket info, item info, message, title, voice name (not sure how they fit...) def make(self) -> Dict: super().make() diff --git a/titles/wacca/handlers/user_trial.py b/titles/wacca/handlers/user_trial.py index b8ad8b6..ba6ea50 100644 --- a/titles/wacca/handlers/user_trial.py +++ b/titles/wacca/handlers/user_trial.py @@ -1,7 +1,6 @@ from typing import Dict, List - from titles.wacca.handlers.base import BaseRequest, BaseResponse -from titles.wacca.handlers.helpers import GenericItemRecv, StageInfo, StageupClearType +from titles.wacca.handlers.helpers import StageInfo, StageupClearType, GenericItemRecv # --user/trial/get-- diff --git a/titles/wacca/handlers/user_vip.py b/titles/wacca/handlers/user_vip.py index 820f565..bc418b5 100644 --- a/titles/wacca/handlers/user_vip.py +++ b/titles/wacca/handlers/user_vip.py @@ -1,5 +1,4 @@ from typing import Dict, List - from titles.wacca.handlers.base import BaseRequest, BaseResponse from titles.wacca.handlers.helpers import VipLoginBonus diff --git a/titles/wacca/index.py b/titles/wacca/index.py index cbbeaee..90e4a0b 100644 --- a/titles/wacca/index.py +++ b/titles/wacca/index.py @@ -1,32 +1,29 @@ -import json -import logging -import sys -import traceback -from hashlib import md5 -from logging.handlers import TimedRotatingFileHandler -from os import path -from typing import Dict, List, Tuple - -import coloredlogs import yaml -from core import CoreConfig, Utils -from core.title import BaseServlet -from starlette.requests import Request -from starlette.responses import Response -from starlette.routing import Route +import logging, coloredlogs +from logging.handlers import TimedRotatingFileHandler +import logging +import json +from hashlib import md5 +from twisted.web.http import Request +from typing import Dict, Tuple, List +from os import path +import traceback +import sys -from .base import WaccaBase +from core import CoreConfig, Utils +from .config import WaccaConfig from .config import WaccaConfig from .const import WaccaConstants -from .handlers.base import BaseRequest, BaseResponse -from .handlers.helpers import Version -from .lily import WaccaLily -from .lilyr import WaccaLilyR 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(BaseServlet): +class WaccaServlet: def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: self.core_cfg = core_cfg self.game_cfg = WaccaConfig() @@ -66,24 +63,17 @@ class WaccaServlet(BaseServlet): level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str ) - def get_routes(self) -> List[Route]: - return [ - Route( - "/WaccaServlet/api/{api:str}/{endpoint:str}", - self.render_POST, - methods=["POST"], - ), - Route( - "/WaccaServlet/api/{api:str}/{branch:str}/{endpoint:str}", - self.render_POST, - methods=["POST"], - ), - ] + 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: + 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( @@ -94,37 +84,27 @@ class WaccaServlet(BaseServlet): return False return True - - 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 - ): + + 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.server.hostname}:{Utils.get_title_port(self.core_cfg)}/WaccaServlet", - self.core_cfg.server.hostname, + 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.server.hostname}/WaccaServlet", - self.core_cfg.server.hostname, - ) - - async def render_POST(self, request: Request) -> bytes: + 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() - j_Resp = Response(json.dumps(resp, ensure_ascii=False)) - j_Resp.raw_headers.append((b"X-Wacca-Hash", hash.hex().encode())) - return j_Resp + request.responseHeaders.addRawHeader(b"X-Wacca-Hash", hash.hex().encode()) + return json.dumps(resp).encode() - api = request.path_params.get("api", "") - branch = request.path_params.get("branch", "") - endpoint = request.path_params.get("endpoint", "") + api = matchers['api'] + branch = matchers.get('branch', '') + endpoint = matchers['endpoint'] client_ip = Utils.get_ip_addr(request) - bod = await request.body() - + if branch: url_path = f"{api}/{branch}/{endpoint}" func_to_find = f"handle_{api}_{branch}_{endpoint}_request" @@ -134,21 +114,23 @@ class WaccaServlet(BaseServlet): func_to_find = f"handle_{api}_{endpoint}_request" try: - req_json = json.loads(bod) + req_json = json.loads(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 {bod} -> Missing required value {e}" + 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} -> {bod} -> {e}") + self.logger.error( + f"Failed to parse request to {url_path} -> {request.content.getvalue()} -> {e}" + ) resp = BaseResponse() resp.status = 1 resp.message = "不正なリクエスト エラーです" @@ -194,7 +176,7 @@ class WaccaServlet(BaseServlet): try: handler = getattr(self.versions[internal_ver], func_to_find) - resp = await handler(req_json) + resp = handler(req_json) self.logger.debug(f"{req.appVersion} response {resp}") return end(resp) @@ -204,11 +186,9 @@ class WaccaServlet(BaseServlet): f"{req.appVersion} Error handling method {url_path} -> {e}" ) if self.logger.level == logging.DEBUG: - tp, val, tb = sys.exc_info() + 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: + 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() diff --git a/titles/wacca/lily.py b/titles/wacca/lily.py index da80e8c..33928ec 100644 --- a/titles/wacca/lily.py +++ b/titles/wacca/lily.py @@ -1,11 +1,13 @@ +from typing import Any, List, Dict from datetime import datetime, timedelta -from typing import Dict +import json from core.config import CoreConfig +from titles.wacca.s import WaccaS from titles.wacca.config import WaccaConfig from titles.wacca.const import WaccaConstants + from titles.wacca.handlers import * -from titles.wacca.s import WaccaS class WaccaLily(WaccaS): @@ -35,44 +37,44 @@ class WaccaLily(WaccaS): (210003, 0), ] - async def handle_advertise_GetNews_request(self, data: Dict) -> Dict: + def handle_advertise_GetNews_request(self, data: Dict) -> Dict: resp = GetNewsResponseV3() return resp.make() - async def handle_user_status_create_request(self, data: Dict) -> Dict: + def handle_user_status_create_request(self, data: Dict) -> Dict: req = UserStatusCreateRequest(data) - ret = await super().handle_user_status_create_request(data) + ret = super().handle_user_status_create_request(data) - new_user = await self.data.profile.get_profile(aime_id=req.aimeId) + new_user = self.data.profile.get_profile(aime_id=req.aimeId) if new_user is None: return BaseResponse().make() - - await self.data.item.put_item( + + self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["user_plate"], 211001 ) # Added lily - - await self.data.item.put_item( + + self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["note_sound"], 205005 ) # Added lily - await self.data.item.put_item( + self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210002 ) # Lily, Added Lily return ret - async def handle_user_status_get_request(self, data: Dict) -> Dict: + def handle_user_status_get_request(self, data: Dict) -> Dict: req = UserStatusGetRequest(data) resp = UserStatusGetV2Response() - profile = await self.data.profile.get_profile(aime_id=req.aimeId) + 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() - opts = await self.data.profile.get_options(req.aimeId) + 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: @@ -92,14 +94,14 @@ class WaccaLily(WaccaS): resp.userStatus.loginsToday = profile["login_count_today"] resp.userStatus.rating = profile["rating"] - set_title_id = await self.data.profile.get_options( + 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 = await self.data.profile.get_options( + set_icon_id = self.data.profile.get_options( WaccaConstants.OPTIONS["set_title_id"], profile["user"] ) if set_icon_id is None: @@ -143,7 +145,7 @@ class WaccaLily(WaccaS): return resp.make() - async def handle_user_status_login_request(self, data: Dict) -> Dict: + def handle_user_status_login_request(self, data: Dict) -> Dict: req = UserStatusLoginRequest(data) resp = UserStatusLoginResponseV2() is_consec_day = True @@ -153,7 +155,7 @@ class WaccaLily(WaccaS): resp.lastLoginDate = 0 else: - profile = await self.data.profile.get_profile(req.userId) + profile = self.data.profile.get_profile(req.userId) if profile is None: self.logger.warning( f"Unknown user id {req.userId} attempted login from {req.chipId}" @@ -177,7 +179,7 @@ class WaccaLily(WaccaS): if midnight_today_ts - last_login_time > 86400: is_consec_day = False - await self.data.profile.session_login( + self.data.profile.session_login( req.userId, resp.firstLoginDaily, is_consec_day ) resp.vipInfo.pageYear = datetime.now().year @@ -187,14 +189,14 @@ class WaccaLily(WaccaS): return resp.make() - async def handle_user_status_getDetail_request(self, data: Dict) -> Dict: + def handle_user_status_getDetail_request(self, data: Dict) -> Dict: req = UserStatusGetDetailRequest(data) if req.appVersion.minor >= 53: resp = UserStatusGetDetailResponseV3() else: resp = UserStatusGetDetailResponseV2() - profile = await self.data.profile.get_profile(req.userId) + profile = self.data.profile.get_profile(req.userId) if profile is None: self.logger.warning(f"Unknown profile {req.userId}") return resp.make() @@ -202,14 +204,14 @@ class WaccaLily(WaccaS): self.logger.info(f"Get detail for profile {req.userId}") user_id = profile["user"] - profile_scores = await self.data.score.get_best_scores(user_id) - profile_items = await self.data.item.get_items(user_id) - profile_song_unlocks = await self.data.item.get_song_unlocks(user_id) - profile_options = await self.data.profile.get_options(user_id) - profile_favorites = await self.data.profile.get_favorite_songs(user_id) - profile_gates = await self.data.profile.get_gates(user_id) - profile_trophies = await self.data.item.get_trophies(user_id) - profile_tickets = await self.data.item.get_tickets(user_id) + profile_scores = self.data.score.get_best_scores(user_id) + profile_items = self.data.item.get_items(user_id) + profile_song_unlocks = self.data.item.get_song_unlocks(user_id) + profile_options = self.data.profile.get_options(user_id) + profile_favorites = self.data.profile.get_favorite_songs(user_id) + profile_gates = self.data.profile.get_gates(user_id) + 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 @@ -438,13 +440,13 @@ class WaccaLily(WaccaS): return resp.make() - async def handle_user_info_getMyroom_request(self, data: Dict) -> Dict: + def handle_user_info_getMyroom_request(self, data: Dict) -> Dict: return UserInfogetMyroomResponseV2().make() - async def handle_user_status_update_request(self, data: Dict) -> Dict: - await super().handle_user_status_update_request(data) + def handle_user_status_update_request(self, data: Dict) -> Dict: + super().handle_user_status_update_request(data) req = UserStatusUpdateRequestV2(data) - await self.data.profile.update_profile_lastplayed( + self.data.profile.update_profile_lastplayed( req.profileId, req.lastSongInfo.lastSongId, req.lastSongInfo.lastSongDiff, diff --git a/titles/wacca/lilyr.py b/titles/wacca/lilyr.py index f4eceea..7204761 100644 --- a/titles/wacca/lilyr.py +++ b/titles/wacca/lilyr.py @@ -1,12 +1,15 @@ -from typing import Dict +from typing import Any, List, Dict +from datetime import datetime, timedelta +import json from core.config import CoreConfig -from core.const import AllnetCountryCode +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 titles.wacca.handlers import Dict -from titles.wacca.lily import WaccaLily + +from core.const import AllnetCountryCode class WaccaLilyR(WaccaLily): @@ -36,13 +39,13 @@ class WaccaLilyR(WaccaLily): (210003, 0), ] - async def handle_housing_start_request(self, data: Dict) -> Dict: + def handle_housing_start_request(self, data: Dict) -> Dict: req = HousingStartRequestV2(data) allnet_region_id = None - - machine = await self.data.arcade.get_machine(req.chipId) + + machine = self.data.arcade.get_machine(req.chipId) if machine is not None: - arcade = await self.data.arcade.get_arcade(machine["arcade"]) + arcade = self.data.arcade.get_arcade(machine["arcade"]) allnet_region_id = arcade["region_id"] if req.appVersion.country == AllnetCountryCode.JAPAN.value: @@ -68,36 +71,36 @@ class WaccaLilyR(WaccaLily): resp = HousingStartResponseV1(region_id) return resp.make() - async def handle_user_status_create_request(self, data: Dict) -> Dict: + def handle_user_status_create_request(self, data: Dict) -> Dict: req = UserStatusCreateRequest(data) - resp = await super().handle_user_status_create_request(data) + resp = super().handle_user_status_create_request(data) - await self.data.item.put_item( + self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210054 ) # Added lily r - await self.data.item.put_item( + self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210055 ) # Added lily r - await self.data.item.put_item( + self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210056 ) # Added lily r - await self.data.item.put_item( + self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210057 ) # Added lily r - await self.data.item.put_item( + self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210058 ) # Added lily r - await self.data.item.put_item( + self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210059 ) # Added lily r - await self.data.item.put_item( + self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210060 ) # Added lily r - await self.data.item.put_item( + self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210061 ) # Added lily r return resp - async def handle_user_status_logout_request(self, data: Dict) -> Dict: + 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 b8041a9..8fe5b6a 100644 --- a/titles/wacca/read.py +++ b/titles/wacca/read.py @@ -1,12 +1,12 @@ -import json -from os import path from typing import Optional - import wacky -from core.config import CoreConfig +import json +from os import walk, path + from read import BaseReader -from titles.wacca.const import WaccaConstants +from core.config import CoreConfig from titles.wacca.database import WaccaData +from titles.wacca.const import WaccaConstants class WaccaReader(BaseReader): @@ -29,7 +29,7 @@ class WaccaReader(BaseReader): self.logger.error(f"Invalid wacca version {version}") exit(1) - async def read(self) -> None: + def read(self) -> None: if not ( path.exists(f"{self.bin_dir}/Table") and path.exists(f"{self.bin_dir}/Message") @@ -37,9 +37,9 @@ class WaccaReader(BaseReader): self.logger.error("Could not find Table or Message folder, nothing to read") return - await self.read_music(f"{self.bin_dir}/Table", "MusicParameterTable") + self.read_music(f"{self.bin_dir}/Table", "MusicParameterTable") - async def read_music(self, base_dir: str, table: str) -> None: + def read_music(self, base_dir: str, table: str) -> None: if not self.check_valid_pair(base_dir, table): self.logger.warning( f"Cannot find {table} uasset/uexp pair at {base_dir}, music will not be read" @@ -67,7 +67,7 @@ class WaccaReader(BaseReader): designer = wacca_data[str(key)]["NotesDesignerNormal"] if diff > 0: - await self.data.static.put_music( + self.data.static.put_music( self.version, song_id, 1, @@ -84,7 +84,7 @@ class WaccaReader(BaseReader): designer = wacca_data[str(key)]["NotesDesignerHard"] if diff > 0: - await self.data.static.put_music( + self.data.static.put_music( self.version, song_id, 2, @@ -101,7 +101,7 @@ class WaccaReader(BaseReader): designer = wacca_data[str(key)]["NotesDesignerExpert"] if diff > 0: - await self.data.static.put_music( + self.data.static.put_music( self.version, song_id, 3, @@ -118,7 +118,7 @@ class WaccaReader(BaseReader): designer = wacca_data[str(key)]["NotesDesignerInferno"] if diff > 0: - await self.data.static.put_music( + self.data.static.put_music( self.version, song_id, 4, diff --git a/titles/wacca/reverse.py b/titles/wacca/reverse.py index c57afe8..6206940 100644 --- a/titles/wacca/reverse.py +++ b/titles/wacca/reverse.py @@ -1,11 +1,13 @@ -from datetime import timedelta -from typing import Dict +from typing import Any, List, Dict +from datetime import datetime, timedelta +import json from core.config import CoreConfig +from titles.wacca.lilyr import WaccaLilyR from titles.wacca.config import WaccaConfig from titles.wacca.const import WaccaConstants + from titles.wacca.handlers import * -from titles.wacca.lilyr import WaccaLilyR class WaccaReverse(WaccaLilyR): @@ -45,16 +47,16 @@ class WaccaReverse(WaccaLilyR): (310006, 0), ] - async def handle_user_status_login_request(self, data: Dict) -> Dict: - resp = await super().handle_user_status_login_request(data) + def handle_user_status_login_request(self, data: Dict) -> Dict: + resp = super().handle_user_status_login_request(data) resp["params"].append([]) return resp - async def handle_user_status_getDetail_request(self, data: Dict) -> Dict: + def handle_user_status_getDetail_request(self, data: Dict) -> Dict: req = UserStatusGetDetailRequest(data) resp = UserStatusGetDetailResponseV4() - profile = await self.data.profile.get_profile(req.userId) + profile = self.data.profile.get_profile(req.userId) if profile is None: self.logger.warning(f"Unknown profile {req.userId}") return resp.make() @@ -62,15 +64,15 @@ class WaccaReverse(WaccaLilyR): self.logger.info(f"Get detail for profile {req.userId}") user_id = profile["user"] - profile_scores = await self.data.score.get_best_scores(user_id) - profile_items = await self.data.item.get_items(user_id) - profile_song_unlocks = await self.data.item.get_song_unlocks(user_id) - profile_options = await self.data.profile.get_options(user_id) - profile_favorites = await self.data.profile.get_favorite_songs(user_id) - profile_gates = await self.data.profile.get_gates(user_id) - profile_bingo = await self.data.profile.get_bingo(user_id) - profile_trophies = await self.data.item.get_trophies(user_id) - profile_tickets = await self.data.item.get_tickets(user_id) + profile_scores = self.data.score.get_best_scores(user_id) + profile_items = self.data.item.get_items(user_id) + profile_song_unlocks = self.data.item.get_song_unlocks(user_id) + profile_options = self.data.profile.get_options(user_id) + profile_favorites = self.data.profile.get_favorite_songs(user_id) + profile_gates = self.data.profile.get_gates(user_id) + profile_bingo = self.data.profile.get_bingo(user_id) + profile_trophies = self.data.item.get_trophies(user_id) + profile_tickets = self.data.item.get_tickets(user_id) if profile["gate_tutorial_flags"] is not None: for x in profile["gate_tutorial_flags"]: @@ -303,21 +305,21 @@ class WaccaReverse(WaccaLilyR): return resp.make() - async def handle_user_status_create_request(self, data: Dict) -> Dict: + def handle_user_status_create_request(self, data: Dict) -> Dict: req = UserStatusCreateRequest(data) - resp = await super().handle_user_status_create_request(data) + resp = super().handle_user_status_create_request(data) - await self.data.item.put_item( + self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 310001 ) # Added reverse - await self.data.item.put_item( + self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 310002 ) # Added reverse - - await self.data.item.put_item( + + self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["touch_effect"], 312000 ) # Added reverse - await self.data.item.put_item( + self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["touch_effect"], 312001 ) # Added reverse diff --git a/titles/wacca/s.py b/titles/wacca/s.py index 948092c..4b1e997 100644 --- a/titles/wacca/s.py +++ b/titles/wacca/s.py @@ -1,32 +1,36 @@ -from typing import Dict +from typing import Any, List, Dict +from datetime import datetime, timedelta +import json from core.config import CoreConfig from titles.wacca.base import WaccaBase from titles.wacca.config import WaccaConfig from titles.wacca.const import WaccaConstants + from titles.wacca.handlers import * class WaccaS(WaccaBase): + allowed_stages = [ + (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 - self.allowed_stages = [ - (1513, 13), - (1512, 12), - (1511, 11), - (1510, 10), - (1509, 9), - (1508, 8), - (1507, 7), - (1506, 6), - (1505, 5), - (1504, 4), - (1503, 3), - (1502, 2), - (1501, 1), - ] - async def handle_advertise_GetNews_request(self, data: Dict) -> Dict: + 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 257837a..2ccb661 100644 --- a/titles/wacca/schema/__init__.py +++ b/titles/wacca/schema/__init__.py @@ -1,6 +1,6 @@ -from titles.wacca.schema.item import WaccaItemData from titles.wacca.schema.profile import WaccaProfileData 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"] diff --git a/titles/wacca/schema/item.py b/titles/wacca/schema/item.py index be8f37a..df3380a 100644 --- a/titles/wacca/schema/item.py +++ b/titles/wacca/schema/item.py @@ -1,12 +1,12 @@ -from typing import List, Optional +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean +from sqlalchemy.schema import ForeignKey +from sqlalchemy.sql import func, select, update, delete +from sqlalchemy.engine import Row +from sqlalchemy.dialects.mysql import insert from core.data.schema import BaseData, metadata -from sqlalchemy import Column, Table, UniqueConstraint, and_, case -from sqlalchemy.dialects.mysql import insert -from sqlalchemy.engine import Row -from sqlalchemy.schema import ForeignKey -from sqlalchemy.sql import delete, func, select -from sqlalchemy.types import TIMESTAMP, Integer item = Table( "wacca_item", @@ -75,18 +75,16 @@ trophy = Table( class WaccaItemData(BaseData): - async def get_song_unlocks(self, user_id: int) -> Optional[List[Row]]: + def get_song_unlocks(self, user_id: int) -> Optional[List[Row]]: sql = song_unlock.select(song_unlock.c.user == user_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def unlock_song( - self, user_id: int, song_id: int, difficulty: int - ) -> Optional[int]: + 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 ) @@ -101,7 +99,7 @@ class WaccaItemData(BaseData): ) ) - result = await self.execute(conflict) + 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}" @@ -110,9 +108,7 @@ class WaccaItemData(BaseData): return result.lastrowid - async def put_item( - self, user_id: int, item_type: int, item_id: int - ) -> Optional[int]: + 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, @@ -121,7 +117,7 @@ class WaccaItemData(BaseData): conflict = sql.on_duplicate_key_update(use_count=item.c.use_count + 1) - result = await self.execute(conflict) + 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}" @@ -130,7 +126,7 @@ class WaccaItemData(BaseData): return result.lastrowid - async def get_items( + def get_items( self, user_id: int, item_type: int = None, item_id: int = None ) -> Optional[List[Row]]: """ @@ -144,23 +140,23 @@ class WaccaItemData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_tickets(self, user_id: int) -> Optional[List[Row]]: + def get_tickets(self, user_id: int) -> Optional[List[Row]]: sql = select(ticket).where(ticket.c.user == user_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def add_ticket(self, user_id: int, ticket_id: int) -> None: + def add_ticket(self, user_id: int, ticket_id: int) -> None: sql = insert(ticket).values(user=user_id, ticket_id=ticket_id) - result = await self.execute(sql) + 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}" @@ -168,17 +164,15 @@ class WaccaItemData(BaseData): return None return result.lastrowid - async def spend_ticket(self, id: int) -> None: + def spend_ticket(self, id: int) -> None: sql = delete(ticket).where(ticket.c.id == id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: self.logger.warning(f"Failed to delete ticket id {id}") return None - async 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: @@ -186,12 +180,12 @@ class WaccaItemData(BaseData): and_(trophy.c.user == user_id, trophy.c.season == season) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def update_trophy( + def update_trophy( self, user_id: int, trophy_id: int, season: int, progress: int, badge_type: int ) -> Optional[int]: sql = insert(trophy).values( @@ -204,7 +198,7 @@ class WaccaItemData(BaseData): conflict = sql.on_duplicate_key_update(progress=progress) - result = await self.execute(conflict) + 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}" diff --git a/titles/wacca/schema/profile.py b/titles/wacca/schema/profile.py index 6c32d13..ff13782 100644 --- a/titles/wacca/schema/profile.py +++ b/titles/wacca/schema/profile.py @@ -1,13 +1,12 @@ -from typing import Dict, List, Optional - -from core.data.schema import BaseData, metadata -from sqlalchemy import Column, PrimaryKeyConstraint, Table, UniqueConstraint, and_ -from sqlalchemy.dialects.mysql import insert -from sqlalchemy.engine import Row +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON from sqlalchemy.schema import ForeignKey from sqlalchemy.sql import func, select -from sqlalchemy.types import JSON, TIMESTAMP, Boolean, Integer, String +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( @@ -140,7 +139,7 @@ gate = Table( class WaccaProfileData(BaseData): - async def create_profile( + def create_profile( self, aime_id: int, username: str, version: int ) -> Optional[int]: """ @@ -150,7 +149,7 @@ class WaccaProfileData(BaseData): conflict = sql.on_duplicate_key_update(username=sql.inserted.username) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.error( f"{__name__} Failed to insert wacca profile! aime id: {aime_id} username: {username}" @@ -158,7 +157,7 @@ class WaccaProfileData(BaseData): return None return result.lastrowid - async def update_profile_playtype( + def update_profile_playtype( self, profile_id: int, play_type: int, game_version: str ) -> None: sql = profile.update(profile.c.id == profile_id).values( @@ -180,14 +179,14 @@ class WaccaProfileData(BaseData): last_game_ver=game_version, ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: self.logger.error( f"update_profile: failed to update profile! profile: {profile_id}" ) return None - async def update_profile_lastplayed( + def update_profile_lastplayed( self, profile_id: int, last_song_id: int, @@ -203,21 +202,21 @@ class WaccaProfileData(BaseData): last_folder_id=last_folder_id, last_song_order=last_song_order, ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: self.logger.error( f"update_profile_lastplayed: failed to update profile! profile: {profile_id}" ) return None - async def update_profile_dan( + 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 = await self.execute(sql) + result = self.execute(sql) if result is None: self.logger.warning( f"update_profile_dan: Failed to update! profile {profile_id}" @@ -225,9 +224,7 @@ class WaccaProfileData(BaseData): return None return result.lastrowid - async def get_profile( - self, profile_id: int = 0, aime_id: int = None - ) -> Optional[Row]: + def get_profile(self, profile_id: int = 0, aime_id: int = None) -> Optional[Row]: """ Given a game version and either a profile or aime id, return the profile """ @@ -241,14 +238,12 @@ class WaccaProfileData(BaseData): ) return None - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_options( - self, user_id: int, option_id: int = None - ) -> Optional[List[Row]]: + def get_options(self, user_id: int, option_id: int = None) -> Optional[List[Row]]: """ Get a specific user option for a profile, or all of them if none specified """ @@ -259,7 +254,7 @@ class WaccaProfileData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None if option_id is not None: @@ -267,14 +262,12 @@ class WaccaProfileData(BaseData): else: return result.fetchall() - async def update_option( - self, user_id: int, option_id: int, value: int - ) -> Optional[int]: + 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 = await self.execute(conflict) + 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}" @@ -283,10 +276,10 @@ class WaccaProfileData(BaseData): return result.lastrowid - async def add_favorite_song(self, user_id: int, song_id: int) -> Optional[int]: + def add_favorite_song(self, user_id: int, song_id: int) -> Optional[int]: sql = favorite.insert().values(user=user_id, song_id=song_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: self.logger.error( f"{__name__} failed to insert favorite! profile: {user_id}, song_id: {song_id}" @@ -294,35 +287,35 @@ class WaccaProfileData(BaseData): return None return result.lastrowid - async def remove_favorite_song(self, user_id: int, song_id: int) -> None: + 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) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: self.logger.error( f"{__name__} failed to remove favorite! profile: {user_id}, song_id: {song_id}" ) return None - async def get_favorite_songs(self, user_id: int) -> Optional[List[Row]]: + def get_favorite_songs(self, user_id: int) -> Optional[List[Row]]: sql = favorite.select(favorite.c.user == user_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_gates(self, user_id: int) -> Optional[List[Row]]: + def get_gates(self, user_id: int) -> Optional[List[Row]]: sql = select(gate).where(gate.c.user == user_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def update_gate( + def update_gate( self, user_id: int, gate_id: int, @@ -350,7 +343,7 @@ class WaccaProfileData(BaseData): total_points=sql.inserted.total_points, ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.error( f"{__name__} failed to update gate! user: {user_id}, gate_id: {gate_id}" @@ -358,18 +351,18 @@ class WaccaProfileData(BaseData): return None return result.lastrowid - async def get_friends(self, user_id: int) -> Optional[List[Row]]: + def get_friends(self, user_id: int) -> Optional[List[Row]]: sql = friend.select(friend.c.profile_sender == user_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def profile_to_aime_user(self, profile_id: int) -> Optional[int]: + def profile_to_aime_user(self, profile_id: int) -> Optional[int]: sql = select(profile.c.user).where(profile.c.id == profile_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: self.logger.info( f"profile_to_aime_user: No user found for profile {profile_id}" @@ -385,7 +378,7 @@ class WaccaProfileData(BaseData): return this_profile["user"] - async def session_login( + def session_login( self, profile_id: int, is_new_day: bool, is_consec_day: bool ) -> None: # TODO: Reset consec days counter @@ -402,129 +395,127 @@ class WaccaProfileData(BaseData): last_login_date=func.now(), ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: self.logger.error( f"session_login: failed to update profile! profile: {profile_id}" ) return None - async def session_logout(self, profile_id: int) -> None: + def session_logout(self, profile_id: int) -> None: sql = profile.update(profile.c.id == id).values(login_count_consec=0) - result = await self.execute(sql) + result = self.execute(sql) if result is None: self.logger.error( f"{__name__} failed to update profile! profile: {profile_id}" ) return None - async def add_xp(self, profile_id: int, xp: int) -> 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 = await self.execute(sql) + 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 - async def add_wp(self, profile_id: int, wp: int) -> 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, ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: self.logger.error( f"add_wp: Failed to update profile! profile_id {profile_id} wp {wp}" ) return None - async def spend_wp(self, profile_id: int, wp: int) -> 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, ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: self.logger.error( f"spend_wp: Failed to update profile! profile_id {profile_id} wp {wp}" ) return None - async def activate_vip(self, profile_id: int, expire_time) -> 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 ) - result = await self.execute(sql) + 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}" ) return None - async def update_user_rating(self, profile_id: int, new_rating: int) -> 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) - result = await self.execute(sql) + 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}" ) return None - async def update_bingo( - self, aime_id: int, page: int, progress: int - ) -> Optional[int]: + 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) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.error(f"put_bingo: failed to update! aime_id: {aime_id}") return None return result.lastrowid - async def get_bingo(self, aime_id: int) -> Optional[List[Row]]: + def get_bingo(self, aime_id: int) -> Optional[List[Row]]: sql = select(bingo).where(bingo.c.user == aime_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_bingo_page(self, aime_id: int, page: Dict) -> Optional[List[Row]]: + 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) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def update_vip_time(self, profile_id: int, time_left) -> None: + 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 ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: self.logger.error(f"Failed to update VIP time for profile {profile_id}") - async def update_tutorial_flags(self, profile_id: int, flags: Dict) -> None: + def update_tutorial_flags(self, profile_id: int, flags: Dict) -> None: sql = profile.update(profile.c.id == profile_id).values( gate_tutorial_flags=flags ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: 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 3310e1e..5a3a53f 100644 --- a/titles/wacca/schema/score.py +++ b/titles/wacca/schema/score.py @@ -1,12 +1,13 @@ -from typing import List, Optional - -from core.data.schema import BaseData, metadata -from sqlalchemy import Column, Table, UniqueConstraint, and_ -from sqlalchemy.dialects.mysql import insert -from sqlalchemy.engine import Row +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ +from sqlalchemy.types import Integer, String, TIMESTAMP, JSON, Boolean from sqlalchemy.schema import ForeignKey from sqlalchemy.sql import func, select -from sqlalchemy.types import TIMESTAMP, Integer +from sqlalchemy.engine import Row +from sqlalchemy.dialects.mysql import insert +from typing import Optional, List, Dict, Any + +from core.data.schema import BaseData, metadata +from core.data import cached best_score = Table( "wacca_score_best", @@ -94,7 +95,7 @@ stageup = Table( class WaccaScoreData(BaseData): - async def put_best_score( + def put_best_score( self, user_id: int, song_id: int, @@ -163,7 +164,7 @@ class WaccaScoreData(BaseData): lowest_miss_ct=lowest_miss_ct, ) - result = await self.execute(conflict) + 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}" @@ -172,7 +173,7 @@ class WaccaScoreData(BaseData): return result.lastrowid - async def put_playlog( + def put_playlog( self, user_id: int, song_id: int, @@ -209,7 +210,7 @@ class WaccaScoreData(BaseData): season=season, ) - result = await self.execute(sql) + 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}" @@ -218,7 +219,7 @@ class WaccaScoreData(BaseData): return result.lastrowid - async def get_best_score( + def get_best_score( self, user_id: int, song_id: int, chart_id: int ) -> Optional[Row]: sql = best_score.select( @@ -229,20 +230,20 @@ class WaccaScoreData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() - async def get_best_scores(self, user_id: int) -> Optional[List[Row]]: + def get_best_scores(self, user_id: int) -> Optional[List[Row]]: sql = best_score.select(best_score.c.user == user_id) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def update_song_rating( + def update_song_rating( self, user_id: int, song_id: int, chart_id: int, new_rating: int ) -> None: sql = best_score.update( @@ -253,14 +254,14 @@ class WaccaScoreData(BaseData): ) ).values(rating=new_rating) - result = await self.execute(sql) + 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}" ) return None - async def put_stageup( + def put_stageup( self, user_id: int, version: int, @@ -291,7 +292,7 @@ class WaccaScoreData(BaseData): play_ct=stageup.c.play_ct + 1, ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning( f"put_stageup: failed to update! user_id: {user_id} version: {version} stage_id: {stage_id}" @@ -299,17 +300,17 @@ class WaccaScoreData(BaseData): return None return result.lastrowid - async def get_stageup(self, user_id: int, version: int) -> Optional[List[Row]]: + 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) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchall() - async def get_stageup_stage( + def get_stageup_stage( self, user_id: int, version: int, stage_id: int ) -> Optional[Row]: sql = select(stageup).where( @@ -320,7 +321,7 @@ class WaccaScoreData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() diff --git a/titles/wacca/schema/static.py b/titles/wacca/schema/static.py index d4ee2d5..18e4e23 100644 --- a/titles/wacca/schema/static.py +++ b/titles/wacca/schema/static.py @@ -1,11 +1,13 @@ -from typing import List, Optional +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ +from sqlalchemy.types import Integer, String, TIMESTAMP, JSON, Boolean, Float +from sqlalchemy.schema import ForeignKey +from sqlalchemy.sql import func, select +from sqlalchemy.engine import Row +from sqlalchemy.dialects.mysql import insert +from typing import Optional, List, Dict, Any from core.data.schema import BaseData, metadata -from sqlalchemy import Column, Table, UniqueConstraint, and_ -from sqlalchemy.dialects.mysql import insert -from sqlalchemy.engine import Row -from sqlalchemy.sql import select -from sqlalchemy.types import Float, Integer, String +from core.data import cached music = Table( "wacca_static_music", @@ -26,7 +28,7 @@ music = Table( class WaccaStaticData(BaseData): - async def put_music( + def put_music( self, version: int, song_id: int, @@ -59,13 +61,13 @@ class WaccaStaticData(BaseData): jacketFile=jacket, ) - result = await self.execute(conflict) + result = self.execute(conflict) if result is None: self.logger.warning(f"Failed to insert music {song_id} chart {chart_id}") return None return result.lastrowid - async def get_music_chart( + def get_music_chart( self, version: int, song_id: int, chart_id: int ) -> Optional[List[Row]]: sql = select(music).where( @@ -76,7 +78,7 @@ class WaccaStaticData(BaseData): ) ) - result = await self.execute(sql) + result = self.execute(sql) if result is None: return None return result.fetchone() diff --git a/titles/wacca/templates/wacca_index.jinja b/titles/wacca/templates/wacca_index.jinja deleted file mode 100644 index 974e715..0000000 --- a/titles/wacca/templates/wacca_index.jinja +++ /dev/null @@ -1,4 +0,0 @@ -{% extends "core/templates/index.jinja" %} -{% block content %} -

Wacca

-{% endblock content %} \ No newline at end of file