From 3acc2dc197a73e96d33d16de744199d2d8cae4b9 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Fri, 3 Mar 2023 23:46:29 +0100 Subject: [PATCH 01/63] Initial Card Maker ONGEKI support --- core/data/schema/versions/SDDT_3_rollback.sql | 1 + core/data/schema/versions/SDDT_4_upgrade.sql | 1 + core/data/schema/versions/SDED_1_upgrade.sql | 99 + example_config/cardmaker.yaml | 3 + example_config/ongeki.yaml | 23 + titles/chuni/base.py | 5 +- titles/cm/__init__.py | 15 + titles/cm/base.py | 84 + titles/cm/cm_data/MU3/static_cards.csv | 1616 +++++++++++++++++ titles/cm/cm_data/MU3/static_gacha_cards.csv | 336 ++++ titles/cm/cm_data/MU3/static_gachas.csv | 104 ++ titles/cm/config.py | 19 + titles/cm/const.py | 10 + titles/cm/index.py | 91 + titles/cm/read.py | 199 ++ titles/mai2/base.py | 9 +- titles/ongeki/bright.py | 609 ++++++- titles/ongeki/config.py | 18 +- titles/ongeki/const.py | 18 +- titles/ongeki/schema/item.py | 129 +- titles/ongeki/schema/profile.py | 3 +- titles/ongeki/schema/static.py | 203 +++ 22 files changed, 3582 insertions(+), 13 deletions(-) create mode 100644 core/data/schema/versions/SDDT_3_rollback.sql create mode 100644 core/data/schema/versions/SDDT_4_upgrade.sql create mode 100644 core/data/schema/versions/SDED_1_upgrade.sql create mode 100644 example_config/cardmaker.yaml create mode 100644 titles/cm/__init__.py create mode 100644 titles/cm/base.py create mode 100644 titles/cm/cm_data/MU3/static_cards.csv create mode 100644 titles/cm/cm_data/MU3/static_gacha_cards.csv create mode 100644 titles/cm/cm_data/MU3/static_gachas.csv create mode 100644 titles/cm/config.py create mode 100644 titles/cm/const.py create mode 100644 titles/cm/index.py create mode 100644 titles/cm/read.py diff --git a/core/data/schema/versions/SDDT_3_rollback.sql b/core/data/schema/versions/SDDT_3_rollback.sql new file mode 100644 index 0000000..a248ce3 --- /dev/null +++ b/core/data/schema/versions/SDDT_3_rollback.sql @@ -0,0 +1 @@ +ALTER TABLE ongeki_profile_data DROP COLUMN lastEmoneyCredit; \ No newline at end of file diff --git a/core/data/schema/versions/SDDT_4_upgrade.sql b/core/data/schema/versions/SDDT_4_upgrade.sql new file mode 100644 index 0000000..45510c2 --- /dev/null +++ b/core/data/schema/versions/SDDT_4_upgrade.sql @@ -0,0 +1 @@ +ALTER TABLE ongeki_profile_data ADD COLUMN lastEmoneyCredit INTEGER DEFAULT 0; diff --git a/core/data/schema/versions/SDED_1_upgrade.sql b/core/data/schema/versions/SDED_1_upgrade.sql new file mode 100644 index 0000000..c601745 --- /dev/null +++ b/core/data/schema/versions/SDED_1_upgrade.sql @@ -0,0 +1,99 @@ +CREATE TABLE ongeki_user_gacha ( + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, + user INT NOT NULL, + gachaId INT NOT NULL, + totalGachaCnt INT DEFAULT 0, + ceilingGachaCnt INT DEFAULT 0, + selectPoint INT DEFAULT 0, + useSelectPoint INT DEFAULT 0, + dailyGachaCnt INT DEFAULT 0, + fiveGachaCnt INT DEFAULT 0, + elevenGachaCnt INT DEFAULT 0, + dailyGachaDate TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT ongeki_user_gacha_uk UNIQUE (user, gachaId), + CONSTRAINT ongeki_user_gacha_ibfk_1 FOREIGN KEY (user) REFERENCES aime_user (id) ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE ongeki_user_gacha_supply ( + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, + user INT NOT NULL, + cardId INT NOT NULL, + CONSTRAINT ongeki_user_gacha_supply_uk UNIQUE (user, cardId), + CONSTRAINT ongeki_user_gacha_supply_ibfk_1 FOREIGN KEY (user) REFERENCES aime_user (id) ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE ongeki_static_gachas ( + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, + version INT NOT NULL, + gachaId INT NOT NULL, + gachaName VARCHAR(255) NOT NULL, + kind INT NOT NULL, + type INT DEFAULT 0, + isCeiling BOOLEAN DEFAULT 0, + maxSelectPoint INT DEFAULT 0, + ceilingCnt INT DEFAULT 10, + changeRateCnt1 INT DEFAULT 0, + changeRateCnt2 INT DEFAULT 0, + startDate TIMESTAMP DEFAULT '2018-01-01 00:00:00.0', + endDate TIMESTAMP DEFAULT '2018-01-01 00:00:00.0', + noticeStartDate TIMESTAMP DEFAULT '2018-01-01 00:00:00.0', + noticeEndDate TIMESTAMP DEFAULT '2038-01-01 00:00:00.0', + convertEndDate TIMESTAMP DEFAULT '2038-01-01 00:00:00.0', + CONSTRAINT ongeki_static_gachas_uk UNIQUE (version, gachaId, gachaName) +); + +CREATE TABLE ongeki_static_gacha_cards ( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + gachaId INT NOT NULL, + cardId INT NOT NULL, + rarity INT NOT NULL, + weight INT DEFAULT 1, + isPickup BOOLEAN DEFAULT 0, + isSelect BOOLEAN DEFAULT 1, + CONSTRAINT ongeki_static_gacha_cards_uk UNIQUE (gachaId, cardId) +); + + +CREATE TABLE ongeki_static_cards ( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + version INT NOT NULL, + cardId INT NOT NULL, + name VARCHAR(255) NOT NULL, + charaId INT NOT NULL, + nickName VARCHAR(255), + school VARCHAR(255) NOT NULL, + attribute VARCHAR(5) NOT NULL, + gakunen VARCHAR(255) NOT NULL, + rarity INT NOT NULL, + levelParam VARCHAR(255) NOT NULL, + skillId INT NOT NULL, + choKaikaSkillId INT NOT NULL, + cardNumber VARCHAR(255), + CONSTRAINT ongeki_static_cards_uk UNIQUE (version, cardId) +) CHARACTER SET utf8mb4; + +CREATE TABLE ongeki_user_print_detail ( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + user INT NOT NULL, + cardId INT NOT NULL, + cardType INT DEFAULT 0, + printDate TIMESTAMP NOT NULL, + serialId VARCHAR(20) NOT NULL, + placeId INT NOT NULL, + clientId VARCHAR(11) NOT NULL, + printerSerialId VARCHAR(20) NOT NULL, + isHolograph BOOLEAN DEFAULT 0, + isAutographed BOOLEAN DEFAULT 0, + printOption1 BOOLEAN DEFAULT 1, + printOption2 BOOLEAN DEFAULT 1, + printOption3 BOOLEAN DEFAULT 1, + printOption4 BOOLEAN DEFAULT 1, + printOption5 BOOLEAN DEFAULT 1, + printOption6 BOOLEAN DEFAULT 1, + printOption7 BOOLEAN DEFAULT 1, + printOption8 BOOLEAN DEFAULT 1, + printOption9 BOOLEAN DEFAULT 1, + printOption10 BOOLEAN DEFAULT 0, + FOREIGN KEY (user) REFERENCES aime_user(id) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT ongeki_user_print_detail_uk UNIQUE (serialId) +) CHARACTER SET utf8mb4; \ No newline at end of file diff --git a/example_config/cardmaker.yaml b/example_config/cardmaker.yaml new file mode 100644 index 0000000..a04dda5 --- /dev/null +++ b/example_config/cardmaker.yaml @@ -0,0 +1,3 @@ +server: + enable: True + loglevel: "info" diff --git a/example_config/ongeki.yaml b/example_config/ongeki.yaml index a04dda5..6ebd18a 100644 --- a/example_config/ongeki.yaml +++ b/example_config/ongeki.yaml @@ -1,3 +1,26 @@ server: enable: True loglevel: "info" + +gachas: + enabled_gachas: + - 1011 + - 1012 + - 1043 + - 1067 + - 1068 + - 1069 + - 1070 + - 1071 + - 1072 + - 1073 + - 1074 + - 1075 + - 1076 + - 1077 + - 1081 + - 1085 + - 1089 + - 1104 + - 1111 + - 1135 \ No newline at end of file diff --git a/titles/chuni/base.py b/titles/chuni/base.py index bf32f63..d557583 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -20,7 +20,10 @@ class ChuniBase(): self.logger = logging.getLogger("chuni") self.game = ChuniConstants.GAME_CODE self.version = ChuniConstants.VER_CHUNITHM - + + def handle_ping_request(self, data: Dict) -> Dict: + return {"returnCode": 1} + def handle_game_login_api_request(self, data: Dict) -> Dict: #self.data.base.log_event("chuni", "login", logging.INFO, {"version": self.version, "user": data["userId"]}) return { "returnCode": 1 } diff --git a/titles/cm/__init__.py b/titles/cm/__init__.py new file mode 100644 index 0000000..040bec7 --- /dev/null +++ b/titles/cm/__init__.py @@ -0,0 +1,15 @@ +from titles.cm.index import CardMakerServlet +from titles.cm.const import CardMakerConstants +from titles.cm.read import CardMakerReader + +index = CardMakerServlet +reader = CardMakerReader + +use_default_title = True +include_protocol = True +title_secure = False +game_codes = [CardMakerConstants.GAME_CODE] +trailing_slash = True +use_default_host = True + +current_schema_version = 1 diff --git a/titles/cm/base.py b/titles/cm/base.py new file mode 100644 index 0000000..c22fa41 --- /dev/null +++ b/titles/cm/base.py @@ -0,0 +1,84 @@ +from datetime import date, datetime, timedelta +from typing import Any, Dict, List +import json +import logging +from enum import Enum + +from core.config import CoreConfig +from core.data.cache import cached +from titles.cm.const import CardMakerConstants +from titles.cm.config import CardMakerConfig + + +class CardMakerBase(): + def __init__(self, core_cfg: CoreConfig, game_cfg: CardMakerConfig) -> None: + self.core_cfg = core_cfg + self.game_cfg = game_cfg + self.date_time_format = "%Y-%m-%d %H:%M:%S" + self.date_time_format_ext = "%Y-%m-%d %H:%M:%S.%f" # needs to be lopped off at [:-5] + self.date_time_format_short = "%Y-%m-%d" + self.logger = logging.getLogger("cardmaker") + self.game = CardMakerConstants.GAME_CODE + self.version = CardMakerConstants.VER_CARD_MAKER + + def handle_get_game_connect_api_request(self, data: Dict) -> Dict: + uri = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}" + + # CHUNITHM = 0, maimai = 1, ONGEKI = 2 + return { + "length": 3, + "gameConnectList": [ + { + "modelKind": 0, + "type": 1, + "titleUri": f"{uri}/SDHD/200/" + }, + { + "modelKind": 1, + "type": 1, + "titleUri": f"{uri}/SDEZ/120/" + }, + { + "modelKind": 2, + "type": 1, + "titleUri": f"{uri}/SDDT/130/" + } + ] + } + + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + reboot_start = date.strftime(datetime.now() + timedelta(hours=3), self.date_time_format) + reboot_end = date.strftime(datetime.now() + timedelta(hours=4), self.date_time_format) + + return { + "gameSetting": { + "dataVersion": "1.30.00", + "ongekiCmVersion": "1.30.01", + "chuniCmVersion": "2.00.00", + "maimaiCmVersion": "1.20.00", + "requestInterval": 10, + "rebootStartTime": reboot_start, + "rebootEndTime": reboot_end, + "maxCountCharacter": 100, + "maxCountItem": 100, + "maxCountCard": 100, + "watermark": False, + "isMaintenance": False, + "isBackgroundDistribute": False + }, + "isDumpUpload": False, + "isAou": False + } + + def handle_get_client_bookkeeping_api_request(self, data: Dict) -> Dict: + return { + "placeId": data["placeId"], + "length": 0, + "clientBookkeepingList": [] + } + + def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1, "apiName": "UpsertClientSettingApi"} + + def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1, "apiName": "UpsertClientBookkeepingApi"} diff --git a/titles/cm/cm_data/MU3/static_cards.csv b/titles/cm/cm_data/MU3/static_cards.csv new file mode 100644 index 0000000..53e6e56 --- /dev/null +++ b/titles/cm/cm_data/MU3/static_cards.csv @@ -0,0 +1,1616 @@ +"version","cardId","name","charaId","nickName","school","attribute","gakunen","rarity","levelParam","skillId","choKaikaSkillId","cardNumber" +0,1,"【SR】あかニャン[ボス専用]",1,"ボス専用","-","Fire","-",2,"55,227,242,257,272,287,0,0,0,287",0,0,"[O.N.G.E.K.I.]*.**-****" +0,2,"【SR】あおニャン[ボス専用]",2,"ボス専用","-","Aqua","-",2,"55,227,242,257,272,287,0,0,0,287",0,0,"[O.N.G.E.K.I.]*.**-****" +0,3,"【SR】みどニャン[ボス専用]",3,"ボス専用","-","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",0,0,"[O.N.G.E.K.I.]*.**-****" +0,100001,"【N】星咲 あかり",1000,"","奏坂学園","Fire","高校2年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"[O.N.G.E.K.I.]1.00-0001" +0,100002,"【R】星咲 あかり[シューター・ドレス]",1000,"シューター・ドレス","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100001,100042,"[O.N.G.E.K.I.]1.00-0005" +0,100003,"【R】星咲 あかり[体操着]",1000,"体操着","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.00-0004" +0,100004,"【R】星咲 あかり[私服]",1000,"私服","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100012,100053,"[O.N.G.E.K.I.]1.00-0003" +0,100005,"【SR】星咲 あかり[目指せ! いっぱいのエール!]",1000,"目指せ! いっぱいのエール!","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100008,100049,"[O.N.G.E.K.I.]1.00-0008" +0,100006,"【SR】星咲 あかり[ようこそ!奏坂へ!]",1000,"ようこそ!奏坂へ!","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100026,100067,"[O.N.G.E.K.I.]1.00-0007" +0,100007,"【SR】星咲 あかり[ドジっ子トラブル]",1000,"ドジっ子トラブル","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100032,100073,"[O.N.G.E.K.I.]1.00-0104" +0,100008,"【SSR】星咲 あかり[揺るぎない想い]",1000,"揺るぎない想い","奏坂学園","Fire","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",100034,100075,"[O.N.G.E.K.I.]1.00-0009" +0,100009,"【SSR】星咲 あかり[デイドリーム・フェアリーズ]",1000,"デイドリーム・フェアリーズ","奏坂学園","Fire","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",100038,100079,"[O.N.G.E.K.I.]1.00-0010" +0,100010,"【N】藤沢 柚子",1001,"","奏坂学園","Leaf","高校2年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"[O.N.G.E.K.I.]1.00-0011" +0,100011,"【R】藤沢 柚子[シューター・ドレス]",1001,"シューター・ドレス","奏坂学園","Leaf","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100002,100043,"[O.N.G.E.K.I.]1.00-0015" +0,100012,"【R】藤沢 柚子[体操着]",1001,"体操着","奏坂学園","Leaf","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.00-0014" +0,100013,"【R】藤沢 柚子[私服]",1001,"私服","奏坂学園","Leaf","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100012,100053,"[O.N.G.E.K.I.]1.00-0013" +0,100014,"【SR】藤沢 柚子[目指せ! いっぱいのエール!]",1001,"目指せ! いっぱいのエール!","奏坂学園","Leaf","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100008,100049,"[O.N.G.E.K.I.]1.00-0018" +0,100015,"【SR】藤沢 柚子[笑顔のお裾分け]",1001,"笑顔のお裾分け","奏坂学園","Leaf","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100027,100068,"[O.N.G.E.K.I.]1.00-0017" +0,100016,"【SR】藤沢 柚子[お着替えターイム!]",1001,"お着替えターイム!","奏坂学園","Leaf","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100032,100073,"[O.N.G.E.K.I.]1.00-0105" +0,100017,"【SSR】藤沢 柚子[ハッピー☆マジック]",1001,"ハッピー☆マジック","奏坂学園","Leaf","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",100034,100075,"[O.N.G.E.K.I.]1.00-0019" +0,100018,"【SSR】藤沢 柚子[デイドリーム・フェアリーズ]",1001,"デイドリーム・フェアリーズ","奏坂学園","Leaf","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",100039,100080,"[O.N.G.E.K.I.]1.00-0020" +0,100019,"【N】三角 葵",1002,"","奏坂学園","Aqua","高校2年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"[O.N.G.E.K.I.]1.00-0021" +0,100020,"【R】三角 葵[シューター・ドレス]",1002,"シューター・ドレス","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100003,100044,"[O.N.G.E.K.I.]1.00-0025" +0,100021,"【R】三角 葵[体操着]",1002,"体操着","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.00-0024" +0,100022,"【R】三角 葵[私服]",1002,"私服","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100012,100053,"[O.N.G.E.K.I.]1.00-0023" +0,100023,"【SR】三角 葵[目指せ! いっぱいのエール!]",1002,"目指せ! いっぱいのエール!","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100008,100049,"[O.N.G.E.K.I.]1.00-0028" +0,100024,"【SR】三角 葵[ね、簡単でしょ?]",1002,"ね、簡単でしょ?","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100028,100069,"[O.N.G.E.K.I.]1.00-0027" +0,100025,"【SR】三角 葵[かちこちストレッチ]",1002,"かちこちストレッチ","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100032,100073,"[O.N.G.E.K.I.]1.00-0106" +0,100026,"【SSR】三角 葵[ワールズエンド・ブルー]",1002,"ワールズエンド・ブルー","奏坂学園","Aqua","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",100034,100075,"[O.N.G.E.K.I.]1.00-0029" +0,100027,"【SSR】三角 葵[デイドリーム・フェアリーズ]",1002,"デイドリーム・フェアリーズ","奏坂学園","Aqua","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",100040,100081,"[O.N.G.E.K.I.]1.00-0030" +0,100028,"【N】高瀬 梨緒",1003,"","奏坂学園","Aqua","高校2年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"[O.N.G.E.K.I.]1.00-0031" +0,100029,"【R】高瀬 梨緒[シューター・ドレス]",1003,"シューター・ドレス","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.00-0034" +0,100030,"【R】高瀬 梨緒[体操着]",1003,"体操着","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-0033" +0,100031,"【SR】高瀬 梨緒[自称・超絶最強、登場!]",1003,"自称・超絶最強、登場!","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100015,100056,"[O.N.G.E.K.I.]1.00-0037" +0,100032,"【SR】高瀬 梨緒[カフェ de スクランブル]",1003,"カフェ de スクランブル","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100021,100062,"[O.N.G.E.K.I.]1.00-0038" +0,100033,"【SR】高瀬 梨緒[素直になれないお年頃]",1003,"素直になれないお年頃","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100029,100070,"[O.N.G.E.K.I.]1.00-0036" +0,100034,"【SSR】高瀬 梨緒[ジェットストライクリッパー]",1003,"ジェットストライクリッパー","奏坂学園","Aqua","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",100108,100109,"[O.N.G.E.K.I.]1.00-0039" +0,100035,"【N】結城 莉玖",1004,"","奏坂学園","Fire","高校1年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"[O.N.G.E.K.I.]1.00-0040" +0,100036,"【R】結城 莉玖[シューター・ドレス]",1004,"シューター・ドレス","奏坂学園","Fire","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.00-0043" +0,100037,"【R】結城 莉玖[体操着]",1004,"体操着","奏坂学園","Fire","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-0042" +0,100038,"【SR】結城 莉玖[嘘とがおー!とかたき討ち]",1004,"嘘とがおー!とかたき討ち","奏坂学園","Fire","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",100015,100056,"[O.N.G.E.K.I.]1.00-0046" +0,100039,"【SR】結城 莉玖[カフェ de スクランブル]",1004,"カフェ de スクランブル","奏坂学園","Fire","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",100019,100060,"[O.N.G.E.K.I.]1.00-0047" +0,100040,"【SR】結城 莉玖[未来のロックスター]",1004,"未来のロックスター","奏坂学園","Fire","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",100029,100070,"[O.N.G.E.K.I.]1.00-0045" +0,100041,"【SSR】結城 莉玖[リトルギターモンスター]",1004,"リトルギターモンスター","奏坂学園","Fire","高校1年生",3,"60,257,280,295,307,317,0,0,0,322",100104,100105,"[O.N.G.E.K.I.]1.00-0048" +0,100042,"【N】藍原 椿",1005,"","奏坂学園","Leaf","高校1年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"[O.N.G.E.K.I.]1.00-0049" +0,100043,"【R】藍原 椿[シューター・ドレス]",1005,"シューター・ドレス","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.00-0052" +0,100044,"【R】藍原 椿[体操着]",1005,"体操着","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-0051" +0,100045,"【SR】藍原 椿[嘘とがおー!とかたき討ち]",1005,"嘘とがおー!とかたき討ち","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",100015,100056,"[O.N.G.E.K.I.]1.00-0055" +0,100046,"【SR】藍原 椿[カフェ de スクランブル]",1005,"カフェ de スクランブル","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",100020,100061,"[O.N.G.E.K.I.]1.00-0056" +0,100047,"【SR】藍原 椿[夕暮れノスタルジー]",1005,"夕暮れノスタルジー","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",100029,100070,"[O.N.G.E.K.I.]1.00-0054" +0,100048,"【SSR】藍原 椿[サディスティック・スマイル]",1005,"サディスティック・スマイル","奏坂学園","Leaf","高校1年生",3,"60,257,280,295,307,317,0,0,0,322",100106,100107,"[O.N.G.E.K.I.]1.00-0057" +0,100049,"【N】桜井 春菜",1007,"","奏坂学園","Fire","高校2年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"[O.N.G.E.K.I.]1.00-0058" +0,100050,"【R】桜井 春菜[シューター・ドレス]",1007,"シューター・ドレス","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-0072" +0,100051,"【R】桜井 春菜[体操着]",1007,"体操着","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100001,100042,"[O.N.G.E.K.I.]1.00-0060" +0,100052,"【SR】桜井 春菜[ハルちゃん印のお弁当]",1007,"ハルちゃん印のお弁当","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100016,100057,"[O.N.G.E.K.I.]1.00-0073" +0,100053,"【SR】桜井 春菜[これは味見だから…!]",1007,"これは味見だから…!","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100025,100066,"[O.N.G.E.K.I.]1.00-0062" +0,100054,"【SSR】桜井 春菜[フラワリーブライド]",1007,"フラワリーブライド","奏坂学園","Fire","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]1.00-0074" +0,100055,"【N】早乙女 彩華",1006,"","奏坂学園","Aqua","高校3年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"[O.N.G.E.K.I.]1.00-0063" +0,100056,"【R】早乙女 彩華[シューター・ドレス]",1006,"シューター・ドレス","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-0075" +0,100057,"【R】早乙女 彩華[体操着]",1006,"体操着","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100003,100044,"[O.N.G.E.K.I.]1.00-0065" +0,100058,"【SR】早乙女 彩華[ハルちゃん印のお弁当]",1006,"ハルちゃん印のお弁当","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",100018,100059,"[O.N.G.E.K.I.]1.00-0076" +0,100059,"【SR】早乙女 彩華[しーっ!内緒だよ?]",1006,"しーっ!内緒だよ?","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",100025,100066,"[O.N.G.E.K.I.]1.00-0067" +0,100060,"【SSR】早乙女 彩華[爆アゲ☆バニーガール]",1006,"爆アゲ☆バニーガール","奏坂学園","Aqua","高校3年生",3,"60,257,280,295,307,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]1.00-0077" +0,100061,"【N】九條 楓",1008,"","奏坂学園","Leaf","高校3年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"[O.N.G.E.K.I.]1.00-0068" +0,100062,"【R】九條 楓[シューター・ドレス]",1008,"シューター・ドレス","奏坂学園","Leaf","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-0078" +0,100063,"【R】九條 楓[体操着]",1008,"体操着","奏坂学園","Leaf","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100002,100043,"[O.N.G.E.K.I.]1.00-0070" +0,100064,"【SR】九條 楓[波乱!副会長の通告!]",1008,"波乱!副会長の通告!","奏坂学園","Leaf","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",100017,100058,"[O.N.G.E.K.I.]1.00-0080" +0,100065,"【SR】九條 楓[雨傘を差して]",1008,"雨傘を差して","奏坂学園","Leaf","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",100025,100066,"[O.N.G.E.K.I.]1.00-0079" +0,100066,"【SSR】九條 楓[紫電一閃]",1008,"紫電一閃","奏坂学園","Leaf","高校3年生",3,"60,257,280,295,307,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]1.00-0095" +0,100067,"【N】井之原 小星",1010,"","奏坂学園","Leaf","高校1年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"[O.N.G.E.K.I.]1.00-0081" +0,100068,"【R】井之原 小星[シューター・ドレス]",1010,"シューター・ドレス","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-0084" +0,100069,"【R】井之原 小星[体操着]",1010,"体操着","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100007,100048,"[O.N.G.E.K.I.]1.00-0083" +0,100070,"【SR】井之原 小星[小星と咲姫が あらわれた!]",1010,"小星と咲姫が あらわれた!","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",100023,100064,"[O.N.G.E.K.I.]1.00-0087" +0,100071,"【SR】井之原 小星[ぐーたらゲーマー]",1010,"ぐーたらゲーマー","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",100031,100072,"[O.N.G.E.K.I.]1.00-0086" +0,100072,"【SSR】井之原 小星[お任せオートプレイ]",1010,"お任せオートプレイ","奏坂学園","Leaf","高校1年生",3,"60,257,280,295,307,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]1.00-0107" +0,100073,"【N】柏木 咲姫",1009,"","奏坂学園","Aqua","高校3年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"[O.N.G.E.K.I.]1.00-0088" +0,100074,"【R】柏木 咲姫[シューター・ドレス]",1009,"シューター・ドレス","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100007,100048,"[O.N.G.E.K.I.]1.00-0091" +0,100075,"【R】柏木 咲姫[体操着]",1009,"体操着","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-0090" +0,100076,"【SR】柏木 咲姫[小星と咲姫が あらわれた!]",1009,"小星と咲姫が あらわれた!","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",100024,100065,"[O.N.G.E.K.I.]1.00-0094" +0,100077,"【SR】柏木 咲姫[お姫様は隙だらけ]",1009,"お姫様は隙だらけ","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",100018,100059,"[O.N.G.E.K.I.]1.00-0093" +0,100078,"【SSR】柏木 咲姫[清く、凛々しく、美しく]",1009,"清く、凛々しく、美しく","奏坂学園","Aqua","高校3年生",3,"60,257,280,295,307,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]1.00-0096" +0,100079,"【N】逢坂 茜",1011,"","奏坂学園","Fire","高校3年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"[O.N.G.E.K.I.]1.00-0097" +0,100080,"【R】逢坂 茜[シューター・ドレス]",1011,"シューター・ドレス","奏坂学園","Fire","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100006,100047,"[O.N.G.E.K.I.]1.00-0100" +0,100081,"【R】逢坂 茜[体操着]",1011,"体操着","奏坂学園","Fire","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100136,100137,"[O.N.G.E.K.I.]1.00-0099" +0,100082,"【SR】逢坂 茜[世界征服のお誘い]",1011,"世界征服のお誘い","奏坂学園","Fire","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",100022,100063,"[O.N.G.E.K.I.]1.00-0103" +0,100083,"【SR】逢坂 茜[はちゃめちゃ生徒会長]",1011,"はちゃめちゃ生徒会長","奏坂学園","Fire","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",100030,100071,"[O.N.G.E.K.I.]1.00-0102" +0,100084,"【SSR】逢坂 茜[ショット&デストロイ]",1011,"ショット&デストロイ","奏坂学園","Fire","高校3年生",3,"60,257,280,295,307,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]1.00-0108" +0,100085,"【N】珠洲島 有栖",1012,"","奏坂学園","Aqua","高校1年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"[O.N.G.E.K.I.]1.00-0109" +0,100086,"【R】珠洲島 有栖[シューター・ドレス]",1012,"シューター・ドレス","奏坂学園","Aqua","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100008,100049,"[O.N.G.E.K.I.]1.00-0112" +0,100087,"【R】珠洲島 有栖[体操着]",1012,"体操着","奏坂学園","Aqua","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100014,100055,"[O.N.G.E.K.I.]1.00-0111" +0,100088,"【SR】珠洲島 有栖[黒幕はぬいぐるみ?]",1012,"黒幕はぬいぐるみ?","奏坂学園","Aqua","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",100025,100066,"[O.N.G.E.K.I.]1.00-0115" +0,100089,"【SR】珠洲島 有栖[優雅なティータイム]",1012,"優雅なティータイム","奏坂学園","Aqua","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",100033,100074,"[O.N.G.E.K.I.]1.00-0114" +1,100090,"【SSR】珠洲島 有栖[もふもふシューター]",1012,"もふもふシューター","奏坂学園","Aqua","高校1年生",3,"60,257,280,295,307,317,0,0,0,322",100037,100078,"[O.N.G.E.K.I.]1.05-0018" +0,100091,"【R】霧雨 魔理沙[東方Project]",2001,"東方Project","東方Project","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-E-0019" +0,100092,"【SR】霧雨 魔理沙[普通の魔法使い]",2001,"普通の魔法使い","東方Project","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.00-E-0020" +0,100093,"【SSR】霧雨 魔理沙[恋符「マスタースパーク」]",2001,"恋符「マスタースパーク」","東方Project","Leaf","-",3,"55,252,275,290,302,312,0,0,0,317",100144,100145,"[O.N.G.E.K.I.]1.00-E-0046" +0,100094,"【R】博麗 霊夢[東方Project]",2000,"東方Project","東方Project","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100140,100141,"[O.N.G.E.K.I.]1.00-E-0017" +0,100095,"【SR】博麗 霊夢[楽園の素敵な巫女]",2000,"楽園の素敵な巫女","東方Project","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.00-E-0018" +0,100096,"【SSR】博麗 霊夢[霊符「夢想封印」]",2000,"霊符「夢想封印」","東方Project","Fire","-",3,"55,252,275,290,302,312,0,0,0,317",100138,100139,"[O.N.G.E.K.I.]1.00-E-0045" +0,100097,"【R】十六夜 咲夜[東方紅魔郷]",2002,"東方紅魔郷","東方紅魔郷","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-E-0026" +0,100098,"【SR】十六夜 咲夜[完全で瀟洒な従者]",2002,"完全で瀟洒な従者","東方紅魔郷","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100028,100069,"[O.N.G.E.K.I.]1.00-E-0027" +0,100100,"【R】レミリア・スカーレット[東方紅魔郷]",2003,"東方紅魔郷","東方紅魔郷","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-E-0028" +0,100101,"【SR】レミリア・スカーレット[永遠に紅い幼き月]",2003,"永遠に紅い幼き月","東方紅魔郷","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.00-E-0029" +0,100102,"【SSR】レミリア・スカーレット[神槍「スピア・ザ・グングニル」]",2003,"神槍「スピア・ザ・グングニル」","東方紅魔郷","Fire","-",3,"55,252,275,290,302,312,0,0,0,317",100033,100074,"[O.N.G.E.K.I.]1.00-E-0032" +0,100103,"【R】フランドール・スカーレット[東方紅魔郷]",2004,"東方紅魔郷","東方紅魔郷","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-E-0030" +0,100104,"【SR】フランドール・スカーレット[悪魔の妹]",2004,"悪魔の妹","東方紅魔郷","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.00-E-0031" +0,100105,"【SSR】フランドール・スカーレット[禁忌「レーヴァテイン」]",2004,"禁忌「レーヴァテイン」","東方紅魔郷","Fire","-",3,"55,252,275,290,302,312,0,0,0,317",100036,100077,"[O.N.G.E.K.I.]1.00-E-0033" +0,100106,"【R】紅 美鈴[東方紅魔郷]",2005,"東方紅魔郷","東方紅魔郷","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-E-0022" +0,100107,"【SR】紅 美鈴[華人小娘]",2005,"華人小娘","東方紅魔郷","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100016,100057,"[O.N.G.E.K.I.]1.00-E-0023" +0,100109,"【R】パチュリー・ノーレッジ[東方紅魔郷]",2006,"東方紅魔郷","東方紅魔郷","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-E-0024" +0,100110,"【SR】パチュリー・ノーレッジ[動かない大図書館]",2006,"動かない大図書館","東方紅魔郷","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100027,100068,"[O.N.G.E.K.I.]1.00-E-0025" +1,100112,"【R】チルノ[東方Project]",2007,"東方Project","東方Project","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.05-E-0062" +1,100113,"【SR】チルノ[湖上の氷精]",2007,"湖上の氷精","東方Project","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",105026,105027,"[O.N.G.E.K.I.]1.05-E-0063" +0,100117,"【R】めぐみん[アークウィザード]",11001,"アークウィザード","この素晴らしい世界に祝福を!2","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-E-0034" +0,100118,"【SR】めぐみん[水辺にて]",11001,"水辺にて","この素晴らしい世界に祝福を!2","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100082,100083,"[O.N.G.E.K.I.]1.00-E-0035" +0,100119,"【SR】めぐみん[我が名はめぐみん]",11001,"我が名はめぐみん","この素晴らしい世界に祝福を!2","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.00-E-0040" +0,100120,"【SSR】めぐみん[紅魔族の休息]",11001,"紅魔族の休息","この素晴らしい世界に祝福を!2","Fire","-",3,"55,252,275,290,302,312,0,0,0,317",100084,100085,"[O.N.G.E.K.I.]1.00-E-0044" +0,100121,"【R】アクア[アークプリースト]",11002,"アークプリースト","この素晴らしい世界に祝福を!2","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.00-E-0036" +0,100122,"【SR】アクア[セイクリッド飲みくらべ]",11002,"セイクリッド飲みくらべ","この素晴らしい世界に祝福を!2","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100142,100143,"[O.N.G.E.K.I.]1.00-E-0037" +0,100123,"【SR】アクア[めぐみんのいたずら]",11002,"めぐみんのいたずら","この素晴らしい世界に祝福を!2","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100028,100069,"[O.N.G.E.K.I.]1.00-E-0041" +0,100124,"【R】ダクネス[クルセイダー]",11003,"クルセイダー","この素晴らしい世界に祝福を!2","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100002,100043,"[O.N.G.E.K.I.]1.00-E-0038" +0,100125,"【SR】ダクネス[爆風に吹かれて]",11003,"爆風に吹かれて","この素晴らしい世界に祝福を!2","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100017,100058,"[O.N.G.E.K.I.]1.00-E-0039" +0,100126,"【SR】ダクネス[露天風呂]",11003,"露天風呂","この素晴らしい世界に祝福を!2","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100027,100068,"[O.N.G.E.K.I.]1.00-E-0042" +0,100127,"【R】戸山香澄[ステージ]",18001,"ステージ","バンドリ! ガールズバンドパーティ!","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-E-0001" +0,100128,"【SR】戸山香澄[約束のキャンディ]",18001,"約束のキャンディ","バンドリ! ガールズバンドパーティ!","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.00-E-0003" +0,100129,"【SR】戸山香澄[最高のステージに!]",18001,"最高のステージに!","バンドリ! ガールズバンドパーティ!","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100015,100056,"[O.N.G.E.K.I.]1.00-E-0047" +0,100130,"【SR】戸山香澄[気合の円陣]",18001,"気合の円陣","バンドリ! ガールズバンドパーティ!","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100026,100067,"[O.N.G.E.K.I.]1.00-E-0048" +0,100131,"【SSR】戸山香澄[ガールズバンドパーティ!]",18001,"ガールズバンドパーティ!","バンドリ! ガールズバンドパーティ!","Fire","-",3,"55,252,275,290,302,312,0,0,0,317",100033,100074,"[O.N.G.E.K.I.]1.00-E-0016" +0,100132,"【R】美竹蘭[ステージ]",18002,"ステージ","バンドリ! ガールズバンドパーティ!","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-E-0004" +0,100133,"【SR】美竹蘭[断ち切った迷い]",18002,"断ち切った迷い","バンドリ! ガールズバンドパーティ!","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100016,100057,"[O.N.G.E.K.I.]1.00-E-0006" +0,100134,"【SR】美竹蘭[等身大の夜空]",18002,"等身大の夜空","バンドリ! ガールズバンドパーティ!","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100019,100060,"[O.N.G.E.K.I.]1.00-E-0049" +0,100135,"【SR】美竹蘭[夕焼けの先]",18002,"夕焼けの先","バンドリ! ガールズバンドパーティ!","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.00-E-0050" +0,100136,"【R】丸山彩[ステージ]",18003,"ステージ","バンドリ! ガールズバンドパーティ!","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-E-0007" +0,100137,"【SR】丸山彩[必殺アイドルポーズ☆]",18003,"必殺アイドルポーズ☆","バンドリ! ガールズバンドパーティ!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.00-E-0009" +0,100138,"【SR】丸山彩[私達のポスター]",18003,"私達のポスター","バンドリ! ガールズバンドパーティ!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100027,100068,"[O.N.G.E.K.I.]1.00-E-0051" +0,100139,"【SR】丸山彩[イベント、来てください!]",18003,"イベント、来てください!","バンドリ! ガールズバンドパーティ!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.00-E-0052" +0,100140,"【R】湊友希那[ステージ]",18004,"ステージ","バンドリ! ガールズバンドパーティ!","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-E-0010" +0,100141,"【SR】湊友希那[歌姫の覚悟]",18004,"歌姫の覚悟","バンドリ! ガールズバンドパーティ!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.00-E-0012" +0,100142,"【SR】湊友希那[秋晴れ、その先に]",18004,"秋晴れ、その先に","バンドリ! ガールズバンドパーティ!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100018,100059,"[O.N.G.E.K.I.]1.00-E-0053" +0,100143,"【SR】湊友希那[見守る目線]",18004,"見守る目線","バンドリ! ガールズバンドパーティ!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100028,100069,"[O.N.G.E.K.I.]1.00-E-0054" +0,100144,"【R】弦巻こころ[ステージ]",18005,"ステージ","バンドリ! ガールズバンドパーティ!","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-E-0013" +0,100145,"【SR】弦巻こころ[無敵のヒーロー]",18005,"無敵のヒーロー","バンドリ! ガールズバンドパーティ!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100027,100068,"[O.N.G.E.K.I.]1.00-E-0015" +0,100146,"【SR】弦巻こころ[見習い魔女]",18005,"見習い魔女","バンドリ! ガールズバンドパーティ!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100017,100058,"[O.N.G.E.K.I.]1.00-E-0055" +0,100147,"【SR】弦巻こころ[みんなが花マル1等賞!]",18005,"みんなが花マル1等賞!","バンドリ! ガールズバンドパーティ!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.00-E-0056" +1,100158,"【R】初音ミク[バーチャル・シンガー]",5000,"バーチャル・シンガー","バーチャル・シンガー","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.05-E-0001" +1,100159,"【SR】初音ミク[電子の歌姫]",5000,"電子の歌姫","バーチャル・シンガー","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100021,100062,"[O.N.G.E.K.I.]1.05-E-0002" +1,100160,"【SR】初音ミク[V4X]",5000,"V4X","バーチャル・シンガー","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.05-E-0023" +1,100161,"【R】巡音ルカ[バーチャル・シンガー]",5001,"バーチャル・シンガー","バーチャル・シンガー","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.05-E-0004" +1,100162,"【SR】巡音ルカ[クール&ハスキー]",5001,"クール&ハスキー","バーチャル・シンガー","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100019,100060,"[O.N.G.E.K.I.]1.05-E-0022" +1,100163,"【SR】巡音ルカ[V4X]",5001,"V4X","バーチャル・シンガー","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.05-E-0051" +1,100164,"【R】鏡音リン[バーチャル・シンガー]",5002,"バーチャル・シンガー","バーチャル・シンガー","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.05-E-0003" +1,100165,"【SR】鏡音リン[パワフル&チャーミング]",5002,"パワフル&チャーミング","バーチャル・シンガー","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100020,100061,"[O.N.G.E.K.I.]1.05-E-0021" +1,100166,"【SR】鏡音リン[V4X]",5002,"V4X","バーチャル・シンガー","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.05-E-0038" +0,100167,"【SSR】めぐみん[爆裂魔法]",11001,"爆裂魔法","この素晴らしい世界に祝福を!2","Fire","-",3,"55,252,275,290,302,312,0,0,0,317",100033,100074,"[O.N.G.E.K.I.]1.00-E-0043" +0,100168,"【R】戸山香澄[カラフルポッピン!]",18001,"カラフルポッピン!","バンドリ! ガールズバンドパーティ!","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100012,100053,"[O.N.G.E.K.I.]1.00-E-0002" +0,100169,"【R】美竹蘭[グロウアップロック]",18002,"グロウアップロック","バンドリ! ガールズバンドパーティ!","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100140,100141,"[O.N.G.E.K.I.]1.00-E-0005" +0,100170,"【R】丸山彩[煌めくステージへ]",18003,"煌めくステージへ","バンドリ! ガールズバンドパーティ!","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100012,100053,"[O.N.G.E.K.I.]1.00-E-0008" +0,100171,"【R】湊友希那[重なり合う青薔薇]",18004,"重なり合う青薔薇","バンドリ! ガールズバンドパーティ!","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100012,100053,"[O.N.G.E.K.I.]1.00-E-0011" +0,100172,"【R】弦巻こころ[笑顔のマジック]",18005,"笑顔のマジック","バンドリ! ガールズバンドパーティ!","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100002,100043,"[O.N.G.E.K.I.]1.00-E-0014" +0,100173,"【R】星咲 あかり[表情:笑顔]",1000,"表情:笑顔","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-0002" +0,100174,"【R】藤沢 柚子[表情:むすぅ]",1001,"表情:むすぅ","奏坂学園","Leaf","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-0012" +0,100175,"【R】三角 葵[表情:恥じらい]",1002,"表情:恥じらい","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-0022" +0,100176,"【R】高瀬 梨緒[表情:むかっ]",1003,"表情:むかっ","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100003,100044,"[O.N.G.E.K.I.]1.00-0032" +0,100177,"【R】結城 莉玖[表情:ドヤ]",1004,"表情:ドヤ","奏坂学園","Fire","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100001,100042,"[O.N.G.E.K.I.]1.00-0041" +0,100178,"【R】藍原 椿[表情:びっくり]",1005,"表情:びっくり","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100002,100043,"[O.N.G.E.K.I.]1.00-0050" +0,100179,"【R】早乙女 彩華[表情:ウィンク]",1006,"表情:ウィンク","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.00-0064" +0,100180,"【R】桜井 春菜[表情:笑顔]",1007,"表情:笑顔","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.00-0059" +0,100181,"【R】九條 楓[表情:剣幕]",1008,"表情:剣幕","奏坂学園","Leaf","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.00-0069" +0,100182,"【R】柏木 咲姫[表情:恥じらい]",1009,"表情:恥じらい","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100009,100050,"[O.N.G.E.K.I.]1.00-0089" +0,100183,"【R】井之原 小星[表情:キラキラ]",1010,"表情:キラキラ","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100010,100051,"[O.N.G.E.K.I.]1.00-0082" +0,100184,"【R】逢坂 茜[表情:にやり]",1011,"表情:にやり","奏坂学園","Fire","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100009,100050,"[O.N.G.E.K.I.]1.00-0098" +0,100185,"【R】珠洲島 有栖[表情:照れ]",1012,"表情:照れ","奏坂学園","Aqua","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100011,100052,"[O.N.G.E.K.I.]1.00-0110" +0,100186,"【SSR】戸山香澄[クインティプル☆すまいる]",18001,"クインティプル☆すまいる","バンドリ! ガールズバンドパーティ!","Fire","-",3,"55,252,275,290,302,312,0,0,0,317",100138,100139,"[O.N.G.E.K.I.]1.00-E-0021" +1,100187,"【SSR】めぐみん[エクスプロージョン]",11001,"エクスプロージョン","この素晴らしい世界に祝福を!2","Fire","-",3,"55,252,275,290,302,312,0,0,0,317",105022,105023,"[O.N.G.E.K.I.]1.05-E-0055" +1,100188,"【SSR】アクア[花鳥風月]",11002,"花鳥風月","この素晴らしい世界に祝福を!2","Aqua","-",3,"55,252,275,290,302,312,0,0,0,317",100144,100145,"[O.N.G.E.K.I.]1.05-E-0056" +1,100189,"【SSR】ダクネス[クルセイダーシールド]",11003,"クルセイダーシールド","この素晴らしい世界に祝福を!2","Leaf","-",3,"55,252,275,290,302,312,0,0,0,317",100144,100145,"[O.N.G.E.K.I.]1.05-E-0057" +0,100190,"【R】日向 美海[青の世界]",20001,"青の世界","アンジュ・ヴィエルジュ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100003,100044,"[O.N.G.E.K.I.]1.00-E-0057" +0,100191,"【SR】日向 美海[謹賀新年]",20001,"謹賀新年","アンジュ・ヴィエルジュ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100086,100087,"[O.N.G.E.K.I.]1.00-E-0058" +0,100192,"【SR】日向 美海[無限の可能性]",20001,"無限の可能性","アンジュ・ヴィエルジュ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.00-E-0063" +0,100193,"【SSR】日向 美海[メリークリスマス!]",20001,"メリークリスマス!","アンジュ・ヴィエルジュ","Aqua","-",3,"55,252,275,290,302,312,0,0,0,317",100033,100074,"[O.N.G.E.K.I.]1.00-E-0066" +0,100194,"【SSR】日向 美海[常夏の可能性]",20001,"常夏の可能性","アンジュ・ヴィエルジュ","Aqua","-",3,"55,252,275,290,302,312,0,0,0,317",100088,100089,"[O.N.G.E.K.I.]1.00-E-0067" +0,100195,"【R】彩城 天音[青の世界]",20002,"青の世界","アンジュ・ヴィエルジュ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.00-E-0059" +0,100196,"【SR】彩城 天音[謹賀新年]",20002,"謹賀新年","アンジュ・ヴィエルジュ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100018,100059,"[O.N.G.E.K.I.]1.00-E-0060" +0,100197,"【SR】彩城 天音[みんなの希望]",20002,"みんなの希望","アンジュ・ヴィエルジュ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100028,100069,"[O.N.G.E.K.I.]1.00-E-0064" +0,100198,"【R】東条 遥[青の世界]",20003,"青の世界","アンジュ・ヴィエルジュ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-E-0061" +0,100199,"【SR】東条 遥[謹賀新年]",20003,"謹賀新年","アンジュ・ヴィエルジュ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100028,100069,"[O.N.G.E.K.I.]1.00-E-0062" +0,100200,"【SR】東条 遥[希望の光]",20003,"希望の光","アンジュ・ヴィエルジュ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.00-E-0065" +0,100201,"【SR】星咲 あかり[ONGEKI Vocal Collection 01]",1000,"ONGEKI Vocal Collection 01","プロモーション","Fire","高校2年生",2,"50,222,237,252,267,282,0,0,0,282",100092,100093,"[O.N.G.E.K.I.]1.00-P-0001" +0,100202,"【SR】藤沢 柚子[ONGEKI Vocal Collection 01]",1001,"ONGEKI Vocal Collection 01","プロモーション","Leaf","高校2年生",2,"50,222,237,252,267,282,0,0,0,282",100094,100095,"[O.N.G.E.K.I.]1.00-P-0002" +0,100203,"【SR】三角 葵[ONGEKI Vocal Collection 01]",1002,"ONGEKI Vocal Collection 01","プロモーション","Aqua","高校2年生",2,"50,222,237,252,267,282,0,0,0,282",100096,100097,"[O.N.G.E.K.I.]1.00-P-0003" +0,100204,"【SR】高瀬 梨緒[ONGEKI Vocal Collection 02]",1003,"ONGEKI Vocal Collection 02","プロモーション","Aqua","高校2年生",2,"50,222,237,252,267,282,0,0,0,282",100102,100103,"[O.N.G.E.K.I.]1.00-P-0004" +0,100205,"【SR】結城 莉玖[ONGEKI Vocal Collection 02]",1004,"ONGEKI Vocal Collection 02","プロモーション","Fire","高校1年生",2,"50,222,237,252,267,282,0,0,0,282",100100,100101,"[O.N.G.E.K.I.]1.00-P-0005" +0,100206,"【SR】藍原 椿[ONGEKI Vocal Collection 02]",1005,"ONGEKI Vocal Collection 02","プロモーション","Leaf","高校1年生",2,"50,222,237,252,267,282,0,0,0,282",100098,100099,"[O.N.G.E.K.I.]1.00-P-0006" +2,100208,"【N】柏木 美亜",1013,"","奏坂学園","Fire","中学2年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"[O.N.G.E.K.I.]1.10-0005" +2,100209,"【R】柏木 美亜[シューター・ドレス]",1013,"シューター・ドレス","奏坂学園","Fire","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",110002,110003,"[O.N.G.E.K.I.]1.10-0006" +1,100210,"【R】星咲 あかり[私服2]",1000,"私服2","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",105100,105101,"[O.N.G.E.K.I.]1.05-0002" +1,100211,"【R】藤沢 柚子[私服2]",1001,"私服2","奏坂学園","Leaf","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",105102,105103,"[O.N.G.E.K.I.]1.05-0006" +1,100212,"【R】三角 葵[私服2]",1002,"私服2","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",105104,105105,"[O.N.G.E.K.I.]1.05-0009" +1,100213,"【R】高瀬 梨緒[私服]",1003,"私服","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",105110,105111,"[O.N.G.E.K.I.]1.05-0020" +1,100214,"【R】結城 莉玖[私服]",1004,"私服","奏坂学園","Fire","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",105106,105107,"[O.N.G.E.K.I.]1.05-0022" +1,100215,"【R】藍原 椿[私服]",1005,"私服","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",105108,105109,"[O.N.G.E.K.I.]1.05-0024" +1,100216,"【R】早乙女 彩華[私服]",1006,"私服","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",105060,105061,"[O.N.G.E.K.I.]1.05-0033" +1,100217,"【R】桜井 春菜[私服]",1007,"私服","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",105060,105061,"[O.N.G.E.K.I.]1.05-0031" +1,100218,"【R】九條 楓[私服]",1008,"私服","奏坂学園","Leaf","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",105114,105115,"[O.N.G.E.K.I.]1.05-0057" +1,100219,"【R】柏木 咲姫[私服]",1009,"私服","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100003,100044,"[O.N.G.E.K.I.]1.05-0047" +1,100220,"【R】井之原 小星[私服]",1010,"私服","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100002,100043,"[O.N.G.E.K.I.]1.05-0049" +1,100221,"【R】逢坂 茜[私服]",1011,"私服","奏坂学園","Fire","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",105114,105115,"[O.N.G.E.K.I.]1.05-0059" +1,100222,"【R】珠洲島 有栖[私服]",1012,"私服","奏坂学園","Aqua","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",105114,105115,"[O.N.G.E.K.I.]1.05-0061" +1,100223,"【R】星咲 あかり[私服 表情:げげっ]",1000,"私服 表情:げげっ","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.05-0001" +1,100224,"【R】藤沢 柚子[私服 表情:涙目]",1001,"私服 表情:涙目","奏坂学園","Leaf","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.05-0005" +1,100225,"【R】三角 葵[私服 表情:笑顔]",1002,"私服 表情:笑顔","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.05-0028" +1,100226,"【R】高瀬 梨緒[私服 表情:ぐぬぬ]",1003,"私服 表情:ぐぬぬ","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",105060,105061,"[O.N.G.E.K.I.]1.05-0038" +1,100227,"【R】結城 莉玖[私服 表情:むー]",1004,"私服 表情:むー","奏坂学園","Fire","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",105060,105061,"[O.N.G.E.K.I.]1.05-0029" +1,100228,"【R】藍原 椿[私服 表情:目線を逸らす]",1005,"私服 表情:目線を逸らす","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",105060,105061,"[O.N.G.E.K.I.]1.05-0040" +1,100229,"【R】早乙女 彩華[私服 表情:照れ]",1006,"私服 表情:照れ","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",105118,105119,"[O.N.G.E.K.I.]1.05-0045" +1,100230,"【R】桜井 春菜[私服 表情:恥じらい]",1007,"私服 表情:恥じらい","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",105120,105121,"[O.N.G.E.K.I.]1.05-0046" +1,100231,"【R】九條 楓[私服 表情:恥じらい]",1008,"私服 表情:恥じらい","奏坂学園","Leaf","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",105068,105069,"[O.N.G.E.K.I.]1.05-0066" +1,100232,"【R】柏木 咲姫[私服 表情:得意気]",1009,"私服 表情:得意気","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",105124,105125,"[O.N.G.E.K.I.]1.05-0055" +1,100233,"【R】井之原 小星[私服 表情:悲しい]",1010,"私服 表情:悲しい","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",105122,105123,"[O.N.G.E.K.I.]1.05-0054" +1,100234,"【R】逢坂 茜[私服 表情:わっはっは!]",1011,"私服 表情:わっはっは!","奏坂学園","Fire","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",105068,105069,"[O.N.G.E.K.I.]1.05-0077" +1,100235,"【R】珠洲島 有栖[私服 表情:微笑み]",1012,"私服 表情:微笑み","奏坂学園","Aqua","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",105068,105069,"[O.N.G.E.K.I.]1.05-0068" +1,100236,"【SR】星咲 あかり[わくわくバイト・ミッション]",1000,"わくわくバイト・ミッション","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105078,105079,"[O.N.G.E.K.I.]1.05-0004" +1,100237,"【SR】藤沢 柚子[わくわくバイト・ミッション]",1001,"わくわくバイト・ミッション","奏坂学園","Leaf","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105080,105081,"[O.N.G.E.K.I.]1.05-0008" +1,100238,"【SR】三角 葵[わくわくバイト・ミッション]",1002,"わくわくバイト・ミッション","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105082,105083,"[O.N.G.E.K.I.]1.05-0011" +1,100239,"【SR】高瀬 梨緒[ゲーセン☆チャンス!?]",1003,"ゲーセン☆チャンス!?","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105084,105085,"[O.N.G.E.K.I.]1.05-0021" +1,100240,"【SR】結城 莉玖[ゲーセン☆チャンス!?]",1004,"ゲーセン☆チャンス!?","奏坂学園","Fire","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",105084,105085,"[O.N.G.E.K.I.]1.05-0023" +1,100241,"【SR】藍原 椿[ゲーセン☆チャンス!?]",1005,"ゲーセン☆チャンス!?","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",105084,105085,"[O.N.G.E.K.I.]1.05-0025" +1,100242,"【SR】早乙女 彩華[妄想×動揺×大暴走]",1006,"妄想×動揺×大暴走","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",105088,105089,"[O.N.G.E.K.I.]1.05-0034" +1,100243,"【SR】桜井 春菜[妄想×動揺×大暴走]",1007,"妄想×動揺×大暴走","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105086,105087,"[O.N.G.E.K.I.]1.05-0032" +1,100244,"【SR】九條 楓[ドタバタ子守り大作戦]",1008,"ドタバタ子守り大作戦","奏坂学園","Leaf","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",105096,105097,"[O.N.G.E.K.I.]1.05-0058" +1,100245,"【SR】柏木 咲姫[おかえりなさいませ! ご主人さま♪]",1009,"おかえりなさいませ! ご主人さま♪","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",105092,105093,"[O.N.G.E.K.I.]1.05-0048" +1,100246,"【SR】井之原 小星[おかえりなさいませ! ご主人さま♪]",1010,"おかえりなさいませ! ご主人さま♪","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",105090,105091,"[O.N.G.E.K.I.]1.05-0050" +1,100247,"【SR】逢坂 茜[ドタバタ子守り大作戦]",1011,"ドタバタ子守り大作戦","奏坂学園","Fire","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",105094,105095,"[O.N.G.E.K.I.]1.05-0060" +1,100248,"【SR】珠洲島 有栖[ドタバタ子守り大作戦]",1012,"ドタバタ子守り大作戦","奏坂学園","Aqua","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",105098,105099,"[O.N.G.E.K.I.]1.05-0062" +1,100249,"【SR】星咲 あかり[Jump!! Jump!! Jump!!]",1000,"Jump!! Jump!! Jump!!","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105040,105041,"[O.N.G.E.K.I.]1.05-0003" +1,100250,"【SR】藤沢 柚子[Jump!! Jump!! Jump!!]",1001,"Jump!! Jump!! Jump!!","奏坂学園","Leaf","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105040,105041,"[O.N.G.E.K.I.]1.05-0007" +1,100251,"【SR】三角 葵[Jump!! Jump!! Jump!!]",1002,"Jump!! Jump!! Jump!!","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105040,105041,"[O.N.G.E.K.I.]1.05-0010" +1,100252,"【SR】高瀬 梨緒[Jump!! Jump!! Jump!!]",1003,"Jump!! Jump!! Jump!!","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105040,105041,"[O.N.G.E.K.I.]1.05-0012" +2,100253,"【SR】結城 莉玖[Jump!! Jump!! Jump!!]",1004,"Jump!! Jump!! Jump!!","奏坂学園","Fire","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",105040,105041,"[O.N.G.E.K.I.]1.10-0028" +2,100254,"【SR】藍原 椿[Jump!! Jump!! Jump!!]",1005,"Jump!! Jump!! Jump!!","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",105040,105041,"[O.N.G.E.K.I.]1.10-0055" +2,100255,"【SR】早乙女 彩華[Jump!! Jump!! Jump!!]",1006,"Jump!! Jump!! Jump!!","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",105040,105041,"[O.N.G.E.K.I.]1.10-0039" +2,100256,"【SR】桜井 春菜[Jump!! Jump!! Jump!!]",1007,"Jump!! Jump!! Jump!!","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105040,105041,"[O.N.G.E.K.I.]1.10-0029" +2,100257,"【SR】九條 楓[Jump!! Jump!! Jump!!]",1008,"Jump!! Jump!! Jump!!","奏坂学園","Leaf","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",105040,105041,"[O.N.G.E.K.I.]1.10-0056" +2,100258,"【SR】柏木 咲姫[Jump!! Jump!! Jump!!]",1009,"Jump!! Jump!! Jump!!","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",105040,105041,"[O.N.G.E.K.I.]1.10-0041" +2,100259,"【SR】井之原 小星[Jump!! Jump!! Jump!!]",1010,"Jump!! Jump!! Jump!!","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",105040,105041,"[O.N.G.E.K.I.]1.10-0057" +1,100260,"【SR】逢坂 茜[Jump!! Jump!! Jump!!]",1011,"Jump!! Jump!! Jump!!","奏坂学園","Fire","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",105040,105041,"[O.N.G.E.K.I.]1.05-0017" +2,100261,"【SR】珠洲島 有栖[Jump!! Jump!! Jump!!]",1012,"Jump!! Jump!! Jump!!","奏坂学園","Aqua","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",105040,105041,"[O.N.G.E.K.I.]1.10-0042" +1,100262,"【SR】星咲 あかり[どっちがどっち?]",1000,"どっちがどっち?","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105042,105043,"[O.N.G.E.K.I.]1.05-0026" +1,100263,"【SR】藤沢 柚子[華より団子]",1001,"華より団子","奏坂学園","Leaf","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100031,100072,"[O.N.G.E.K.I.]1.05-0027" +1,100264,"【SR】三角 葵[モテモテ!? ふれあい体験]",1002,"モテモテ!? ふれあい体験","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105044,105045,"[O.N.G.E.K.I.]1.05-0043" +1,100265,"【SR】高瀬 梨緒[巷で噂の看板娘]",1003,"巷で噂の看板娘","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105050,105051,"[O.N.G.E.K.I.]1.05-0039" +1,100266,"【SR】結城 莉玖[食べちゃうぞ♪]",1004,"食べちゃうぞ♪","奏坂学園","Fire","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",105046,105047,"[O.N.G.E.K.I.]1.05-0044" +1,100267,"【SR】藍原 椿[恥じらいバレンタイン]",1005,"恥じらいバレンタイン","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",105048,105049,"[O.N.G.E.K.I.]1.05-0013" +1,100268,"【SR】早乙女 彩華[レッツ、チャレンジ!?]",1006,"レッツ、チャレンジ!?","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",105052,105053,"[O.N.G.E.K.I.]1.05-0075" +1,100269,"【SR】桜井 春菜[びしょ濡れ雨宿り]",1007,"びしょ濡れ雨宿り","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100024,100065,"[O.N.G.E.K.I.]1.05-0053" +1,100270,"【SR】九條 楓[行雲流水の如く]",1008,"行雲流水の如く","奏坂学園","Leaf","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",100024,100065,"[O.N.G.E.K.I.]1.05-0042" +1,100271,"【SR】柏木 咲姫[南国ホリデー]",1009,"南国ホリデー","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",100031,100072,"[O.N.G.E.K.I.]1.05-0065" +1,100272,"【SR】井之原 小星[ぷかぷかコンティニュー]",1010,"ぷかぷかコンティニュー","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",105054,105055,"[O.N.G.E.K.I.]1.05-0076" +1,100273,"【SR】逢坂 茜[いたずらスプラッシュ]",1011,"いたずらスプラッシュ","奏坂学園","Fire","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",105056,105057,"[O.N.G.E.K.I.]1.05-0067" +1,100274,"【SR】珠洲島 有栖[雨上がりの空に]",1012,"雨上がりの空に","奏坂学園","Aqua","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",105058,105059,"[O.N.G.E.K.I.]1.05-0056" +1,100275,"【SSR】星咲 あかり[マーメイド・ランデブー]",1000,"マーメイド・ランデブー","奏坂学園","Fire","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",105028,105029,"[O.N.G.E.K.I.]1.05-0073" +1,100276,"【SSR】藤沢 柚子[ハジけて☆サマー]",1001,"ハジけて☆サマー","奏坂学園","Leaf","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",105028,105029,"[O.N.G.E.K.I.]1.05-0063" +1,100277,"【SSR】三角 葵[with you]",1002,"with you","奏坂学園","Aqua","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",105028,105029,"[O.N.G.E.K.I.]1.05-0064" +1,100278,"【SSR】高瀬 梨緒[黄昏ロマンス]",1003,"黄昏ロマンス","奏坂学園","Aqua","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",105030,105031,"[O.N.G.E.K.I.]1.05-0074" +1,100280,"【SSR】藍原 椿[狐の嫁入り]",1005,"狐の嫁入り","奏坂学園","Leaf","高校1年生",3,"60,257,280,295,307,317,0,0,0,322",105030,105031,"[O.N.G.E.K.I.]1.05-0052" +1,100282,"【SSR】桜井 春菜[スイート&スイート]",1007,"スイート&スイート","奏坂学園","Fire","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",105032,105033,"[O.N.G.E.K.I.]1.05-0014" +1,100284,"【SSR】柏木 咲姫[エキゾチック・トワイライト]",1009,"エキゾチック・トワイライト","奏坂学園","Aqua","高校3年生",3,"60,257,280,295,307,317,0,0,0,322",105036,105037,"[O.N.G.E.K.I.]1.05-0041" +1,100285,"【SSR】井之原 小星[おひな様は脱力系]",1010,"おひな様は脱力系","奏坂学園","Leaf","高校1年生",3,"60,257,280,295,307,317,0,0,0,322",105034,105035,"[O.N.G.E.K.I.]1.05-0030" +1,100286,"【SSR】逢坂 茜[それいけ! 猛獣マスター]",1011,"それいけ! 猛獣マスター","奏坂学園","Fire","高校3年生",3,"60,257,280,295,307,317,0,0,0,322",105038,105039,"[O.N.G.E.K.I.]1.05-0051" +1,100288,"【SSR】九條 楓[デイドリーム・フェアリーズ]",1008,"デイドリーム・フェアリーズ","奏坂学園","Leaf","高校3年生",3,"65,270,291,304,314,322,0,0,0,327",100039,100080,"[O.N.G.E.K.I.]1.05-0015" +1,100289,"【SSR】逢坂 茜[デイドリーム・フェアリーズ]",1011,"デイドリーム・フェアリーズ","奏坂学園","Fire","高校3年生",3,"65,270,291,304,314,322,0,0,0,327",100038,100079,"[O.N.G.E.K.I.]1.05-0016" +1,100290,"【SSR】珠洲島 有栖[デイドリーム・フェアリーズ]",1012,"デイドリーム・フェアリーズ","奏坂学園","Aqua","高校1年生",3,"65,270,291,304,314,322,0,0,0,327",100040,100081,"[O.N.G.E.K.I.]1.05-0019" +1,100291,"【SR】早乙女 彩華[ONGEKI Vocal Collection 03]",1006,"ONGEKI Vocal Collection 03","プロモーション","Aqua","高校3年生",2,"50,222,237,252,267,282,0,0,0,282",105120,105121,"[O.N.G.E.K.I.]1.05-P-0002" +1,100292,"【SR】桜井 春菜[ONGEKI Vocal Collection 03]",1007,"ONGEKI Vocal Collection 03","プロモーション","Fire","高校2年生",2,"50,222,237,252,267,282,0,0,0,282",105118,105119,"[O.N.G.E.K.I.]1.05-P-0003" +1,100293,"【SR】九條 楓[ONGEKI Vocal Collection 05]",1008,"ONGEKI Vocal Collection 05","プロモーション","Leaf","高校3年生",2,"50,222,237,252,267,282,0,0,0,282",105128,105129,"[O.N.G.E.K.I.]1.05-P-0006" +1,100294,"【SR】柏木 咲姫[ONGEKI Vocal Collection 04]",1009,"ONGEKI Vocal Collection 04","プロモーション","Aqua","高校3年生",2,"50,222,237,252,267,282,0,0,0,282",105124,105125,"[O.N.G.E.K.I.]1.05-P-0004" +1,100295,"【SR】井之原 小星[ONGEKI Vocal Collection 04]",1010,"ONGEKI Vocal Collection 04","プロモーション","Leaf","高校1年生",2,"50,222,237,252,267,282,0,0,0,282",105122,105123,"[O.N.G.E.K.I.]1.05-P-0005" +1,100296,"【SR】逢坂 茜[ONGEKI Vocal Collection 05]",1011,"ONGEKI Vocal Collection 05","プロモーション","Fire","高校3年生",2,"50,222,237,252,267,282,0,0,0,282",105126,105127,"[O.N.G.E.K.I.]1.05-P-0007" +1,100297,"【SR】珠洲島 有栖[ONGEKI Vocal Collection 05]",1012,"ONGEKI Vocal Collection 05","プロモーション","Aqua","高校1年生",2,"50,222,237,252,267,282,0,0,0,282",105130,105131,"[O.N.G.E.K.I.]1.05-P-0008" +1,100298,"【SSR】星咲 あかり[ONGEKI Sound Collection 01]",1000,"ONGEKI Sound Collection 01","プロモーション","Fire","高校2年生",3,"55,252,275,290,302,312,0,0,0,317",105144,105145,"[O.N.G.E.K.I.]1.05-P-0001" +1,100299,"【R】橙[東方妖々夢]",2008,"東方妖々夢","東方妖々夢","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100012,100053,"[O.N.G.E.K.I.]1.05-E-0039" +1,100300,"【R】魂魄 妖夢[東方妖々夢]",2009,"東方妖々夢","東方妖々夢","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100002,100043,"[O.N.G.E.K.I.]1.05-E-0041" +1,100301,"【R】西行寺 幽々子[東方妖々夢]",2010,"東方妖々夢","東方妖々夢","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.05-E-0044" +1,100302,"【R】八雲 藍[東方妖々夢]",2011,"東方妖々夢","東方妖々夢","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",105014,105015,"[O.N.G.E.K.I.]1.05-E-0047" +1,100303,"【R】八雲 紫[東方妖々夢]",2012,"東方妖々夢","東方妖々夢","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.05-E-0049" +1,100304,"【SR】橙[すきま妖怪の式の式]",2008,"すきま妖怪の式の式","東方妖々夢","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.05-E-0040" +1,100305,"【SR】魂魄 妖夢[半人半霊の庭師]",2009,"半人半霊の庭師","東方妖々夢","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100027,100068,"[O.N.G.E.K.I.]1.05-E-0042" +1,100306,"【SR】西行寺 幽々子[幽冥楼閣の亡霊少女]",2010,"幽冥楼閣の亡霊少女","東方妖々夢","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.05-E-0045" +1,100307,"【SR】八雲 藍[すきま妖怪の式]",2011,"すきま妖怪の式","東方妖々夢","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",105012,105013,"[O.N.G.E.K.I.]1.05-E-0048" +1,100308,"【SR】八雲 紫[幻想の境界]",2012,"幻想の境界","東方妖々夢","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.05-E-0050" +1,100309,"【SSR】西行寺 幽々子[桜符「完全なる墨染の桜 -開花-」]",2010,"桜符「完全なる墨染の桜 -開花-」","東方妖々夢","Leaf","-",3,"55,252,275,290,302,312,0,0,0,317",100033,100074,"[O.N.G.E.K.I.]1.05-E-0046" +1,100310,"【SSR】魂魄 妖夢[獄界剣「二百由旬の一閃」]",2009,"獄界剣「二百由旬の一閃」","東方妖々夢","Leaf","-",3,"55,252,275,290,302,312,0,0,0,317",100036,100077,"[O.N.G.E.K.I.]1.05-E-0043" +1,100311,"【R】御坂 美琴[常盤台中学]",21001,"常盤台中学","とある科学の超電磁砲S","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.05-E-0005" +1,100312,"【R】白井 黒子[常盤台中学]",21002,"常盤台中学","とある科学の超電磁砲S","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100002,100043,"[O.N.G.E.K.I.]1.05-E-0008" +1,100313,"【R】初春 飾利[柵川中学]",21003,"柵川中学","とある科学の超電磁砲S","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.05-E-0015" +1,100314,"【R】佐天 涙子[柵川中学]",21004,"柵川中学","とある科学の超電磁砲S","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.05-E-0018" +1,100315,"【SR】御坂 美琴[電撃使い]",21001,"電撃使い","とある科学の超電磁砲S","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.05-E-0006" +1,100316,"【SR】白井 黒子[空間移動]",21002,"空間移動","とある科学の超電磁砲S","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",105004,105005,"[O.N.G.E.K.I.]1.05-E-0009" +1,100317,"【SR】初春 飾利[定温保存]",21003,"定温保存","とある科学の超電磁砲S","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.05-E-0016" +1,100318,"【SR】佐天 涙子[空力使い]",21004,"空力使い","とある科学の超電磁砲S","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.05-E-0019" +1,100319,"【SR】御坂 美琴[常盤台のエース]",21001,"常盤台のエース","とある科学の超電磁砲S","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100027,100068,"[O.N.G.E.K.I.]1.05-E-0007" +1,100320,"【SR】白井 黒子[風紀委員ですの!]",21002,"風紀委員ですの!","とある科学の超電磁砲S","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100008,100049,"[O.N.G.E.K.I.]1.05-E-0010" +1,100321,"【SR】初春 飾利[いきなり何するんですか佐天さんっ!]",21003,"いきなり何するんですか佐天さんっ!","とある科学の超電磁砲S","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",105002,105003,"[O.N.G.E.K.I.]1.05-E-0017" +1,100322,"【SR】佐天 涙子[うーいーはーるーん!!]",21004,"うーいーはーるーん!!","とある科学の超電磁砲S","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100008,100049,"[O.N.G.E.K.I.]1.05-E-0020" +1,100323,"【SSR】御坂 美琴[超電磁砲]",21001,"超電磁砲","とある科学の超電磁砲S","Leaf","-",3,"55,252,275,290,302,312,0,0,0,317",100033,100074,"[O.N.G.E.K.I.]1.05-E-0013" +1,100324,"【SSR】御坂 美琴[学園都市の電撃姫]",21001,"学園都市の電撃姫","とある科学の超電磁砲S","Leaf","-",3,"55,252,275,290,302,312,0,0,0,317",105000,105001,"[O.N.G.E.K.I.]1.05-E-0014" +1,100325,"【R】ペコリーヌ[美食殿]",22001,"美食殿","プリンセスコネクト!Re:Dive","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.05-E-0024" +1,100326,"【R】コッコロ[美食殿]",22002,"美食殿","プリンセスコネクト!Re:Dive","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.05-E-0027" +1,100327,"【R】キャル[美食殿]",22003,"美食殿","プリンセスコネクト!Re:Dive","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.05-E-0030" +1,100328,"【SR】ペコリーヌ[ユースティアナ・フォン・アストライア]",22001,"ユースティアナ・フォン・アストライア","プリンセスコネクト!Re:Dive","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.05-E-0025" +1,100329,"【SR】コッコロ[棗こころ]",22002,"棗こころ","プリンセスコネクト!Re:Dive","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.05-E-0028" +1,100330,"【SR】キャル[百地希留耶]",22003,"百地希留耶","プリンセスコネクト!Re:Dive","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",105004,105005,"[O.N.G.E.K.I.]1.05-E-0031" +1,100331,"【SR】ペコリーヌ[プリンセスストライク]",22001,"プリンセスストライク","プリンセスコネクト!Re:Dive","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",105008,105009,"[O.N.G.E.K.I.]1.05-E-0026" +1,100332,"【SR】コッコロ[オーロラ]",22002,"オーロラ","プリンセスコネクト!Re:Dive","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100028,100069,"[O.N.G.E.K.I.]1.05-E-0029" +1,100333,"【SR】キャル[グリムバースト]",22003,"グリムバースト","プリンセスコネクト!Re:Dive","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.05-E-0032" +1,100334,"【SSR】ペコリーヌ[プリンセスフォース]",22001,"プリンセスフォース","プリンセスコネクト!Re:Dive","Aqua","-",3,"55,252,275,290,302,312,0,0,0,317",100036,100077,"[O.N.G.E.K.I.]1.05-E-0033" +1,100335,"【SSR】コッコロ[精霊の啓示]",22002,"精霊の啓示","プリンセスコネクト!Re:Dive","Aqua","-",3,"55,252,275,290,302,312,0,0,0,317",105006,105007,"[O.N.G.E.K.I.]1.05-E-0034" +1,100336,"【SR】星咲 あかり[ギーコギーコでトントントン]",1000,"ギーコギーコでトントントン","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105142,105143,"[O.N.G.E.K.I.]1.05-0069" +1,100337,"【SR】藤沢 柚子[ギーコギーコでトントントン]",1001,"ギーコギーコでトントントン","奏坂学園","Leaf","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105142,105143,"[O.N.G.E.K.I.]1.05-0070" +1,100338,"【SR】三角 葵[ギーコギーコでトントントン]",1002,"ギーコギーコでトントントン","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105142,105143,"[O.N.G.E.K.I.]1.05-0071" +0,100339,"【R】星咲 あかり[シュータードレス:全身ver.]",1000,"シュータードレス:全身ver.","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100110,100111,"[O.N.G.E.K.I.]1.00-0006" +0,100340,"【R】藤沢 柚子[シュータードレス:全身ver.]",1001,"シュータードレス:全身ver.","奏坂学園","Leaf","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100112,100113,"[O.N.G.E.K.I.]1.00-0016" +0,100341,"【R】三角 葵[シュータードレス:全身ver.]",1002,"シュータードレス:全身ver.","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100114,100115,"[O.N.G.E.K.I.]1.00-0026" +0,100342,"【R】高瀬 梨緒[シュータードレス:全身ver.]",1003,"シュータードレス:全身ver.","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100120,100121,"[O.N.G.E.K.I.]1.00-0035" +0,100343,"【R】結城 莉玖[シュータードレス:全身ver.]",1004,"シュータードレス:全身ver.","奏坂学園","Fire","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100116,100117,"[O.N.G.E.K.I.]1.00-0044" +0,100344,"【R】藍原 椿[シュータードレス:全身ver.]",1005,"シュータードレス:全身ver.","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100118,100119,"[O.N.G.E.K.I.]1.00-0053" +0,100345,"【R】早乙女 彩華[シュータードレス:全身ver.]",1006,"シュータードレス:全身ver.","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100126,100127,"[O.N.G.E.K.I.]1.00-0066" +0,100346,"【R】桜井 春菜[シュータードレス:全身ver.]",1007,"シュータードレス:全身ver.","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100122,100123,"[O.N.G.E.K.I.]1.00-0061" +0,100347,"【R】九條 楓[シュータードレス:全身ver.]",1008,"シュータードレス:全身ver.","奏坂学園","Leaf","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100124,100125,"[O.N.G.E.K.I.]1.00-0071" +0,100348,"【R】柏木 咲姫[シュータードレス:全身ver.]",1009,"シュータードレス:全身ver.","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100132,100133,"[O.N.G.E.K.I.]1.00-0092" +0,100349,"【R】井之原 小星[シュータードレス:全身ver.]",1010,"シュータードレス:全身ver.","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100130,100131,"[O.N.G.E.K.I.]1.00-0085" +0,100350,"【R】逢坂 茜[シュータードレス:全身ver.]",1011,"シュータードレス:全身ver.","奏坂学園","Fire","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100128,100129,"[O.N.G.E.K.I.]1.00-0101" +0,100351,"【R】珠洲島 有栖[シュータードレス:全身ver.]",1012,"シュータードレス:全身ver.","奏坂学園","Aqua","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100134,100135,"[O.N.G.E.K.I.]1.00-0113" +1,100352,"【SR】御坂 美琴[ゲコラー]",21001,"ゲコラー","とある科学の超電磁砲S","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",105138,105139,"[O.N.G.E.K.I.]1.05-E-0011" +1,100353,"【SR】御坂 美琴[雷撃の槍]",21001,"雷撃の槍","とある科学の超電磁砲S","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",105140,105141,"[O.N.G.E.K.I.]1.05-E-0012" +1,100375,"【R】アリサ[森の弓使い]",22004,"森の弓使い","プリンセスコネクト!Re:Dive","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.05-E-0035" +1,100376,"【SR】アリサ[乙女な転校生]",22004,"乙女な転校生","プリンセスコネクト!Re:Dive","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",105010,105011,"[O.N.G.E.K.I.]1.05-E-0036" +1,100377,"【SR】アリサ[深緑の射手]",22004,"深緑の射手","プリンセスコネクト!Re:Dive","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100027,100068,"[O.N.G.E.K.I.]1.05-E-0037" +1,100391,"【R】ゆんゆん[アークウィザード]",11004,"アークウィザード","この素晴らしい世界に祝福を!2","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.05-E-0058" +1,100392,"【SR】ゆんゆん[我が名はゆんゆん]",11004,"我が名はゆんゆん","この素晴らしい世界に祝福を!2","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",105024,105025,"[O.N.G.E.K.I.]1.05-E-0059" +1,100393,"【SSR】ゆんゆん[紅魔族の長となるもの]",11004,"紅魔族の長となるもの","この素晴らしい世界に祝福を!2","Fire","-",3,"55,252,275,290,302,312,0,0,0,317",100138,100139,"[O.N.G.E.K.I.]1.05-E-0061" +1,100394,"【SSR】めぐみん[紅魔族随一の天才]",11001,"紅魔族随一の天才","この素晴らしい世界に祝福を!2","Fire","-",3,"55,252,275,290,302,312,0,0,0,317",105020,105021,"[O.N.G.E.K.I.]1.05-E-0060" +2,100395,"【R】因幡 てゐ[東方永夜抄]",2013,"東方永夜抄","東方永夜抄","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0044" +2,100396,"【R】鈴仙・優曇華院・イナバ[東方永夜抄]",2014,"東方永夜抄","東方永夜抄","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.10-E-0042" +2,100397,"【R】八意 永琳[東方永夜抄]",2015,"東方永夜抄","東方永夜抄","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100003,100044,"[O.N.G.E.K.I.]1.10-E-0039" +2,100398,"【R】蓬莱山 輝夜[東方永夜抄]",2016,"東方永夜抄","東方永夜抄","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0036" +2,100399,"【R】藤原 妹紅[東方永夜抄]",2017,"東方永夜抄","東方永夜抄","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0046" +2,100400,"【SR】因幡 てゐ[幸運の素兎]",2013,"幸運の素兎","東方永夜抄","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",105042,105043,"[O.N.G.E.K.I.]1.10-E-0045" +2,100401,"【SR】鈴仙・優曇華院・イナバ[狂気の月の兎]",2014,"狂気の月の兎","東方永夜抄","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.10-E-0043" +2,100402,"【SR】八意 永琳[月の頭脳]",2015,"月の頭脳","東方永夜抄","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100028,100069,"[O.N.G.E.K.I.]1.10-E-0040" +2,100403,"【SR】蓬莱山 輝夜[永遠と須臾の罪人]",2016,"永遠と須臾の罪人","東方永夜抄","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.10-E-0037" +2,100404,"【SR】藤原 妹紅[蓬莱の人の形]",2017,"蓬莱の人の形","東方永夜抄","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110002,110003,"[O.N.G.E.K.I.]1.10-E-0047" +2,100405,"【SSR】八意 永琳[天呪「アポロ13」]",2015,"天呪「アポロ13」","東方永夜抄","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",100033,100074,"[O.N.G.E.K.I.]1.10-E-0041" +2,100406,"【SSR】蓬莱山 輝夜[神宝「蓬莱の玉の枝 -夢色の郷-」]",2016,"神宝「蓬莱の玉の枝 -夢色の郷-」","東方永夜抄","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]1.10-E-0038" +2,100407,"【SSR】柏木 美亜[にゃんにゃんファンファーレ]",1013,"にゃんにゃんファンファーレ","奏坂学園","Fire","中学2年生",3,"65,262,285,300,312,322,0,0,0,327",110004,110005,"[O.N.G.E.K.I.]1.10-0023" +1,100408,"【SR】日向 美海[絶対の可能性]",20001,"絶対の可能性","アンジュ・ヴィエルジュ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",105016,105017,"[O.N.G.E.K.I.]1.05-E-0052" +1,100409,"【SR】彩城 天音[サンシャイン・ヒロイン]",20002,"サンシャイン・ヒロイン","アンジュ・ヴィエルジュ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",105010,105011,"[O.N.G.E.K.I.]1.05-E-0053" +1,100410,"【SR】東条 遥[蒼輝絢爛]",20003,"蒼輝絢爛","アンジュ・ヴィエルジュ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",105018,105019,"[O.N.G.E.K.I.]1.05-E-0054" +2,100411,"【R】高瀬 梨緒[クリスマス]",1003,"クリスマス","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",110006,110007,"[O.N.G.E.K.I.]1.10-0046" +2,100412,"【R】結城 莉玖[クリスマス]",1004,"クリスマス","奏坂学園","Fire","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",110006,110007,"[O.N.G.E.K.I.]1.10-0048" +2,100413,"【R】藍原 椿[クリスマス]",1005,"クリスマス","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",110006,110007,"[O.N.G.E.K.I.]1.10-0050" +2,100414,"【R】桜井 春菜[着物]",1007,"着物","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",110008,110009,"[O.N.G.E.K.I.]1.10-0060" +2,100415,"【R】早乙女 彩華[着物]",1006,"着物","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",110010,110011,"[O.N.G.E.K.I.]1.10-0059" +2,100416,"【R】井之原 小星[私服・冬]",1010,"私服・冬","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",110012,110013,"[O.N.G.E.K.I.]1.10-0069" +2,100417,"【R】柏木 咲姫[私服・冬]",1009,"私服・冬","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",110014,110015,"[O.N.G.E.K.I.]1.10-0068" +2,100418,"【SR】星咲 あかり[あま~いひと時]",1000,"あま~いひと時","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",110016,110017,"[O.N.G.E.K.I.]1.10-0001" +2,100419,"【SR】藍原 椿[街角ランデブー]",1005,"街角ランデブー","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",110018,110019,"[O.N.G.E.K.I.]1.10-0024" +2,100420,"【SR】早乙女 彩華[次はこっち!]",1006,"次はこっち!","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",100031,100072,"[O.N.G.E.K.I.]1.10-0020" +2,100421,"【SR】珠洲島 有栖[ぐるぐるヴァンパイア]",1012,"ぐるぐるヴァンパイア","奏坂学園","Aqua","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",110030,110031,"[O.N.G.E.K.I.]1.10-0032" +2,100422,"【SR】桜井 春菜[パンプキン・メランコリー]",1007,"パンプキン・メランコリー","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100031,100072,"[O.N.G.E.K.I.]1.10-0031" +2,100423,"【SR】藤沢 柚子[ただいま捜査中]",1001,"ただいま捜査中","奏坂学園","Leaf","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",110034,110035,"[O.N.G.E.K.I.]1.10-0038" +2,100424,"【SR】日向 千夏[謎は全て解けたよ!]",1014,"謎は全て解けたよ!","奏坂学園","Leaf","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",110022,110023,"[O.N.G.E.K.I.]1.10-0044" +2,100425,"【SR】柏木 咲姫[Let's Party!]",1009,"Let's Party!","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",110024,110025,"[O.N.G.E.K.I.]1.10-0052" +2,100426,"【SR】逢坂 茜[エクストリーム・クリスマス]",1011,"エクストリーム・クリスマス","奏坂学園","Fire","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",110026,110027,"[O.N.G.E.K.I.]1.10-0053" +2,100427,"【SR】井之原 小星[だらだらニューイヤー]",1010,"だらだらニューイヤー","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",110028,110029,"[O.N.G.E.K.I.]1.10-0062" +2,100428,"【SR】東雲 つむぎ[次は負けませんから!]",1015,"次は負けませんから!","奏坂学園","Aqua","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",105044,105045,"[O.N.G.E.K.I.]1.10-0063" +2,100429,"【SR】柏木 美亜[ア・ゲ・ル♡]",1013,"ア・ゲ・ル♡","奏坂学園","Fire","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",110032,110033,"[O.N.G.E.K.I.]1.10-0070" +2,100430,"【SR】三角 葵[スイート・イルミネーション]",1002,"スイート・イルミネーション","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",110020,110021,"[O.N.G.E.K.I.]1.10-0066" +2,100431,"【SSR】日向 千夏[全力リトルマーチ]",1014,"全力リトルマーチ","奏坂学園","Leaf","中学2年生",3,"65,262,285,300,312,322,0,0,0,327",110004,110005,"[O.N.G.E.K.I.]1.10-0015" +2,100432,"【SSR】東雲 つむぎ[張り切りゴシックリズム]",1015,"張り切りゴシックリズム","奏坂学園","Aqua","中学2年生",3,"65,262,285,300,312,322,0,0,0,327",110004,110005,"[O.N.G.E.K.I.]1.10-0027" +2,100433,"【SSR】結城 莉玖[オオカミは食いしん坊]",1004,"オオカミは食いしん坊","奏坂学園","Fire","高校1年生",3,"65,262,285,300,312,322,0,0,0,327",105030,105031,"[O.N.G.E.K.I.]1.10-0030" +2,100434,"【SSR】早乙女 彩華[闇夜の挑戦状]",1006,"闇夜の挑戦状","奏坂学園","Aqua","高校3年生",3,"65,262,285,300,312,322,0,0,0,327",110036,110037,"[O.N.G.E.K.I.]1.10-0040" +2,100435,"【SSR】珠洲島 有栖[しあわせデリバリー]",1012,"しあわせデリバリー","奏坂学園","Aqua","高校1年生",3,"65,262,285,300,312,322,0,0,0,327",110038,110039,"[O.N.G.E.K.I.]1.10-0054" +2,100436,"【SSR】結城 莉玖[ホーリーナイト・セッション]",1004,"ホーリーナイト・セッション","奏坂学園","Fire","高校1年生",3,"65,262,285,300,312,322,0,0,0,327",110040,110041,"[O.N.G.E.K.I.]1.10-0049" +2,100437,"【SSR】星咲 あかり[お願い!開運巫女!]",1000,"お願い!開運巫女!","奏坂学園","Fire","高校2年生",3,"65,262,285,300,312,322,0,0,0,327",110042,110043,"[O.N.G.E.K.I.]1.10-0058" +2,100438,"【SSR】九條 楓[風流韻事]",1008,"風流韻事","奏坂学園","Leaf","高校3年生",3,"65,262,285,300,312,322,0,0,0,327",110044,110045,"[O.N.G.E.K.I.]1.10-0061" +2,100439,"【SSR】高瀬 梨緒[アタシがプレゼント!?]",1003,"アタシがプレゼント!?","奏坂学園","Aqua","高校2年生",3,"65,262,285,300,312,322,0,0,0,327",110046,110047,"[O.N.G.E.K.I.]1.10-0067" +2,100440,"【R】日向 千夏[シューター・ドレス]",1014,"シューター・ドレス","奏坂学園","Leaf","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",110012,110013,"[O.N.G.E.K.I.]1.10-0010" +2,100441,"【R】東雲 つむぎ[シューター・ドレス]",1015,"シューター・ドレス","奏坂学園","Aqua","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",110014,110015,"[O.N.G.E.K.I.]1.10-0017" +2,100442,"【SR+】柏木 美亜[いきなりシューターフェス決勝!+]",1013,"いきなりシューターフェス決勝!+","奏坂学園","Fire","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",110048,110049,"[O.N.G.E.K.I.]1.10-0008" +2,100443,"【SR+】日向 千夏[いきなりシューターフェス決勝!+]",1014,"いきなりシューターフェス決勝!+","奏坂学園","Leaf","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",110050,110051,"[O.N.G.E.K.I.]1.10-0014" +2,100444,"【SR+】東雲 つむぎ[いきなりシューターフェス決勝!+]",1015,"いきなりシューターフェス決勝!+","奏坂学園","Aqua","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",110052,110053,"[O.N.G.E.K.I.]1.10-0019" +2,100445,"【R】柏木 美亜[私服]",1013,"私服","奏坂学園","Fire","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",110054,110055,"[O.N.G.E.K.I.]1.10-0043" +2,100446,"【SR+】珠洲島 有栖[ぷれぜんと・ふぉー・ゆー+]",1012,"ぷれぜんと・ふぉー・ゆー+","奏坂学園","Aqua","高校1年生",12,"53,240,263,278,290,300,0,0,0,300",110056,110057,"[O.N.G.E.K.I.]1.10-0072" +2,100447,"【R】日向 千夏[私服]",1014,"私服","奏坂学園","Leaf","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",110058,110059,"[O.N.G.E.K.I.]1.10-0033" +3,100448,"【SR+】日向 千夏[ひよこたちのダ・カーポ+]",1014,"ひよこたちのダ・カーポ+","奏坂学園","Leaf","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",115046,115047,"[O.N.G.E.K.I.]1.15-0039" +2,100449,"【R】東雲 つむぎ[私服]",1015,"私服","奏坂学園","Aqua","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",110060,110061,"[O.N.G.E.K.I.]1.10-0045" +3,100450,"【SR+】東雲 つむぎ[ひよこたちのダ・カーポ+]",1015,"ひよこたちのダ・カーポ+","奏坂学園","Aqua","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",115050,115051,"[O.N.G.E.K.I.]1.15-0052" +2,100451,"【N】日向 千夏",1014,"","奏坂学園","Leaf","中学2年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"[O.N.G.E.K.I.]1.10-0009" +2,100452,"【N】東雲 つむぎ",1015,"","奏坂学園","Aqua","中学2年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"[O.N.G.E.K.I.]1.10-0016" +1,100453,"【SR】茜ニャン[(世界の半分はいただくにゃん!)]",5,"(世界の半分はいただくにゃん!)","プロモーション","Fire","-",2,"1,30,35,40,45,50,0,0,0,50",105132,105133,"[O.N.G.E.K.I.]1.05-0035" +1,100454,"【SSR】星咲 あかり[1st Anniversary]",1000,"1st Anniversary","プロモーション","Fire","高校2年生",3,"52,237,252,267,282,297,0,0,0,302",105146,105146,"[O.N.G.E.K.I.]1.05-0072" +1,100455,"【SR】茜ニャン[(とりあえず爆発させとくにゃん?)]",5,"(とりあえず爆発させとくにゃん?)","プロモーション","Fire","-",2,"1,30,35,40,45,50,0,0,0,50",105134,105135,"[O.N.G.E.K.I.]1.05-0036" +1,100456,"【SR】茜ニャン[(まったくネコ使いが荒いにゃん……)]",5,"(まったくネコ使いが荒いにゃん……)","プロモーション","Fire","-",2,"1,30,35,40,45,50,0,0,0,50",105136,105137,"[O.N.G.E.K.I.]1.05-0037" +1,100457,"【R】あおニャン[松丸君]",2,"松丸君","-","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",0,0,"[O.N.G.E.K.I.]1.05-****" +2,100458,"【SSR】高瀬 梨緒[デイドリーム・フェアリーズ]",1003,"デイドリーム・フェアリーズ","奏坂学園","Aqua","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",100040,100081,"[O.N.G.E.K.I.]1.10-0002" +2,100459,"【SSR】結城 莉玖[デイドリーム・フェアリーズ]",1004,"デイドリーム・フェアリーズ","奏坂学園","Fire","高校1年生",3,"65,270,291,304,314,322,0,0,0,327",100038,100079,"[O.N.G.E.K.I.]1.10-0003" +2,100460,"【SSR】藍原 椿[デイドリーム・フェアリーズ]",1005,"デイドリーム・フェアリーズ","奏坂学園","Leaf","高校1年生",3,"65,270,291,304,314,322,0,0,0,327",100039,100080,"[O.N.G.E.K.I.]1.10-0004" +2,100461,"【SSR】高瀬 梨緒[聖夜のハッピーベル]",1003,"聖夜のハッピーベル","奏坂学園","Aqua","高校2年生",3,"65,262,285,300,312,322,0,0,0,327",110062,110063,"[O.N.G.E.K.I.]1.10-0047" +2,100462,"【SSR】藍原 椿[ハッピー・ゴー・ラウンド]",1005,"ハッピー・ゴー・ラウンド","奏坂学園","Leaf","高校1年生",3,"65,262,285,300,312,322,0,0,0,327",110064,110065,"[O.N.G.E.K.I.]1.10-0051" +2,100463,"【R】アリス・マーガトロイド[東方Project]",2018,"東方Project","東方Project","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0135" +2,100464,"【SR】アリス・マーガトロイド[七色の人形使い]",2018,"七色の人形使い","東方Project","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.10-E-0136" +2,100465,"【SSR】かなで[エンジェルズウィング]",23001,"エンジェルズウィング","Angel Beats!","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",110070,110071,"[O.N.G.E.K.I.]1.10-E-0124" +2,100466,"【SSR】かなで[水辺の天使]",23001,"水辺の天使","Angel Beats!","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",110068,110069,"[O.N.G.E.K.I.]1.10-E-0123" +2,100472,"【SR+】柏木 美亜[ガールズ・ウォッチ+]",1013,"ガールズ・ウォッチ+","奏坂学園","Fire","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",110074,110075,"[O.N.G.E.K.I.]1.10-0035" +2,100473,"【SR+】日向 千夏[大好き☆アタック+]",1014,"大好き☆アタック+","奏坂学園","Leaf","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",110076,110077,"[O.N.G.E.K.I.]1.10-0065" +2,100474,"【SR+】東雲 つむぎ[背伸びしたいお年頃+]",1015,"背伸びしたいお年頃+","奏坂学園","Aqua","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",110078,110079,"[O.N.G.E.K.I.]1.10-0037" +3,100475,"【SR+】柏木 咲姫[コスプレ喫茶へようこそ+]",1009,"コスプレ喫茶へようこそ+","奏坂学園","Aqua","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",115042,115043,"[O.N.G.E.K.I.]1.15-0006" +3,100476,"【SR+】桜井 春菜[クレープはママの味+]",1007,"クレープはママの味+","奏坂学園","Fire","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",115044,115045,"[O.N.G.E.K.I.]1.15-0015" +3,100477,"【SR+】逢坂 茜[逢坂茜の挑戦状+]",1011,"逢坂茜の挑戦状+","奏坂学園","Fire","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",110134,110135,"[O.N.G.E.K.I.]1.15-0024" +2,100483,"【R】かなで[死んだ世界戦線]",23001,"死んだ世界戦線","Angel Beats!","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.10-E-0120" +2,100484,"【R】ゆり[死んだ世界戦線]",23002,"死んだ世界戦線","Angel Beats!","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.10-E-0125" +2,100485,"【R】ユイ[死んだ世界戦線]",23003,"死んだ世界戦線","Angel Beats!","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0128" +2,100486,"【SR】かなで[ハンドソニック]",23001,"ハンドソニック","Angel Beats!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.10-E-0121" +2,100487,"【SR】ゆり[運命と神に抗う少女]",23002,"運命と神に抗う少女","Angel Beats!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.10-E-0126" +2,100488,"【SR】ユイ[Girls Dead Monster]",23003,"Girls Dead Monster","Angel Beats!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110138,110139,"[O.N.G.E.K.I.]1.10-E-0129" +1,100489,"【SSR】星咲 あかり[「オンゲキ」LIVE vol.1]",1000,"「オンゲキ」LIVE vol.1","プロモーション","Fire","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",100033,100074,"[O.N.G.E.K.I.]Special Card" +2,100490,"【SR】かなで[私は天使なんかじゃないわ]",23001,"私は天使なんかじゃないわ","Angel Beats!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110080,110081,"[O.N.G.E.K.I.]1.10-E-0122" +2,100491,"【SR】ゆり[本当は馬鹿よ]",23002,"本当は馬鹿よ","Angel Beats!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.10-E-0127" +2,100492,"【SR】ユイ[要はアホですねっ!]",23003,"要はアホですねっ!","Angel Beats!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110082,110083,"[O.N.G.E.K.I.]1.10-E-0130" +2,100493,"【R】春日部 ハル[777☆SISTERS]",24001,"777☆SISTERS","Tokyo 7th シスターズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0075" +2,100494,"【R】天堂寺 ムスビ[777☆SISTERS]",24001,"777☆SISTERS","Tokyo 7th シスターズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",105004,105005,"[O.N.G.E.K.I.]1.10-E-0078" +2,100495,"【R】角森 ロナ[777☆SISTERS]",24001,"777☆SISTERS","Tokyo 7th シスターズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0080" +2,100496,"【R】野ノ原 ヒメ [777☆SISTERS]",24001,"777☆SISTERS","Tokyo 7th シスターズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100001,100042,"[O.N.G.E.K.I.]1.10-E-0082" +2,100497,"【R】芹沢 モモカ[777☆SISTERS]",24001,"777☆SISTERS","Tokyo 7th シスターズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.10-E-0084" +2,100498,"【R】臼田 スミレ[777☆SISTERS]",24001,"777☆SISTERS","Tokyo 7th シスターズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",105112,105113,"[O.N.G.E.K.I.]1.10-E-0086" +2,100499,"【R】神城 スイ[777☆SISTERS]",24001,"777☆SISTERS","Tokyo 7th シスターズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.10-E-0088" +2,100500,"【R】久遠寺 シズカ [777☆SISTERS]",24001,"777☆SISTERS","Tokyo 7th シスターズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.10-E-0090" +2,100501,"【R】アレサンドラ・スース[777☆SISTERS]",24001,"777☆SISTERS","Tokyo 7th シスターズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.10-E-0092" +2,100502,"【R】晴海 サワラ[777☆SISTERS]",24001,"777☆SISTERS","Tokyo 7th シスターズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0094" +2,100503,"【R】晴海 カジカ[777☆SISTERS]",24001,"777☆SISTERS","Tokyo 7th シスターズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0096" +2,100504,"【R】晴海 シンジュ [777☆SISTERS]",24001,"777☆SISTERS","Tokyo 7th シスターズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110084,110085,"[O.N.G.E.K.I.]1.10-E-0098" +2,100505,"【SR】春日部 ハル[星屑☆シーカー]",24001,"星屑☆シーカー","Tokyo 7th シスターズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.10-E-0076" +2,100506,"【SR】天堂寺 ムスビ[Girls Talk!!]",24001,"Girls Talk!!","Tokyo 7th シスターズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.10-E-0079" +2,100507,"【SR】角森 ロナ[星屑☆シーカー]",24001,"星屑☆シーカー","Tokyo 7th シスターズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.10-E-0081" +2,100508,"【SR】野ノ原 ヒメ [星屑☆シーカー]",24001,"星屑☆シーカー","Tokyo 7th シスターズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100026,100067,"[O.N.G.E.K.I.]1.10-E-0083" +2,100509,"【SR】芹沢 モモカ[星屑☆シーカー]",24001,"星屑☆シーカー","Tokyo 7th シスターズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100016,100057,"[O.N.G.E.K.I.]1.10-E-0085" +2,100510,"【SR】臼田 スミレ[お願い☆My Boy]",24001,"お願い☆My Boy","Tokyo 7th シスターズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110086,110087,"[O.N.G.E.K.I.]1.10-E-0087" +2,100511,"【SR】神城 スイ[お願い☆My Boy]",24001,"お願い☆My Boy","Tokyo 7th シスターズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.10-E-0089" +2,100512,"【SR】久遠寺 シズカ [お願い☆My Boy]",24001,"お願い☆My Boy","Tokyo 7th シスターズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.10-E-0091" +2,100513,"【SR】アレサンドラ・スース[Girls Talk!!]",24001,"Girls Talk!!","Tokyo 7th シスターズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110088,110089,"[O.N.G.E.K.I.]1.10-E-0093" +2,100514,"【SR】晴海 サワラ[セカイのヒミツ]",24001,"セカイのヒミツ","Tokyo 7th シスターズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.10-E-0095" +2,100515,"【SR】晴海 カジカ[セカイのヒミツ]",24001,"セカイのヒミツ","Tokyo 7th シスターズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.10-E-0097" +2,100516,"【SR】晴海 シンジュ [セカイのヒミツ]",24001,"セカイのヒミツ","Tokyo 7th シスターズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100027,100068,"[O.N.G.E.K.I.]1.10-E-0099" +2,100517,"【SR+】春日部 ハル[スタートライン]",24001,"スタートライン","Tokyo 7th シスターズ","Fire","-",12,"53,240,263,278,290,300,0,0,0,300",110090,110091,"[O.N.G.E.K.I.]1.10-E-0077" +2,100518,"【R】草津 結衣奈[SPRiNGS]",25001,"SPRiNGS","温泉むすめ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0048" +2,100519,"【R】箱根 彩耶[SPRiNGS]",25001,"SPRiNGS","温泉むすめ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110054,110055,"[O.N.G.E.K.I.]1.10-E-0051" +2,100520,"【R】秋保 那菜子[SPRiNGS]",25001,"SPRiNGS","温泉むすめ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.10-E-0053" +2,100521,"【R】有馬 輪花[SPRiNGS]",25001,"SPRiNGS","温泉むすめ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110054,110055,"[O.N.G.E.K.I.]1.10-E-0055" +2,100522,"【R】道後 泉海[SPRiNGS]",25001,"SPRiNGS","温泉むすめ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100003,100044,"[O.N.G.E.K.I.]1.10-E-0057" +2,100523,"【R】登別 綾瀬[SPRiNGS]",25001,"SPRiNGS","温泉むすめ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0059" +2,100524,"【R】下呂 美月[SPRiNGS]",25001,"SPRiNGS","温泉むすめ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100003,100044,"[O.N.G.E.K.I.]1.10-E-0061" +2,100525,"【R】有馬 楓花[SPRiNGS]",25001,"SPRiNGS","温泉むすめ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.10-E-0063" +2,100526,"【R】奏・バーデン・由布院[SPRiNGS]",25001,"SPRiNGS","温泉むすめ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0065" +2,100527,"【SR】草津 結衣奈[SPicA]",25001,"SPicA","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.10-E-0049" +2,100528,"【SR】箱根 彩耶[雪月花]",25001,"雪月花","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110094,110095,"[O.N.G.E.K.I.]1.10-E-0052" +2,100529,"【SR】秋保 那菜子[しゃんぷーはっと]",25001,"しゃんぷーはっと","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",105052,105053,"[O.N.G.E.K.I.]1.10-E-0054" +2,100530,"【SR】有馬 輪花[雪月花]",25001,"雪月花","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.10-E-0056" +2,100531,"【SR】道後 泉海[SPicA]",25001,"SPicA","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110096,110097,"[O.N.G.E.K.I.]1.10-E-0058" +2,100532,"【SR】登別 綾瀬[しゃんぷーはっと]",25001,"しゃんぷーはっと","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110022,110023,"[O.N.G.E.K.I.]1.10-E-0060" +2,100533,"【SR】下呂 美月[しゃんぷーはっと]",25001,"しゃんぷーはっと","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100018,100059,"[O.N.G.E.K.I.]1.10-E-0062" +2,100534,"【SR】有馬 楓花[SPicA]",25001,"SPicA","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100028,100069,"[O.N.G.E.K.I.]1.10-E-0064" +2,100535,"【SR】奏・バーデン・由布院[雪月花]",25001,"雪月花","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.10-E-0066" +2,100536,"【SR】鬼怒川 日向[AKATSUKI]",25001,"AKATSUKI","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",105042,105043,"[O.N.G.E.K.I.]1.10-E-0137" +2,100537,"【SR】熱海 初夏[LUSH STAR☆]",25001,"LUSH STAR☆","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100031,100072,"[O.N.G.E.K.I.]1.10-E-0138" +2,100538,"【SR】黒川 姫楽[Adhara]",25001,"Adhara","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110022,110023,"[O.N.G.E.K.I.]1.10-E-0139" +2,100539,"【SR】伊香保 葉凪[petit corolla]",25001,"petit corolla","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",105052,105053,"[O.N.G.E.K.I.]1.10-E-0140" +2,100540,"【SR+】草津 結衣奈[ゆのはな選抜]",25001,"ゆのはな選抜","温泉むすめ","Aqua","-",12,"53,240,263,278,290,300,0,0,0,300",105010,105011,"[O.N.G.E.K.I.]1.10-E-0050" +2,100541,"【SSR】草津 結衣奈[NOW ON☆SENSATION!!]",25001,"NOW ON☆SENSATION!!","プロモーション","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",110098,110099,"Special Card" +2,100542,"【R】愛城 華恋[聖翔音楽学園 第99期生]",26001,"聖翔音楽学園 第99期生","レヴュースタァライト","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0021" +2,100543,"【R】神楽 ひかり[聖翔音楽学園 第99期生]",26002,"聖翔音楽学園 第99期生","レヴュースタァライト","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0027" +2,100544,"【SR】愛城 華恋[ポジションゼロ]",26001,"ポジションゼロ","レヴュースタァライト","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110100,110101,"[O.N.G.E.K.I.]1.10-E-0022" +2,100545,"【SR】神楽 ひかり[ポジションゼロ]",26002,"ポジションゼロ","レヴュースタァライト","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110102,110103,"[O.N.G.E.K.I.]1.10-E-0028" +2,100546,"【SR】愛城 華恋[2人のハロウィン]",26001,"2人のハロウィン","レヴュースタァライト","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100026,100067,"[O.N.G.E.K.I.]1.10-E-0023" +2,100547,"【SR】神楽 ひかり[2人のハロウィン]",26002,"2人のハロウィン","レヴュースタァライト","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",105094,105095,"[O.N.G.E.K.I.]1.10-E-0029" +2,100548,"【SR】愛城 華恋[私のキラめき]",26001,"私のキラめき","レヴュースタァライト","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.10-E-0024" +2,100549,"【SR】神楽 ひかり[私のキラめき]",26002,"私のキラめき","レヴュースタァライト","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100016,100057,"[O.N.G.E.K.I.]1.10-E-0030" +2,100550,"【SR】愛城 華恋[舞台少女は日々進化中!]",26001,"舞台少女は日々進化中!","レヴュースタァライト","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",105042,105043,"[O.N.G.E.K.I.]1.10-E-0025" +2,100551,"【SSR】愛城 華恋[Show Must Go On]",26001,"Show Must Go On","レヴュースタァライト","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",100033,100074,"[O.N.G.E.K.I.]1.10-E-0026" +2,100552,"【SSR】神楽 ひかり[『運命』の交換]",26002,"『運命』の交換","レヴュースタァライト","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",110104,110105,"[O.N.G.E.K.I.]1.10-E-0031" +2,100553,"【R】サクラ[人形の感覚]",27001,"人形の感覚","プロジェクト東京ドールズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0005" +2,100554,"【R】ミサキ[死神]",27001,"死神","プロジェクト東京ドールズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110106,110107,"[O.N.G.E.K.I.]1.10-E-0007" +2,100555,"【R】シオリ[クロユリの花言葉]",27001,"クロユリの花言葉","プロジェクト東京ドールズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110108,110109,"[O.N.G.E.K.I.]1.10-E-0009" +2,100556,"【R】レイナ[美しき狩人]",27001,"美しき狩人","プロジェクト東京ドールズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110108,110109,"[O.N.G.E.K.I.]1.10-E-0011" +2,100557,"【R】ヒヨ[もう一人の、ワタシ]",27001,"もう一人の、ワタシ","プロジェクト東京ドールズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110106,110107,"[O.N.G.E.K.I.]1.10-E-0013" +2,100558,"【R】ナナミ[魂の叫喚]",27001,"魂の叫喚","プロジェクト東京ドールズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0015" +2,100559,"【R】アヤ[人形の本懐]",27001,"人形の本懐","プロジェクト東京ドールズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110108,110109,"[O.N.G.E.K.I.]1.10-E-0017" +2,100560,"【R】ユキ[人形の呪縛]",27001,"人形の呪縛","プロジェクト東京ドールズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0001" +2,100561,"【R】ヤマダ[全てを壊すもの]",27001,"全てを壊すもの","プロジェクト東京ドールズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110106,110107,"[O.N.G.E.K.I.]1.10-E-0019" +2,100562,"【SR】サクラ[弔イノ仇華]",27001,"弔イノ仇華","プロジェクト東京ドールズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.10-E-0006" +2,100563,"【SR】ミサキ[陰掃ウ眼光]",27001,"陰掃ウ眼光","プロジェクト東京ドールズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110110,110111,"[O.N.G.E.K.I.]1.10-E-0008" +2,100564,"【SR】シオリ[罪ヲ嘆ク聲]",27001,"罪ヲ嘆ク聲","プロジェクト東京ドールズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100026,100067,"[O.N.G.E.K.I.]1.10-E-0010" +2,100565,"【SR】レイナ[深淵ヲ穿ツ]",27001,"深淵ヲ穿ツ","プロジェクト東京ドールズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100028,100069,"[O.N.G.E.K.I.]1.10-E-0012" +2,100566,"【SR】ヒヨ[鮮血ノ暴風]",27001,"鮮血ノ暴風","プロジェクト東京ドールズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110110,110111,"[O.N.G.E.K.I.]1.10-E-0014" +2,100567,"【SR】ナナミ[哀切ノ檻]",27001,"哀切ノ檻","プロジェクト東京ドールズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.10-E-0016" +2,100568,"【SR】アヤ[暗闇ヲ照ラス]",27001,"暗闇ヲ照ラス","プロジェクト東京ドールズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100027,100068,"[O.N.G.E.K.I.]1.10-E-0018" +2,100569,"【SR】ユキ[血涙ヲ注グ]",27001,"血涙ヲ注グ","プロジェクト東京ドールズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.10-E-0002" +2,100570,"【SR】ヤマダ[爛レル猛毒]",27001,"爛レル猛毒","プロジェクト東京ドールズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110110,110111,"[O.N.G.E.K.I.]1.10-E-0020" +2,100571,"【SSR】ユキ[静謐の花園]",27001,"静謐の花園","プロジェクト東京ドールズ","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",110112,110113,"[O.N.G.E.K.I.]1.10-E-0004" +2,100572,"【SR+】ユキ[Sweet♡Wedding]",27001,"Sweet♡Wedding","プロジェクト東京ドールズ","Leaf","-",12,"53,240,263,278,290,300,0,0,0,300",105010,105011,"[O.N.G.E.K.I.]1.10-E-0003" +2,100573,"【R】明坂 芹菜[イロドリミドリ]",6001,"イロドリミドリ","イロドリミドリ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0105" +2,100574,"【R】御形 アリシアナ[イロドリミドリ]",6001,"イロドリミドリ","イロドリミドリ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110084,110085,"[O.N.G.E.K.I.]1.10-E-0108" +2,100575,"【R】天王洲 なずな[イロドリミドリ]",6001,"イロドリミドリ","イロドリミドリ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100032,100073,"[O.N.G.E.K.I.]1.10-E-0110" +2,100576,"【R】小仏 凪[イロドリミドリ]",6001,"イロドリミドリ","イロドリミドリ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110142,110143,"[O.N.G.E.K.I.]1.10-E-0112" +2,100577,"【R】箱部 なる[イロドリミドリ]",6001,"イロドリミドリ","イロドリミドリ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0114" +2,100578,"【R】月鈴 那知[イロドリミドリ]",6001,"イロドリミドリ","イロドリミドリ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0116" +2,100579,"【R】月鈴 白奈[イロドリミドリ]",6001,"イロドリミドリ","イロドリミドリ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100002,100043,"[O.N.G.E.K.I.]1.10-E-0118" +2,100580,"【SR】明坂 芹菜[試練の三送会編]",6001,"試練の三送会編","イロドリミドリ","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",100025,100066,"[O.N.G.E.K.I.]1.10-E-0106" +2,100581,"【SR】御形 アリシアナ[試練の三送会編]",6001,"試練の三送会編","イロドリミドリ","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",100025,100066,"[O.N.G.E.K.I.]1.10-E-0109" +2,100582,"【SR】天王洲 なずな[試練の三送会編]",6001,"試練の三送会編","イロドリミドリ","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",110114,110115,"[O.N.G.E.K.I.]1.10-E-0111" +2,100583,"【SR】小仏 凪[試練の三送会編]",6001,"試練の三送会編","イロドリミドリ","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",100027,100068,"[O.N.G.E.K.I.]1.10-E-0113" +2,100584,"【SR】箱部 なる[試練の三送会編]",6001,"試練の三送会編","イロドリミドリ","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",110116,110117,"[O.N.G.E.K.I.]1.10-E-0115" +2,100585,"【SR】月鈴 那知[試練の三送会編]",6001,"試練の三送会編","イロドリミドリ","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",110116,110117,"[O.N.G.E.K.I.]1.10-E-0117" +2,100586,"【SR】月鈴 白奈[試練の三送会編]",6001,"試練の三送会編","イロドリミドリ","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",100017,100058,"[O.N.G.E.K.I.]1.10-E-0119" +2,100587,"【SR+】明坂 芹菜[7つのメロディ]",6001,"7つのメロディ","イロドリミドリ","Leaf","-",12,"53,240,263,278,290,300,0,0,0,300",110118,110119,"[O.N.G.E.K.I.]1.10-E-0107" +2,100588,"【SR】博麗 霊夢[Halloween]",2000,"Halloween","東方Project","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.10-E-0068" +2,100589,"【SR】霧雨 魔理沙[Halloween]",2001,"Halloween","東方Project","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100027,100068,"[O.N.G.E.K.I.]1.10-E-0069" +2,100590,"【SR】紅 美鈴[Halloween]",2005,"Halloween","東方Project","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.10-E-0073" +2,100591,"【SR】パチュリー・ノーレッジ[Halloween]",2006,"Halloween","東方Project","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110082,110083,"[O.N.G.E.K.I.]1.10-E-0074" +2,100592,"【SR】十六夜 咲夜[Halloween]",2002,"Halloween","東方Project","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110082,110083,"[O.N.G.E.K.I.]1.10-E-0070" +2,100593,"【SR】レミリア・スカーレット[Halloween]",2003,"Halloween","東方Project","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100026,100067,"[O.N.G.E.K.I.]1.10-E-0071" +2,100594,"【SR】フランドール・スカーレット[Halloween]",2004,"Halloween","東方Project","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110122,110123,"[O.N.G.E.K.I.]1.10-E-0072" +2,100595,"【SR+】博麗 霊夢[幻想郷のハロウィン]",2000,"幻想郷のハロウィン","東方Project","Fire","-",12,"53,240,263,278,290,300,0,0,0,300",110124,110125,"[O.N.G.E.K.I.]1.10-E-0067" +2,100596,"【R】キズナアイ[バーチャルYouTuber]",28001,"バーチャルYouTuber","キズナアイ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0032" +2,100597,"【SR】キズナアイ[ぴょこぴょこ]",28001,"ぴょこぴょこ","キズナアイ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.10-E-0033" +2,100598,"【SR】キズナアイ[インテリジェントなスーパーAI]",28001,"インテリジェントなスーパーAI","キズナアイ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100026,100067,"[O.N.G.E.K.I.]1.10-E-0034" +2,100599,"【SR+】キズナアイ[はい、どうもー!]",28001,"はい、どうもー!","キズナアイ","Fire","-",12,"53,240,263,278,290,300,0,0,0,300",105010,105011,"[O.N.G.E.K.I.]1.10-E-0035" +2,100600,"【SR+】初音ミク[Project DIVA - Future Tone -]",5000,"Project DIVA - Future Tone -","バーチャル・シンガー","Aqua","-",12,"53,240,263,278,290,300,0,0,0,300",110126,110127,"[O.N.G.E.K.I.]1.10-E-0134" +2,100601,"【SR】初春 飾利[私だって風紀委員なんだから!]",21003,"私だって風紀委員なんだから!","とある科学の超電磁砲S","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",105096,105097,"[O.N.G.E.K.I.]1.10-E-0103" +2,100602,"【SR】御坂 美琴[逃げられるとでも思ってんの?]",21001,"逃げられるとでも思ってんの?","とある科学の超電磁砲S","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110128,110129,"[O.N.G.E.K.I.]1.10-E-0100" +2,100603,"【SR】佐天 涙子[どーんといってみよーか!]",21004,"どーんといってみよーか!","とある科学の超電磁砲S","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.10-E-0104" +2,100604,"【SR】白井 黒子[おっねえさまーんっ!]",21002,"おっねえさまーんっ!","とある科学の超電磁砲S","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110082,110083,"[O.N.G.E.K.I.]1.10-E-0102" +2,100605,"【SR+】御坂 美琴[じゃあ行くよっ!]",21001,"じゃあ行くよっ!","とある科学の超電磁砲S","Leaf","-",12,"53,240,263,278,290,300,0,0,0,300",110056,110057,"[O.N.G.E.K.I.]1.10-E-0101" +2,100606,"【SR】星咲 あかり[最強 the サマータイム!!!!!]",1000,"最強 the サマータイム!!!!!","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",110130,110131,"[O.N.G.E.K.I.]1.10-0073" +2,100607,"【SR】高瀬 梨緒[最強 the サマータイム!!!!!]",1003,"最強 the サマータイム!!!!!","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",110130,110131,"[O.N.G.E.K.I.]1.10-0074" +2,100608,"【SR】柏木 美亜[最強 the サマータイム!!!!!]",1013,"最強 the サマータイム!!!!!","奏坂学園","Fire","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",110130,110131,"[O.N.G.E.K.I.]1.10-0022" +2,100609,"【SR】日向 千夏[最強 the サマータイム!!!!!]",1014,"最強 the サマータイム!!!!!","奏坂学園","Leaf","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",110130,110131,"[O.N.G.E.K.I.]1.10-0012" +2,100610,"【SR】東雲 つむぎ[最強 the サマータイム!!!!!]",1015,"最強 the サマータイム!!!!!","奏坂学園","Aqua","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",110130,110131,"[O.N.G.E.K.I.]1.10-0026" +2,100611,"【SSR】日向 千夏[ONGEKI Sound Collection 02]",1014,"ONGEKI Sound Collection 02","プロモーション","Leaf","中学2年生",3,"65,262,285,300,312,322,0,0,0,327",105144,105145,"[O.N.G.E.K.I.]1.10-P0001" +2,100612,"【SR+】日向 千夏[ONGEKI Memorial Soundtrack Himawari]",1014,"ONGEKI Memorial Soundtrack Himawari","奏坂学園","Leaf","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",110132,110133,"[O.N.G.E.K.I.]1.10-0075" +2,100613,"【SR+】柏木 美亜[ONGEKI Vocal Collection 06]",1013,"ONGEKI Vocal Collection 06","プロモーション","Fire","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",110134,110135,"[O.N.G.E.K.I.]1.10-P0003" +2,100614,"【SR+】日向 千夏[ONGEKI Vocal Collection 06]",1014,"ONGEKI Vocal Collection 06","プロモーション","Leaf","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",110134,110135,"[O.N.G.E.K.I.]1.10-P0002" +2,100615,"【SR+】東雲 つむぎ[ONGEKI Vocal Collection 06]",1015,"ONGEKI Vocal Collection 06","プロモーション","Aqua","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",110134,110135,"[O.N.G.E.K.I.]1.10-P0004" +3,100616,"【SR+】早乙女 彩華[ONGEKI Vocal Collection 07]",1006,"ONGEKI Vocal Collection 07","プロモーション","Aqua","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",110146,110147,"[O.N.G.E.K.I.]1.15-P-0002" +3,100617,"【SR+】井之原 小星[ONGEKI Vocal Collection 07]",1010,"ONGEKI Vocal Collection 07","プロモーション","Leaf","高校1年生",12,"53,240,263,278,290,300,0,0,0,300",110146,110147,"[O.N.G.E.K.I.]1.15-P-0003" +3,100618,"【SR+】逢坂 茜[ONGEKI Vocal Collection 07]",1011,"ONGEKI Vocal Collection 07","プロモーション","Fire","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",110146,110147,"[O.N.G.E.K.I.]1.15-P-0001" +4,100619,"【R】射命丸 文[東方Project]",2019,"東方Project","東方Project","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0137" +4,100620,"【R】古明地 さとり[東方Project]",2020,"東方Project","東方Project 古明地姉妹編","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0071" +4,100621,"【R】古明地 こいし[東方Project]",2021,"東方Project","東方Project 古明地姉妹編","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100001,100042,"[O.N.G.E.K.I.]1.20-E-0074" +4,100622,"【SR】射命丸 文[里に最も近い天狗]",2019,"里に最も近い天狗","東方Project","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0138" +4,100623,"【SR】古明地 さとり[怨霊も恐れ怯む少女]",2020,"怨霊も恐れ怯む少女","東方Project 古明地姉妹編","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110136,110137,"[O.N.G.E.K.I.]1.20-E-0072" +4,100624,"【SR】古明地 こいし[閉じた恋の瞳]",2021,"閉じた恋の瞳","東方Project 古明地姉妹編","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0075" +4,100625,"【SSR】射命丸 文[「無双風神」]",2019,"「無双風神」","東方Project","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",110070,110071,"[O.N.G.E.K.I.]1.20-E-0191" +4,100626,"【SSR】古明地 さとり[想起「テリブルスーヴニール」]",2020,"想起「テリブルスーヴニール」","東方Project 古明地姉妹編","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",115030,115031,"[O.N.G.E.K.I.]1.20-E-0073" +4,100627,"【SSR】古明地 こいし[本能「イドの解放」]",2021,"本能「イドの解放」","東方Project 古明地姉妹編","Fire","-",3,"60,265,286,299,309,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]1.20-E-0076" +3,100631,"【SR+】星咲 あかり[仲直りのフィナーレ+]",1000,"仲直りのフィナーレ+","奏坂学園","Fire","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",115054,115055,"[O.N.G.E.K.I.]1.15-0054" +3,100632,"【SR+】藤沢 柚子[仲直りのフィナーレ+]",1001,"仲直りのフィナーレ+","奏坂学園","Leaf","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",115056,115057,"[O.N.G.E.K.I.]1.15-0064" +3,100633,"【SR+】三角 葵[仲直りのフィナーレ+]",1002,"仲直りのフィナーレ+","奏坂学園","Aqua","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",115058,115059,"[O.N.G.E.K.I.]1.15-0076" +3,100634,"【SR】日向 千夏[ひよこたちのダ・カーポ]",1014,"ひよこたちのダ・カーポ","奏坂学園","Leaf","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",100025,100066,"[O.N.G.E.K.I.]1.15-0040" +3,100636,"【SR】東雲 つむぎ[ひよこたちのダ・カーポ]",1015,"ひよこたちのダ・カーポ","奏坂学園","Aqua","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",100025,100066,"[O.N.G.E.K.I.]1.15-0041" +3,100637,"【SR】星咲 あかり[仲直りのフィナーレ]",1000,"仲直りのフィナーレ","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",115052,115053,"[O.N.G.E.K.I.]1.15-0055" +3,100638,"【SR】藤沢 柚子[仲直りのフィナーレ]",1001,"仲直りのフィナーレ","奏坂学園","Leaf","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",115052,115053,"[O.N.G.E.K.I.]1.15-0057" +3,100639,"【SR】三角 葵[仲直りのフィナーレ]",1002,"仲直りのフィナーレ","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",115052,115053,"[O.N.G.E.K.I.]1.15-0059" +2,100640,"【SR】柏木 美亜[いきなりシューターフェス決勝!]",1013,"いきなりシューターフェス決勝!","奏坂学園","Fire","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",110116,110117,"[O.N.G.E.K.I.]1.10-0007" +2,100641,"【SR】日向 千夏[いきなりシューターフェス決勝!]",1014,"いきなりシューターフェス決勝!","奏坂学園","Leaf","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",110116,110117,"[O.N.G.E.K.I.]1.10-0013" +2,100642,"【SR】東雲 つむぎ[いきなりシューターフェス決勝!]",1015,"いきなりシューターフェス決勝!","奏坂学園","Aqua","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",110116,110117,"[O.N.G.E.K.I.]1.10-0018" +2,100643,"【SR】珠洲島 有栖[ぷれぜんと・ふぉー・ゆー]",1012,"ぷれぜんと・ふぉー・ゆー","奏坂学園","Aqua","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",105042,105043,"[O.N.G.E.K.I.]1.10-0071" +2,100644,"【SR】柏木 美亜[ガールズ・ウォッチ]",1013,"ガールズ・ウォッチ","奏坂学園","Fire","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",110136,110137,"[O.N.G.E.K.I.]1.10-0034" +2,100645,"【SR】日向 千夏[大好き☆アタック]",1014,"大好き☆アタック","奏坂学園","Leaf","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",110138,110139,"[O.N.G.E.K.I.]1.10-0064" +2,100646,"【SR】東雲 つむぎ[背伸びしたいお年頃]",1015,"背伸びしたいお年頃","奏坂学園","Aqua","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",110140,110141,"[O.N.G.E.K.I.]1.10-0036" +3,100647,"【R】柏木 美亜[私服 表情:ふしゃぁぁぁぁっ!]",1013,"私服 表情:ふしゃぁぁぁぁっ!","奏坂学園","Fire","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",110058,110059,"[O.N.G.E.K.I.]1.15-0010" +3,100648,"【R】日向 千夏[私服 表情:ぎゅっ]",1014,"私服 表情:ぎゅっ","奏坂学園","Leaf","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",110060,110061,"[O.N.G.E.K.I.]1.15-0021" +3,100649,"【R】東雲 つむぎ[私服 表情:憤り]",1015,"私服 表情:憤り","奏坂学園","Aqua","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",110058,110059,"[O.N.G.E.K.I.]1.15-0046" +3,100650,"【R】柏木 美亜[シュータードレス:全身ver.]",1013,"シュータードレス:全身ver.","奏坂学園","Fire","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",115148,115149,"[O.N.G.E.K.I.]1.15-0011" +3,100651,"【R】日向 千夏[シュータードレス:全身ver.]",1014,"シュータードレス:全身ver.","奏坂学園","Leaf","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",110084,110085,"[O.N.G.E.K.I.]1.15-0012" +3,100652,"【R】東雲 つむぎ[シュータードレス:全身ver.]",1015,"シュータードレス:全身ver.","奏坂学園","Aqua","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",115146,115147,"[O.N.G.E.K.I.]1.15-0013" +2,100653,"【R】柏木 美亜[制服 表情:にゃふふふふ♡]",1013,"制服 表情:にゃふふふふ♡","奏坂学園","Fire","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",110142,110143,"[O.N.G.E.K.I.]1.10-0021" +2,100654,"【R】日向 千夏[制服 表情:わーい!]",1014,"制服 表情:わーい!","奏坂学園","Leaf","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",100032,100073,"[O.N.G.E.K.I.]1.10-0011" +2,100655,"【R】東雲 つむぎ[制服 表情:得意げ]",1015,"制服 表情:得意げ","奏坂学園","Aqua","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",105112,105113,"[O.N.G.E.K.I.]1.10-0025" +2,100656,"【SR】初音ミク[Project DIVA - f -]",5000,"Project DIVA - f -","バーチャル・シンガー","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.10-E-0131" +2,100657,"【SR】初音ミク[Project DIVA - F 2nd -]",5000,"Project DIVA - F 2nd -","バーチャル・シンガー","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110144,110145,"[O.N.G.E.K.I.]1.10-E-0132" +2,100658,"【SR】初音ミク[Project DIVA - X -]",5000,"Project DIVA - X -","バーチャル・シンガー","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100028,100069,"[O.N.G.E.K.I.]1.10-E-0133" +2,100659,"【SSR】星咲 あかり[「これから」を奏でていこうよ!!]",1000,"「これから」を奏でていこうよ!!","プロモーション","Fire","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"Special Card" +2,100660,"【SSR】藤沢 柚子[「これから」を奏でていこうよ!!]",1001,"「これから」を奏でていこうよ!!","プロモーション","Leaf","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",100033,100074,"Special Card" +3,100661,"【SSR】三角 葵[「これから」を奏でていこうよ!!]",1002,"「これから」を奏でていこうよ!!","プロモーション","Aqua","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",100033,100074,"Special Card" +3,100662,"【R】日向 千夏[体操着]",1014,"体操着","奏坂学園","Leaf","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.15-0044" +3,100663,"【R】東雲 つむぎ[体操着]",1015,"体操着","奏坂学園","Aqua","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.15-0022" +3,100664,"【R】柏木 美亜[体操着]",1013,"体操着","奏坂学園","Fire","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.15-0050" +3,100665,"【SR】柏木 咲姫[コスプレ喫茶へようこそ]",1009,"コスプレ喫茶へようこそ","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",110116,110117,"[O.N.G.E.K.I.]1.15-0008" +3,100666,"【SR】桜井 春菜[クレープはママの味]",1007,"クレープはママの味","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",110116,110117,"[O.N.G.E.K.I.]1.15-0004" +3,100667,"【SR】逢坂 茜[逢坂茜の挑戦状]",1011,"逢坂茜の挑戦状","奏坂学園","Fire","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",110116,110117,"[O.N.G.E.K.I.]1.15-0025" +3,100681,"【SR+】高瀬 梨緒[うきうきサイクリング+]",1003,"うきうきサイクリング+","奏坂学園","Aqua","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",115060,115061,"[O.N.G.E.K.I.]1.15-0016" +3,100682,"【SR+】井之原 小星[暑がりクールタイム+]",1010,"暑がりクールタイム+","奏坂学園","Leaf","高校1年生",12,"53,240,263,278,290,300,0,0,0,300",110066,110067,"[O.N.G.E.K.I.]1.15-0074" +3,100688,"【SR】高瀬 梨緒[うきうきサイクリング]",1003,"うきうきサイクリング","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100025,100066,"[O.N.G.E.K.I.]1.15-0017" +3,100691,"【R】九條 楓[水着]",1008,"水着","奏坂学園","Leaf","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",115138,115139,"[O.N.G.E.K.I.]1.15-0073" +3,100692,"【R】星咲 あかり[水着]",1000,"水着","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",115138,115139,"[O.N.G.E.K.I.]1.15-0056" +3,100693,"【R】藤沢 柚子[水着]",1001,"水着","奏坂学園","Leaf","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",115138,115139,"[O.N.G.E.K.I.]1.15-0058" +3,100694,"【R】三角 葵[水着]",1002,"水着","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",115138,115139,"[O.N.G.E.K.I.]1.15-0060" +3,100695,"【SSR】柏木 咲姫[絢爛たる正義の審判]",1009,"絢爛たる正義の審判","奏坂学園","Aqua","高校3年生",3,"65,262,285,300,312,322,0,0,0,327",115122,115123,"[O.N.G.E.K.I.]1.15-0027" +3,100696,"【SSR】柏木 美亜[混沌たる欲深き災厄]",1013,"混沌たる欲深き災厄","奏坂学園","Fire","中学2年生",3,"65,262,285,300,312,322,0,0,0,327",115128,115129,"[O.N.G.E.K.I.]1.15-0034" +4,100701,"【R】高瀬 梨緒[水着]",1003,"水着","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",115138,115139,"[O.N.G.E.K.I.]1.20-0001" +4,100711,"【SR】結城 莉玖[きーーーーん!]",1004,"きーーーーん!","奏坂学園","Fire","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",105044,105045,"[O.N.G.E.K.I.]1.20-0002" +4,100712,"【SR】珠洲島 有栖[小さな建築家]",1012,"小さな建築家","奏坂学園","Aqua","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",115080,115081,"[O.N.G.E.K.I.]1.20-0008" +4,100725,"【SSR】藍原 椿[宵闇シークレット]",1005,"宵闇シークレット","奏坂学園","Leaf","高校1年生",3,"65,270,291,304,314,322,0,0,0,327",115124,115125,"[O.N.G.E.K.I.]1.20-0003" +4,100726,"【SSR】桜井 春菜[ひと夏の思い出]",1007,"ひと夏の思い出","奏坂学園","Fire","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",115120,115121,"[O.N.G.E.K.I.]1.20-0006" +3,100729,"【SSR】柏木 咲姫[デイドリーム・フェアリーズ]",1009,"デイドリーム・フェアリーズ","奏坂学園","Aqua","高校3年生",3,"65,270,291,304,314,322,0,0,0,327",100040,100081,"[O.N.G.E.K.I.]1.15-0005" +3,100730,"【SSR】井之原 小星[デイドリーム・フェアリーズ]",1010,"デイドリーム・フェアリーズ","奏坂学園","Leaf","高校1年生",3,"65,270,291,304,314,322,0,0,0,327",100039,100080,"[O.N.G.E.K.I.]1.15-0009" +4,100740,"【SR】星咲 あかり[Splash Dance!!]",1000,"Splash Dance!!","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",115084,115085,"[O.N.G.E.K.I.]Special Card" +4,100741,"【SR】高瀬 梨緒[Splash Dance!!]",1003,"Splash Dance!!","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",115084,115085,"[O.N.G.E.K.I.]Special Card" +4,100742,"【SR】桜井 春菜[Splash Dance!!]",1007,"Splash Dance!!","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",115084,115085,"[O.N.G.E.K.I.]Special Card" +4,100743,"【SR】井之原 小星[Splash Dance!!]",1010,"Splash Dance!!","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",115084,115085,"[O.N.G.E.K.I.]Special Card" +4,100744,"【SR】日向 千夏[Splash Dance!!]",1014,"Splash Dance!!","奏坂学園","Leaf","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",115084,115085,"[O.N.G.E.K.I.]Special Card" +3,100745,"【SSR】日向 千夏[ONGEKI Sound Collection 03]",1014,"ONGEKI Sound Collection 03","プロモーション","Leaf","中学2年生",3,"65,262,285,300,312,322,0,0,0,327",115026,115027,"[O.N.G.E.K.I.]1.15-P-0004" +3,100746,"【SR+】藍原 椿[ONGEKI Vocal Party 01]",1005,"ONGEKI Vocal Party 01","プロモーション","Leaf","高校1年生",12,"53,240,263,278,290,300,0,0,0,300",115090,115091,"[O.N.G.E.K.I.]VP01-0001" +3,100747,"【SR+】珠洲島 有栖[ONGEKI Vocal Party 01]",1012,"ONGEKI Vocal Party 01","プロモーション","Aqua","高校1年生",12,"53,240,263,278,290,300,0,0,0,300",115092,115093,"[O.N.G.E.K.I.]VP01-0002" +3,100748,"【SR+】柏木 美亜[ONGEKI Vocal Party 01]",1013,"ONGEKI Vocal Party 01","プロモーション","Fire","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",115094,115095,"[O.N.G.E.K.I.]VP01-0003" +3,100749,"【SR+】結城 莉玖[ONGEKI Vocal Party 02]",1004,"ONGEKI Vocal Party 02","プロモーション","Fire","高校1年生",12,"53,240,263,278,290,300,0,0,0,300",115026,115027,"[O.N.G.E.K.I.]VP02-0002" +3,100750,"【SR+】柏木 咲姫[ONGEKI Vocal Party 02]",1009,"ONGEKI Vocal Party 02","プロモーション","Aqua","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",115026,115027,"[O.N.G.E.K.I.]VP02-0003" +3,100751,"【SR+】藤沢 柚子[ONGEKI Vocal Party 02]",1001,"ONGEKI Vocal Party 02","プロモーション","Leaf","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",115026,115027,"[O.N.G.E.K.I.]VP02-0001" +4,100752,"【SR+】逢坂 茜[ONGEKI Memorial Soundtrack Momiji]",1011,"ONGEKI Memorial Soundtrack Momiji","奏坂学園","Fire","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",100006,100047,"[O.N.G.E.K.I.]Special Card" +3,100754,"【R】逢坂 茜[CHUNITHM衣装]",1011,"CHUNITHM衣装","奏坂学園","Fire","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",115140,115141,"[O.N.G.E.K.I.]1.15-0032" +3,100755,"【R】柏木 咲姫[CHUNITHM衣装]",1009,"CHUNITHM衣装","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",115140,115141,"[O.N.G.E.K.I.]1.15-0028" +3,100756,"【R】柏木 美亜[CHUNITHM衣装]",1013,"CHUNITHM衣装","奏坂学園","Fire","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",115140,115141,"[O.N.G.E.K.I.]1.15-0035" +3,100757,"【SR】桜井 春菜[くるくるパニック]",1007,"くるくるパニック","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105042,105043,"[O.N.G.E.K.I.]1.15-0003" +3,100758,"【SR】柏木 咲姫[二人きり]",1009,"二人きり","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",115064,115065,"[O.N.G.E.K.I.]1.15-0007" +3,100759,"【SR】東雲 つむぎ[夜更かし羊は今日もおねむ]",1015,"夜更かし羊は今日もおねむ","奏坂学園","Aqua","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",115072,115073,"[O.N.G.E.K.I.]1.15-0045" +3,100760,"【SSR】藤沢 柚子[DREAM PARADE]",1001,"DREAM PARADE","奏坂学園","Leaf","高校2年生",3,"65,262,285,300,312,322,0,0,0,327",115116,115117,"[O.N.G.E.K.I.]1.15-0001" +3,100761,"【SSR】三角 葵[DREAM PARADE]",1002,"DREAM PARADE","奏坂学園","Aqua","高校2年生",3,"65,262,285,300,312,322,0,0,0,327",115116,115117,"[O.N.G.E.K.I.]1.15-0002" +3,100762,"【SR】九條 楓[キメてゴージャス]",1008,"キメてゴージャス","奏坂学園","Leaf","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",105044,105045,"[O.N.G.E.K.I.]1.15-0042" +3,100763,"【SR】早乙女 彩華[そっと目を閉じて]",1006,"そっと目を閉じて","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",115074,115075,"[O.N.G.E.K.I.]1.15-0048" +3,100764,"【SR】日向 千夏[はつらつブーケトス]",1014,"はつらつブーケトス","奏坂学園","Leaf","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",115076,115077,"[O.N.G.E.K.I.]1.15-0051" +3,100765,"【SR】藤沢 柚子[花びらキャッチング]",1001,"花びらキャッチング","奏坂学園","Leaf","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",115066,115067,"[O.N.G.E.K.I.]1.15-0018" +3,100766,"【SR】藍原 椿[はい、あ~ん♪]",1005,"はい、あ~ん♪","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",105042,105043,"[O.N.G.E.K.I.]1.15-0019" +3,100767,"【SSR】柏木 咲姫[祝福のマリアージュ]",1009,"祝福のマリアージュ","奏坂学園","Aqua","高校3年生",3,"65,262,285,300,312,322,0,0,0,327",115120,115121,"[O.N.G.E.K.I.]1.15-0049" +3,100768,"【SSR】井之原 小星[パンダ親分はサボりたい]",1010,"パンダ親分はサボりたい","奏坂学園","Leaf","高校1年生",3,"65,262,285,300,312,322,0,0,0,327",115120,115121,"[O.N.G.E.K.I.]1.15-0043" +3,100769,"【R】藤沢 柚子[浴衣]",1001,"浴衣","奏坂学園","Leaf","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",115142,115143,"[O.N.G.E.K.I.]1.15-0063" +3,100770,"【R】早乙女 彩華[浴衣]",1006,"浴衣","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",115142,115143,"[O.N.G.E.K.I.]1.15-0070" +4,100771,"【R】早乙女 彩華[水着]",1006,"水着","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",115138,115139,"[O.N.G.E.K.I.]1.20-0004" +3,100772,"【SR】柏木 美亜[ひよこたちのダ・カーポ]",1013,"ひよこたちのダ・カーポ","奏坂学園","Fire","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",100025,100066,"[O.N.G.E.K.I.]1.15-0038" +3,100773,"【SR+】柏木 美亜[ひよこたちのダ・カーポ+]",1013,"ひよこたちのダ・カーポ+","奏坂学園","Fire","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",115048,115049,"[O.N.G.E.K.I.]1.15-0047" +3,100774,"【R】井之原 小星[CHUNITHM衣装]",1010,"CHUNITHM衣装","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",115144,115145,"[O.N.G.E.K.I.]1.15-0030" +3,100775,"【R】星咲 あかり[CHUNITHM衣装]",1000,"CHUNITHM衣装","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",115144,115145,"[O.N.G.E.K.I.]1.15-0026" +3,100776,"【R】珠洲島 有栖[CHUNITHM衣装]",1012,"CHUNITHM衣装","奏坂学園","Aqua","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",115144,115145,"[O.N.G.E.K.I.]1.15-0033" +3,100777,"【SR】高瀬 梨緒[短冊に願いを]",1003,"短冊に願いを","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",115136,115137,"[O.N.G.E.K.I.]1.15-0061" +3,100778,"【SR】柏木 美亜[蛍火センチメンタル]",1013,"蛍火センチメンタル","奏坂学園","Fire","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",115078,115079,"[O.N.G.E.K.I.]1.15-0062" +3,100779,"【SSR】逢坂 茜[ひらり、宵桜]",1011,"ひらり、宵桜","奏坂学園","Fire","高校3年生",3,"65,262,285,300,312,322,0,0,0,327",115118,115119,"[O.N.G.E.K.I.]1.15-0020" +3,100780,"【SSR】星咲 あかり[届けて!ミルキーウェイ]",1000,"届けて!ミルキーウェイ","奏坂学園","Fire","高校2年生",3,"65,262,285,300,312,322,0,0,0,327",110062,110063,"[O.N.G.E.K.I.]1.15-0053" +3,100781,"【SSR】藤沢 柚子[オリエンタル・サマーナイト]",1001,"オリエンタル・サマーナイト","奏坂学園","Leaf","高校2年生",3,"65,262,285,300,312,322,0,0,0,327",115130,115131,"[O.N.G.E.K.I.]1.15-0065" +3,100782,"【SSR】早乙女 彩華[オリエンタル・サマーナイト]",1006,"オリエンタル・サマーナイト","奏坂学園","Aqua","高校3年生",3,"65,262,285,300,312,322,0,0,0,327",115132,115133,"[O.N.G.E.K.I.]1.15-0071" +3,100783,"【SR】逢坂 茜[魔王(仮)、爆誕!]",1011,"魔王(仮)、爆誕!","奏坂学園","Fire","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",115068,115069,"[O.N.G.E.K.I.]1.15-0031" +3,100784,"【SSR】日向 千夏[CHINATSU DiVE]",1014,"CHINATSU DiVE","奏坂学園","Leaf","中学2年生",3,"65,262,285,300,312,322,0,0,0,327",115122,115123,"[O.N.G.E.K.I.]1.15-0036" +5,100785,"【SR】三角 葵[夢見るマーメイド]",1002,"夢見るマーメイド","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",125064,125065,"[O.N.G.E.K.I.]1.25-0034" +5,100786,"【SR】星咲 あかり[るんるんスキップ]",1000,"るんるんスキップ","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",125062,125063,"[O.N.G.E.K.I.]1.25-0033" +5,100787,"【SSR】東雲 つむぎ[シンデレラ・マジック]",1015,"シンデレラ・マジック","奏坂学園","Aqua","中学2年生",3,"65,270,291,304,314,322,0,0,0,327",125044,125045,"[O.N.G.E.K.I.]1.25-0035" +3,100788,"【SSR】星咲 あかり[ずっと、夏!]",1000,"ずっと、夏!","プロモーション","Fire","高校2年生",3,"55,252,275,290,302,312,0,0,0,317",110070,110071,"[O.N.G.E.K.I.]Special Card" +3,100789,"【SSR】高瀬 梨緒[ずっと、夏!]",1003,"ずっと、夏!","プロモーション","Aqua","高校2年生",3,"55,252,275,290,302,312,0,0,0,317",110070,110071,"[O.N.G.E.K.I.]Special Card" +3,100790,"【SSR】日向 千夏[ずっと、夏!]",1014,"ずっと、夏!","プロモーション","Leaf","中学2年生",3,"55,252,275,290,302,312,0,0,0,317",110070,110071,"[O.N.G.E.K.I.]Special Card" +3,100791,"【SSR】柏木 美亜[ずっと、夏!]",1013,"ずっと、夏!","プロモーション","Fire","中学2年生",3,"55,252,275,290,302,312,0,0,0,317",110070,110071,"[O.N.G.E.K.I.]Special Card" +3,100792,"【SSR】東雲 つむぎ[ずっと、夏!]",1015,"ずっと、夏!","プロモーション","Aqua","中学2年生",3,"55,252,275,290,302,312,0,0,0,317",110070,110071,"[O.N.G.E.K.I.]Special Card" +3,100793,"【SSR】由比ヶ浜 結衣[夢と魔法の夜]",31002,"夢と魔法の夜","やはり俺の青春ラブコメはまちがっている。続","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",100037,100078,"[O.N.G.E.K.I.]1.15-E-0007" +3,100794,"【SSR】一色 いろは[夢と魔法の夜]",31003,"夢と魔法の夜","やはり俺の青春ラブコメはまちがっている。続","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]1.15-E-0011" +3,100795,"【SR】珠洲島 有栖[今日は天丼の気分]",1012,"今日は天丼の気分","奏坂学園","Aqua","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",115096,115097,"[O.N.G.E.K.I.]1.15-0023" +3,100796,"【SR】ステーキ[赤身と脂身のバランスがとれた]",11,"赤身と脂身のバランスがとれた","奏坂学園","Fire","-",2,"55,227,242,257,272,287,0,0,0,287",115098,115099,"Special Menu" +3,100797,"【SR】エビフライ[サクッと衣にプリッとジューシー]",9,"サクッと衣にプリッとジューシー","奏坂学園","Fire","-",2,"55,227,242,257,272,287,0,0,0,287",115100,115101,"Special Menu" +3,100798,"【SR】ピザ[カリカリ生地に海の幸を乗せて]",10,"カリカリ生地に海の幸を乗せて","奏坂学園","Fire","-",2,"55,227,242,257,272,287,0,0,0,287",115102,115103,"Special Menu" +3,100799,"【SR】カレー[ごろっと牛肉の赤ワイン煮込み]",11,"ごろっと牛肉の赤ワイン煮込み","奏坂学園","Aqua","-",2,"55,227,242,257,272,287,0,0,0,287",115104,115105,"Special Menu" +3,100800,"【SR】牛丼[無添加国産牛の極うま]",10,"無添加国産牛の極うま","奏坂学園","Aqua","-",2,"55,227,242,257,272,287,0,0,0,287",115106,115107,"Special Menu" +3,100801,"【SR】うな重[創業40年老舗の]",9,"創業40年老舗の","奏坂学園","Aqua","-",2,"55,227,242,257,272,287,0,0,0,287",115108,115109,"Special Menu" +3,100802,"【SR】麻婆豆腐[シビれる辛さの]",11,"シビれる辛さの","奏坂学園","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",115110,115111,"Special Menu" +3,100803,"【SR】豆腐[納豆とオクラかけてチン]",10,"納豆とオクラかけてチン","奏坂学園","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",115112,115113,"Special Menu" +3,100804,"【SR】蕎麦[石臼挽き自家製二八]",9,"石臼挽き自家製二八","奏坂学園","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",115114,115115,"Special Menu" +3,100805,"【SR】井之原 小星[リトルスターの大冒険]",1010,"リトルスターの大冒険","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",115070,115071,"[O.N.G.E.K.I.]1.15-0029" +3,100806,"【SSR】初音ミク[Project DIVA 10th Anniversary]",5000,"Project DIVA 10th Anniversary","バーチャル・シンガー","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",100033,100074,"[O.N.G.E.K.I.]1.15-E-0109" +3,100807,"【R】雪ノ下 雪乃[総武高校奉仕部]",31001,"総武高校奉仕部","やはり俺の青春ラブコメはまちがっている。続","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0001" +3,100808,"【R】由比ヶ浜 結衣[総武高校奉仕部]",31002,"総武高校奉仕部","やはり俺の青春ラブコメはまちがっている。続","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0004" +3,100809,"【R】一色 いろは[総武高校生徒会長]",31003,"総武高校生徒会長","やはり俺の青春ラブコメはまちがっている。続","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.15-E-0008" +3,100810,"【SR】雪ノ下 雪乃[貴方もいかが]",31001,"貴方もいかが","やはり俺の青春ラブコメはまちがっている。続","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0002" +3,100811,"【SR】由比ヶ浜 結衣[食べる?]",31002,"食べる?","やはり俺の青春ラブコメはまちがっている。続","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",115024,115025,"[O.N.G.E.K.I.]1.15-E-0005" +3,100812,"【SR】一色 いろは[口説いてるんですか?]",31003,"口説いてるんですか?","やはり俺の青春ラブコメはまちがっている。続","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100016,100057,"[O.N.G.E.K.I.]1.15-E-0009" +3,100813,"【SR】雪ノ下 雪乃[はんなり花景色]",31001,"はんなり花景色","やはり俺の青春ラブコメはまちがっている。続","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110082,110083,"[O.N.G.E.K.I.]1.15-E-0003" +3,100814,"【SR】由比ヶ浜 結衣[ゆきのんと一緒]",31002,"ゆきのんと一緒","やはり俺の青春ラブコメはまちがっている。続","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.15-E-0006" +3,100815,"【SR】一色 いろは[せーんぱい!]",31003,"せーんぱい!","やはり俺の青春ラブコメはまちがっている。続","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",115000,115001,"[O.N.G.E.K.I.]1.15-E-0010" +3,100816,"【R】ロザリー[ゴシックドレス]",34001,"ゴシックドレス","ゴシックは魔法乙女","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0012" +3,100817,"【R】プルメリア[ゴシックドレス]",34001,"ゴシックドレス","ゴシックは魔法乙女","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.15-E-0015" +3,100818,"【R】ラナン[ゴシックドレス]",34001,"ゴシックドレス","ゴシックは魔法乙女","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100001,100042,"[O.N.G.E.K.I.]1.15-E-0017" +3,100819,"【R】カトレア[ゴシックドレス]",34001,"ゴシックドレス","ゴシックは魔法乙女","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0019" +3,100820,"【R】スフレ[ゴシックドレス]",34001,"ゴシックドレス","ゴシックは魔法乙女","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0021" +3,100821,"【R】ジギタリス[ゴシックパンクス]",34001,"ゴシックパンクス","ゴシックは魔法乙女","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0023" +3,100822,"【R】リリー[ゴシックパンクス]",34001,"ゴシックパンクス","ゴシックは魔法乙女","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.15-E-0025" +3,100823,"【R】ルチカ[ゴシックパンクス]",34001,"ゴシックパンクス","ゴシックは魔法乙女","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0027" +3,100824,"【R】カルミア[ゴシックパンクス]",34001,"ゴシックパンクス","ゴシックは魔法乙女","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",105004,105005,"[O.N.G.E.K.I.]1.15-E-0029" +3,100825,"【R】ダチュラ[ゴシックパンクス]",34001,"ゴシックパンクス","ゴシックは魔法乙女","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0031" +3,100826,"【SR】ロザリー[闇星夜]",34001,"闇星夜","ゴシックは魔法乙女","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0013" +3,100827,"【SR】プルメリア[光星夜]",34001,"光星夜","ゴシックは魔法乙女","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110138,110139,"[O.N.G.E.K.I.]1.15-E-0016" +3,100828,"【SR】ラナン[炎星夜]",34001,"炎星夜","ゴシックは魔法乙女","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100016,100057,"[O.N.G.E.K.I.]1.15-E-0018" +3,100829,"【SR】カトレア[水星夜]",34001,"水星夜","ゴシックは魔法乙女","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.15-E-0020" +3,100830,"【SR】スフレ[冬遊]",34001,"冬遊","ゴシックは魔法乙女","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0022" +3,100831,"【SR】ジギタリス[頬張]",34001,"頬張","ゴシックは魔法乙女","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.15-E-0024" +3,100832,"【SR】リリー[夢夜]",34001,"夢夜","ゴシックは魔法乙女","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100018,100059,"[O.N.G.E.K.I.]1.15-E-0026" +3,100833,"【SR】ルチカ[空星夜]",34001,"空星夜","ゴシックは魔法乙女","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.15-E-0028" +3,100834,"【SR】カルミア[輝瞳]",34001,"輝瞳","ゴシックは魔法乙女","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100017,100058,"[O.N.G.E.K.I.]1.15-E-0030" +3,100835,"【SR】ダチュラ[妖星夜]",34001,"妖星夜","ゴシックは魔法乙女","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0032" +3,100836,"【SR+】ロザリー[真夏のLOVEMAX!]",34001,"真夏のLOVEMAX!","ゴシックは魔法乙女","Aqua","-",12,"53,240,263,278,290,300,0,0,0,300",115004,115005,"[O.N.G.E.K.I.]1.15-E-0014" +3,100837,"【SR】サクラ[結実した努力]",27001,"結実した努力","プロジェクト東京ドールズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.15-E-0033" +3,100838,"【SR】ミサキ[力強い眼差し]",27001,"力強い眼差し","プロジェクト東京ドールズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",115006,115007,"[O.N.G.E.K.I.]1.15-E-0034" +3,100839,"【SR】シオリ[強くて優しいお姉さん]",27001,"強くて優しいお姉さん","プロジェクト東京ドールズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",115008,115009,"[O.N.G.E.K.I.]1.15-E-0035" +3,100840,"【SR】レイナ[ミス・ビューティー]",27001,"ミス・ビューティー","プロジェクト東京ドールズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",115010,115011,"[O.N.G.E.K.I.]1.15-E-0036" +3,100841,"【SR】ヒヨ[底抜けハイテンション娘]",27001,"底抜けハイテンション娘","プロジェクト東京ドールズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",115006,115007,"[O.N.G.E.K.I.]1.15-E-0037" +3,100842,"【SR】ナナミ[素直になれないお年頃]",27001,"素直になれないお年頃","プロジェクト東京ドールズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.15-E-0038" +3,100843,"【SR】アヤ[私についてきて!]",27001,"私についてきて!","プロジェクト東京ドールズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115012,115013,"[O.N.G.E.K.I.]1.15-E-0039" +3,100844,"【SR】ユキ[不思議系居眠り姫]",27001,"不思議系居眠り姫","プロジェクト東京ドールズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.15-E-0040" +3,100845,"【SR】ヤマダ[まあ悪くはないっす…]",27001,"まあ悪くはないっす…","プロジェクト東京ドールズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115006,115007,"[O.N.G.E.K.I.]1.15-E-0041" +3,100846,"【SR+】ユキ[あなたの笑顔こそ未来]",27001,"あなたの笑顔こそ未来","プロジェクト東京ドールズ","Leaf","-",12,"53,240,263,278,290,300,0,0,0,300",110072,110073,"[O.N.G.E.K.I.]1.15-E-0042" +3,100847,"【SSR】高瀬 梨緒[プロジェクト東京ドールズ]",1003,"プロジェクト東京ドールズ","奏坂学園","Aqua","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",115126,115127,"[O.N.G.E.K.I.]1.15-0014" +3,100848,"【R】星月 みき[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0043" +3,100849,"【R】若葉 昴[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0046" +3,100850,"【R】成海 遥香[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",105004,105005,"[O.N.G.E.K.I.]1.15-E-0048" +3,100851,"【R】天野 望[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0050" +3,100852,"【R】火向井 ゆり[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0052" +3,100853,"【R】常磐 くるみ[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",105004,105005,"[O.N.G.E.K.I.]1.15-E-0054" +3,100854,"【R】粒咲 あんこ[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.15-E-0056" +3,100855,"【R】芹沢 蓮華[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0058" +3,100856,"【R】楠 明日葉[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0060" +3,100857,"【SR】星月 みき[バースデー'19]",33001,"バースデー'19","バトルガール ハイスクール","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0044" +3,100858,"【SR】若葉 昴[バースデー'18]",33001,"バースデー'18","バトルガール ハイスクール","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0047" +3,100859,"【SR】成海 遥香[バースデー'18]",33001,"バースデー'18","バトルガール ハイスクール","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110136,110137,"[O.N.G.E.K.I.]1.15-E-0049" +3,100860,"【SR】天野 望[バースデー'18]",33001,"バースデー'18","バトルガール ハイスクール","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0051" +3,100861,"【SR】火向井 ゆり[バースデー'18]",33001,"バースデー'18","バトルガール ハイスクール","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0053" +3,100862,"【SR】常磐 くるみ[バースデー'18]",33001,"バースデー'18","バトルガール ハイスクール","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110140,110141,"[O.N.G.E.K.I.]1.15-E-0055" +3,100863,"【SR】粒咲 あんこ[バースデー'18]",33001,"バースデー'18","バトルガール ハイスクール","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110086,110087,"[O.N.G.E.K.I.]1.15-E-0057" +3,100864,"【SR】芹沢 蓮華[バースデー'18]",33001,"バースデー'18","バトルガール ハイスクール","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.15-E-0059" +3,100865,"【SR】楠 明日葉[バースデー'19]",33001,"バースデー'19","バトルガール ハイスクール","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.15-E-0061" +3,100866,"【R】藤宮 桜[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0062" +3,100867,"【R】南 ひなた[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0064" +3,100868,"【R】千導院 楓[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",105004,105005,"[O.N.G.E.K.I.]1.15-E-0066" +3,100869,"【R】綿木 ミシェル[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.15-E-0068" +3,100870,"【R】朝比奈 心美[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0070" +3,100871,"【R】蓮見 うらら[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0072" +3,100872,"【R】サドネ[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.15-E-0074" +3,100873,"【R】煌上 花音[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0076" +3,100874,"【R】国枝 詩穂[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0079" +3,100875,"【R】ミサキ[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.15-E-0081" +3,100876,"【SR】藤宮 桜[バースデー'19]",33001,"バースデー'19","バトルガール ハイスクール","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0063" +3,100877,"【SR】南 ひなた[バースデー'19]",33001,"バースデー'19","バトルガール ハイスクール","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0065" +3,100878,"【SR】千導院 楓[バースデー'18]",33001,"バースデー'18","バトルガール ハイスクール","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110138,110139,"[O.N.G.E.K.I.]1.15-E-0067" +3,100879,"【SR】綿木 ミシェル[バースデー'18]",33001,"バースデー'18","バトルガール ハイスクール","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110088,110089,"[O.N.G.E.K.I.]1.15-E-0069" +3,100880,"【SR】朝比奈 心美[バースデー'18]",33001,"バースデー'18","バトルガール ハイスクール","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.15-E-0071" +3,100881,"【SR】蓮見 うらら[バースデー'19]",33001,"バースデー'19","バトルガール ハイスクール","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.15-E-0073" +3,100882,"【SR】サドネ[バースデー'18]",33001,"バースデー'18","バトルガール ハイスクール","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110082,110083,"[O.N.G.E.K.I.]1.15-E-0075" +3,100883,"【SR】煌上 花音[バースデー'18]",33001,"バースデー'18","バトルガール ハイスクール","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.15-E-0077" +3,100884,"【SR】国枝 詩穂[バースデー'18]",33001,"バースデー'18","バトルガール ハイスクール","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.15-E-0080" +3,100885,"【SR】ミサキ[バースデー'18]",33001,"バースデー'18","バトルガール ハイスクール","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110082,110083,"[O.N.G.E.K.I.]1.15-E-0082" +3,100886,"【SR+】煌上 花音[復活ライブ]",33001,"復活ライブ","バトルガール ハイスクール","Leaf","-",12,"53,240,263,278,290,300,0,0,0,300",115028,115029,"[O.N.G.E.K.I.]1.15-E-0078" +3,100887,"【SSR】星月 みき[5th Anniversary]",33001,"5th Anniversary","バトルガール ハイスクール","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.15-E-0045" +4,100897,"【SR】藤原 妹紅[full bloom]",2017,"full bloom","東方Project","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",105044,105045,"[O.N.G.E.K.I.]1.20-E-0188" +4,100898,"【SR】鈴仙・優曇華院・イナバ[full bloom]",2014,"full bloom","東方Project","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.20-E-0187" +4,100899,"【SR】古明地 さとり[full bloom]",2020,"full bloom","東方Project","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100016,100057,"[O.N.G.E.K.I.]1.20-E-0189" +4,100900,"【SR】古明地 こいし[full bloom]",2021,"full bloom","東方Project","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",120074,120075,"[O.N.G.E.K.I.]1.20-E-0190" +4,100901,"【SR】博麗 霊夢[full bloom]",2000,"full bloom","東方Project","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0184" +4,100902,"【SR】霧雨 魔理沙[full bloom]",2001,"full bloom","東方Project","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",120074,120075,"[O.N.G.E.K.I.]1.20-E-0185" +4,100903,"【SR+】霧雨 魔理沙[幻想郷の花吹雪]",2001,"幻想郷の花吹雪","東方Project","Leaf","-",12,"53,240,263,278,290,300,0,0,0,300",105010,105011,"[O.N.G.E.K.I.]1.20-E-0186" +3,100904,"【R】リムル=テンペスト[人型][転生者]",29001,"転生者","転生したらスライムだった件","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0092" +3,100905,"【R】ミリム・ナーヴァ[竜魔人(ドラゴノイド)]",29003,"竜魔人(ドラゴノイド)","転生したらスライムだった件","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0100" +3,100906,"【R】リムル=テンペスト[スライム][捕食者]",29002,"捕食者","転生したらスライムだった件","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100003,100044,"[O.N.G.E.K.I.]1.15-E-0096" +3,100907,"【SR】リムル=テンペスト[人型][暴風の紋章]",29001,"暴風の紋章","転生したらスライムだった件","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110086,110087,"[O.N.G.E.K.I.]1.15-E-0093" +3,100908,"【SR】ミリム・ナーヴァ[破壊の暴君(デストロイ)]",29003,"破壊の暴君(デストロイ)","転生したらスライムだった件","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0101" +3,100909,"【SR】リムル=テンペスト[人型][暴食者(グラトニー)]",29001,"暴食者(グラトニー)","転生したらスライムだった件","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110082,110083,"[O.N.G.E.K.I.]1.15-E-0094" +3,100910,"【SR】ミリム・ナーヴァ[竜星拡散爆(ドラゴ・バスター)]",29003,"竜星拡散爆(ドラゴ・バスター)","転生したらスライムだった件","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",115016,115017,"[O.N.G.E.K.I.]1.15-E-0102" +3,100911,"【SR】シズエ・イザワ[迸る炎]",29002,"迸る炎","転生したらスライムだった件","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0097" +3,100912,"【SR】大賢者[ユニークスキル]",29002,"ユニークスキル","転生したらスライムだった件","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",115018,115019,"[O.N.G.E.K.I.]1.15-E-0098" +3,100913,"【SR】ヴェルドラ=テンペスト[暴風竜]",29002,"暴風竜","転生したらスライムだった件","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115020,115021,"[O.N.G.E.K.I.]1.15-E-0099" +3,100914,"【SR+】リムル=テンペスト[人型][魔物を統べる者]",29001,"魔物を統べる者","転生したらスライムだった件","Aqua","-",12,"53,240,263,278,290,300,0,0,0,300",105144,105145,"[O.N.G.E.K.I.]1.15-E-0095" +3,100915,"【R】式宮 舞菜[KiRaRe]",32001,"KiRaRe","Re:ステージ!プリズムステップ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0110" +3,100916,"【R】月坂 紗由[KiRaRe]",32001,"KiRaRe","Re:ステージ!プリズムステップ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0113" +3,100917,"【R】市杵島 瑞葉[KiRaRe]",32001,"KiRaRe","Re:ステージ!プリズムステップ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.15-E-0115" +3,100918,"【R】柊 かえ[KiRaRe]",32001,"KiRaRe","Re:ステージ!プリズムステップ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100002,100043,"[O.N.G.E.K.I.]1.15-E-0117" +3,100919,"【R】本城 香澄[KiRaRe]",32001,"KiRaRe","Re:ステージ!プリズムステップ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0119" +3,100920,"【R】長谷川 みい[KiRaRe]",32001,"KiRaRe","Re:ステージ!プリズムステップ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0121" +3,100921,"【R】伊津村 紫[オルタンシア]",32001,"オルタンシア","Re:ステージ!プリズムステップ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0123" +3,100922,"【R】伊津村 陽花[オルタンシア]",32001,"オルタンシア","Re:ステージ!プリズムステップ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100003,100044,"[O.N.G.E.K.I.]1.15-E-0125" +3,100923,"【R】式宮 碧音[ステラマリス]",32001,"ステラマリス","Re:ステージ!プリズムステップ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0127" +3,100924,"【R】一条 瑠夏[ステラマリス]",32001,"ステラマリス","Re:ステージ!プリズムステップ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0129" +3,100925,"【R】岬 珊瑚[ステラマリス]",32001,"ステラマリス","Re:ステージ!プリズムステップ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.15-E-0131" +3,100926,"【SR】式宮 舞菜[ボクら×夢の数]",32001,"ボクら×夢の数","Re:ステージ!プリズムステップ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0111" +3,100927,"【SR】月坂 紗由[ボクら×夢の数]",32001,"ボクら×夢の数","Re:ステージ!プリズムステップ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0114" +3,100928,"【SR】市杵島 瑞葉[ボクら×夢の数]",32001,"ボクら×夢の数","Re:ステージ!プリズムステップ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110114,110115,"[O.N.G.E.K.I.]1.15-E-0116" +3,100929,"【SR】柊 かえ[ボクら×夢の数]",32001,"ボクら×夢の数","Re:ステージ!プリズムステップ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115024,115025,"[O.N.G.E.K.I.]1.15-E-0118" +3,100930,"【SR】本城 香澄[ボクら×夢の数]",32001,"ボクら×夢の数","Re:ステージ!プリズムステップ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.15-E-0120" +3,100931,"【SR】長谷川 みい[ボクら×夢の数]",32001,"ボクら×夢の数","Re:ステージ!プリズムステップ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.15-E-0122" +3,100932,"【SR】伊津村 紫[出会えたキセキ]",32001,"出会えたキセキ","Re:ステージ!プリズムステップ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0124" +3,100933,"【SR】伊津村 陽花[出会えたキセキ]",32001,"出会えたキセキ","Re:ステージ!プリズムステップ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0126" +3,100934,"【SR】式宮 碧音[Next:STAGE!!]",32001,"Next:STAGE!!","Re:ステージ!プリズムステップ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",105042,105043,"[O.N.G.E.K.I.]1.15-E-0128" +3,100935,"【SR】一条 瑠夏[Next:STAGE!!]",32001,"Next:STAGE!!","Re:ステージ!プリズムステップ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",105042,105043,"[O.N.G.E.K.I.]1.15-E-0130" +3,100936,"【SR】岬 珊瑚[Next:STAGE!!]",32001,"Next:STAGE!!","Re:ステージ!プリズムステップ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110088,110089,"[O.N.G.E.K.I.]1.15-E-0132" +3,100937,"【SR+】式宮 舞菜[KiRaRe!輝け、私たちの夢!!]",32001,"KiRaRe!輝け、私たちの夢!!","Re:ステージ!プリズムステップ","Leaf","-",12,"53,240,263,278,290,300,0,0,0,300",100031,100072,"[O.N.G.E.K.I.]1.15-E-0112" +3,100938,"【R】赤座 あかり[七森中 ごらく部]",30003,"七森中 ごらく部","ゆるゆり♪♪","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100007,100048,"[O.N.G.E.K.I.]1.15-E-0133" +3,100939,"【R】歳納 京子[七森中 ごらく部]",30001,"七森中 ごらく部","ゆるゆり♪♪","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0136" +3,100940,"【R】船見 結衣[七森中 ごらく部]",30001,"七森中 ごらく部","ゆるゆり♪♪","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0138" +3,100941,"【R】吉川 ちなつ[七森中 ごらく部]",30003,"七森中 ごらく部","ゆるゆり♪♪","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.15-E-0140" +3,100942,"【R】杉浦 綾乃[七森中 生徒会]",30002,"七森中 生徒会","ゆるゆり♪♪","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0142" +3,100943,"【R】池田 千歳[七森中 生徒会]",30002,"七森中 生徒会","ゆるゆり♪♪","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110084,110085,"[O.N.G.E.K.I.]1.15-E-0145" +3,100944,"【R】大室 櫻子[七森中 生徒会]",30002,"七森中 生徒会","ゆるゆり♪♪","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0147" +3,100945,"【R】古谷 向日葵[七森中 生徒会]",30002,"七森中 生徒会","ゆるゆり♪♪","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0149" +3,100946,"【SR】赤座 あかり[\アッカリ~ン/]",30003,"\アッカリ~ン/","ゆるゆり♪♪","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110114,110115,"[O.N.G.E.K.I.]1.15-E-0134" +3,100947,"【SR】歳納 京子[真の主人公]",30001,"真の主人公","ゆるゆり♪♪","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115026,115027,"[O.N.G.E.K.I.]1.15-E-0137" +3,100948,"【SR】船見 結衣[影の主人公]",30001,"影の主人公","ゆるゆり♪♪","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0139" +3,100949,"【SR】吉川 ちなつ[遅れてきた主人公]",30003,"遅れてきた主人公","ゆるゆり♪♪","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110138,110139,"[O.N.G.E.K.I.]1.15-E-0141" +3,100950,"【SR】杉浦 綾乃[罰金バッキンガムよ!]",30002,"罰金バッキンガムよ!","ゆるゆり♪♪","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115022,115023,"[O.N.G.E.K.I.]1.15-E-0143" +3,100951,"【SR】池田 千歳[今日も豊作やわ~♪♪]",30002,"今日も豊作やわ~♪♪","ゆるゆり♪♪","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100017,100058,"[O.N.G.E.K.I.]1.15-E-0146" +3,100952,"【SR】大室 櫻子[さみしくないモン!!]",30002,"さみしくないモン!!","ゆるゆり♪♪","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0148" +3,100953,"【SR】古谷 向日葵[嫌な予感しかしませんわ]",30002,"嫌な予感しかしませんわ","ゆるゆり♪♪","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",105008,105009,"[O.N.G.E.K.I.]1.15-E-0150" +3,100954,"【SSR】赤座 あかり[笑われればいいと思うよ。]",30003,"笑われればいいと思うよ。","ゆるゆり♪♪","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",115030,115031,"[O.N.G.E.K.I.]1.15-E-0135" +3,100955,"【SSR】杉浦 綾乃[Miracle Duet]",30002,"Miracle Duet","ゆるゆり♪♪","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]1.15-E-0144" +3,100956,"【R】倉科 明日香[オールラウンダー]",35001,"オールラウンダー","蒼の彼方のフォーリズム","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0083" +3,100957,"【R】鳶沢 みさき[ファイター]",35001,"ファイター","蒼の彼方のフォーリズム","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0085" +3,100958,"【R】有坂 真白[スピーダー]",35001,"スピーダー","蒼の彼方のフォーリズム","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0088" +3,100959,"【R】市ノ瀬 莉佳[スピーダー]",35001,"スピーダー","蒼の彼方のフォーリズム","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.15-E-0090" +3,100960,"【SR】倉科 明日香[エアキックターン]",35001,"エアキックターン","蒼の彼方のフォーリズム","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100018,100059,"[O.N.G.E.K.I.]1.15-E-0084" +3,100961,"【SR】鳶沢 みさき[ドッグファイト]",35001,"ドッグファイト","蒼の彼方のフォーリズム","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",115036,115037,"[O.N.G.E.K.I.]1.15-E-0086" +3,100962,"【SR】有坂 真白[ましろうどんの看板娘]",35001,"ましろうどんの看板娘","蒼の彼方のフォーリズム","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.15-E-0089" +3,100963,"【SR】市ノ瀬 莉佳[一年生同盟]",35001,"一年生同盟","蒼の彼方のフォーリズム","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",115038,115039,"[O.N.G.E.K.I.]1.15-E-0091" +3,100964,"【SR+】鳶沢 みさき[負けてからが、本当の勝負]",35001,"負けてからが、本当の勝負","蒼の彼方のフォーリズム","Aqua","-",12,"53,240,263,278,290,300,0,0,0,300",100031,100072,"[O.N.G.E.K.I.]1.15-E-0087" +5,100965,"【SSR】倉科 明日香[Beyond the sky]",35001,"Beyond the sky","蒼の彼方のフォーリズム","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",110070,110071,"[O.N.G.E.K.I.]1.25-E-0129" +5,100966,"【SSR】鳶沢 みさき[into the firmament]",35001,"into the firmament","蒼の彼方のフォーリズム","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",115040,115041,"[O.N.G.E.K.I.]1.25-E-0128" +3,100967,"【SR】春日部 ハル[777☆夏合宿]",24001,"777☆夏合宿","Tokyo 7th シスターズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110082,110083,"[O.N.G.E.K.I.]1.15-E-0158" +3,100968,"【SR】天堂寺 ムスビ[777☆夏合宿]",24001,"777☆夏合宿","Tokyo 7th シスターズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",115032,115033,"[O.N.G.E.K.I.]1.15-E-0160" +3,100969,"【SR】角森 ロナ[777☆夏合宿]",24001,"777☆夏合宿","Tokyo 7th シスターズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110082,110083,"[O.N.G.E.K.I.]1.15-E-0161" +3,100970,"【SR】野ノ原 ヒメ [777☆夏合宿]",24001,"777☆夏合宿","Tokyo 7th シスターズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110000,110001,"[O.N.G.E.K.I.]1.15-E-0162" +3,100971,"【SR】芹沢 モモカ[777☆夏合宿]",24001,"777☆夏合宿","Tokyo 7th シスターズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",115034,115035,"[O.N.G.E.K.I.]1.15-E-0163" +3,100972,"【SR】臼田 スミレ[777☆夏合宿]",24001,"777☆夏合宿","Tokyo 7th シスターズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100018,100059,"[O.N.G.E.K.I.]1.15-E-0164" +3,100973,"【SR】神城 スイ[777☆夏合宿]",24001,"777☆夏合宿","Tokyo 7th シスターズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0165" +3,100974,"【SR】久遠寺 シズカ [777☆夏合宿]",24001,"777☆夏合宿","Tokyo 7th シスターズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0166" +3,100975,"【SR】アレサンドラ・スース[777☆夏合宿]",24001,"777☆夏合宿","Tokyo 7th シスターズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",115032,115033,"[O.N.G.E.K.I.]1.15-E-0167" +3,100976,"【SR】晴海 サワラ[777☆夏合宿]",24001,"777☆夏合宿","Tokyo 7th シスターズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110094,110095,"[O.N.G.E.K.I.]1.15-E-0168" +3,100977,"【SR】晴海 カジカ[777☆夏合宿]",24001,"777☆夏合宿","Tokyo 7th シスターズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.15-E-0169" +3,100978,"【SR】晴海 シンジュ [777☆夏合宿]",24001,"777☆夏合宿","Tokyo 7th シスターズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100017,100058,"[O.N.G.E.K.I.]1.15-E-0170" +3,100979,"【SR+】春日部 ハル[MELODY IN THE POCKET]",24001,"MELODY IN THE POCKET","Tokyo 7th シスターズ","Fire","-",12,"53,240,263,278,290,300,0,0,0,300",105010,105011,"[O.N.G.E.K.I.]1.15-E-0159" +3,100980,"【SSR】日向 千夏[2nd Anniversary]",1014,"2nd Anniversary","プロモーション","Leaf","中学2年生",3,"52,237,252,267,282,297,0,0,0,302",100033,100033,"[O.N.G.E.K.I.]Special Card" +4,100982,"【SSR】東雲 つむぎ[Summer☆Splash]",1015,"Summer☆Splash","奏坂学園","Aqua","中学2年生",3,"65,270,291,304,314,322,0,0,0,327",120000,120001,"[O.N.G.E.K.I.]1.20-0045" +4,100983,"【SSR】柏木 美亜[Summer☆Splash]",1013,"Summer☆Splash","奏坂学園","Fire","中学2年生",3,"65,270,291,304,314,322,0,0,0,327",120000,120001,"[O.N.G.E.K.I.]1.20-0044" +4,100984,"【SSR】結城 莉玖[目指せ!得点王]",1004,"目指せ!得点王","奏坂学園","Fire","高校1年生",3,"65,270,291,304,314,322,0,0,0,327",120078,120079,"[O.N.G.E.K.I.]1.20-0015" +4,100985,"【SSR】九條 楓[月夜のパンプキンマジック]",1008,"月夜のパンプキンマジック","奏坂学園","Leaf","高校3年生",3,"65,270,291,304,314,322,0,0,0,327",115120,115121,"[O.N.G.E.K.I.]1.20-0022" +4,100986,"【SSR】珠洲島 有栖[ぱくぱく宇宙飛行]",1012,"ぱくぱく宇宙飛行","奏坂学園","Aqua","高校1年生",3,"65,270,291,304,314,322,0,0,0,327",120006,120007,"[O.N.G.E.K.I.]1.20-0035" +4,100987,"【SSR】三角 葵[ビタースイート☆クリスマス]",1002,"ビタースイート☆クリスマス","奏坂学園","Aqua","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",120008,120009,"[O.N.G.E.K.I.]1.20-0027" +4,100988,"【SSR】井之原 小星[ビタースイート☆クリスマス]",1010,"ビタースイート☆クリスマス","奏坂学園","Leaf","高校1年生",3,"65,270,291,304,314,322,0,0,0,327",120010,120011,"[O.N.G.E.K.I.]1.20-0029" +4,100989,"【SSR】高瀬 梨緒[六花の妖精]",1003,"六花の妖精","奏坂学園","Aqua","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",120034,120035,"[O.N.G.E.K.I.]1.20-0040" +4,100990,"【SSR】星咲 あかり[無敵のツーマンセル]",1000,"無敵のツーマンセル","奏坂学園","Fire","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",120080,120081,"[O.N.G.E.K.I.]1.20-0011" +4,100991,"【SSR】高瀬 梨緒[無敵のツーマンセル]",1003,"無敵のツーマンセル","奏坂学園","Aqua","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",120082,120083,"[O.N.G.E.K.I.]1.20-0014" +4,100992,"【SSR】東雲 つむぎ[謎解きはおやつの前に]",1015,"謎解きはおやつの前に","奏坂学園","Aqua","中学2年生",3,"65,270,291,304,314,322,0,0,0,327",120006,120007,"[O.N.G.E.K.I.]1.20-0025" +4,100993,"【SSR】結城 莉玖[謎解きはおやつの前に]",1004,"謎解きはおやつの前に","奏坂学園","Fire","高校1年生",3,"65,270,291,304,314,322,0,0,0,327",120072,120073,"[O.N.G.E.K.I.]1.20-0020" +4,100994,"【SSR】桜井 春菜[おこたで鍋パ!]",1007,"おこたで鍋パ!","奏坂学園","Fire","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",120084,120085,"[O.N.G.E.K.I.]1.20-0033" +4,100995,"【SSR】珠洲島 有栖[おこたで鍋パ!]",1012,"おこたで鍋パ!","奏坂学園","Aqua","高校1年生",3,"65,270,291,304,314,322,0,0,0,327",120086,120087,"[O.N.G.E.K.I.]1.20-0036" +4,100996,"【SR】九條 楓[魅惑のプールサイド]",1008,"魅惑のプールサイド","奏坂学園","Leaf","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",120012,120013,"[O.N.G.E.K.I.]1.20-0043" +4,100997,"【SR】三角 葵[絡まりハプニング]",1002,"絡まりハプニング","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",120088,120089,"[O.N.G.E.K.I.]1.20-0012" +4,100998,"【SR】逢坂 茜[宣言通りの一等賞]",1011,"宣言通りの一等賞","奏坂学園","Fire","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",120090,120091,"[O.N.G.E.K.I.]1.20-0017" +4,100999,"【SR】日向 千夏[とりっく・おあ・ぎゅー!]",1014,"とりっく・おあ・ぎゅー!","奏坂学園","Leaf","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",100031,100072,"[O.N.G.E.K.I.]1.20-0024" +4,101000,"【SR】柏木 咲姫[なりきりヴァンパイア]",1009,"なりきりヴァンパイア","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",120018,120019,"[O.N.G.E.K.I.]1.20-0023" +4,101001,"【SR】桜井 春菜[検温のお時間です♪]",1007,"検温のお時間です♪","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",110066,110067,"[O.N.G.E.K.I.]1.20-0032" +4,101002,"【SR】藍原 椿[教えて!椿先生]",1005,"教えて!椿先生","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",115000,115001,"[O.N.G.E.K.I.]1.20-0031" +4,101003,"【SR】早乙女 彩華[サンタのお仕事]",1006,"サンタのお仕事","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",120020,120021,"[O.N.G.E.K.I.]1.20-0028" +4,101004,"【SR】柏木 美亜[雪と子猫とコーヒーと]",1013,"雪と子猫とコーヒーと","奏坂学園","Fire","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",120092,120093,"[O.N.G.E.K.I.]1.20-0041" +4,101005,"【SR】藤沢 柚子[一緒にポーズ!]",1001,"一緒にポーズ!","奏坂学園","Leaf","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",120092,120093,"[O.N.G.E.K.I.]1.20-0039" +4,101006,"【SR+】結城 莉玖[夢に向かって]",1004,"夢に向かって","奏坂学園","Fire","高校1年生",12,"53,240,263,278,290,300,0,0,0,300",120036,120037,"[O.N.G.E.K.I.]1.20-0018" +4,101007,"【SR+】三角 葵[穏やかな時間]",1002,"穏やかな時間","奏坂学園","Aqua","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",120038,120039,"[O.N.G.E.K.I.]1.20-0046" +5,101008,"【SR+】藤沢 柚子[お悩みシンキング]",1001,"お悩みシンキング","奏坂学園","Leaf","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",120040,120041,"" +4,101009,"【SR+】桜井 春菜[元気にしてた?]",1007,"元気にしてた?","奏坂学園","Fire","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",120042,120043,"[O.N.G.E.K.I.]1.20-0042" +4,101010,"【SR+】柏木 咲姫[お出かけホリデー]",1009,"お出かけホリデー","奏坂学園","Aqua","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",120044,120045,"[O.N.G.E.K.I.]1.20-0034" +4,101011,"【SR+】逢坂 茜[世界征服☆大作戦]",1011,"世界征服☆大作戦","奏坂学園","Fire","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",110066,110067,"[O.N.G.E.K.I.]1.20-0009" +4,101012,"【SR+】珠洲島 有栖[もふもふシエスタ]",1012,"もふもふシエスタ","奏坂学園","Aqua","高校1年生",12,"53,240,263,278,290,300,0,0,0,300",110066,110067,"[O.N.G.E.K.I.]1.20-0026" +5,101013,"【SR+】九條 楓[ぽんぽんクリーニング]",1008,"ぽんぽんクリーニング","奏坂学園","Leaf","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",110066,110067,"[O.N.G.E.K.I.]1.25-0036" +4,101020,"【R】星咲 あかり[体操着2]",1000,"体操着2","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",120094,120095,"[O.N.G.E.K.I.]1.20-0010" +4,101021,"【R】高瀬 梨緒[体操着2]",1003,"体操着2","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",120094,120095,"[O.N.G.E.K.I.]1.20-0013" +4,101022,"【R】逢坂 茜[サバゲー]",1011,"サバゲー","奏坂学園","Fire","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",120094,120095,"[O.N.G.E.K.I.]1.20-0016" +4,101023,"【R】藍原 椿[体操着2]",1005,"体操着2","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",120048,120049,"[O.N.G.E.K.I.]1.20-0021" +4,101024,"【R】結城 莉玖[体操着2]",1004,"体操着2","奏坂学園","Fire","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",120050,120051,"[O.N.G.E.K.I.]1.20-0019" +4,101025,"【R】三角 葵[体操着2]",1002,"体操着2","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",120052,120053,"[O.N.G.E.K.I.]1.20-0030" +4,101026,"【R】柏木 美亜[体操着2]",1013,"体操着2","奏坂学園","Fire","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",120054,120055,"[O.N.G.E.K.I.]1.20-0037" +4,101027,"【R】日向 千夏[体操着2]",1014,"体操着2","奏坂学園","Leaf","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",120056,120057,"[O.N.G.E.K.I.]1.20-0038" +5,101038,"【R】洩矢 諏訪子[東方Project]",2023,"東方Project","東方Project 早苗&諏訪子編","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0201" +5,101039,"【R】東風谷 早苗[東方Project]",2022,"東方Project","東方Project 早苗&諏訪子編","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100002,100043,"[O.N.G.E.K.I.]1.25-E-0198" +5,101041,"【SR】洩矢 諏訪子[土着神の頂点]",2023,"土着神の頂点","東方Project 早苗&諏訪子編","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0200" +5,101042,"【SR】東風谷 早苗[祀られる風の人間]",2022,"祀られる風の人間","東方Project 早苗&諏訪子編","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110114,110115,"[O.N.G.E.K.I.]1.25-E-0197" +5,101044,"【SSR】洩矢 諏訪子[神具「洩矢の鉄の輪」]",2023,"神具「洩矢の鉄の輪」","東方Project 早苗&諏訪子編","Leaf","-",3,"60,265,286,299,309,317,0,0,0,322",110070,110071,"[O.N.G.E.K.I.]1.25-E-0199" +5,101045,"【SSR】東風谷 早苗[秘術「グレイソーマタージ」]",2022,"秘術「グレイソーマタージ」","東方Project 早苗&諏訪子編","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]1.25-E-0196" +4,101047,"【SSR】早乙女 彩華[デイドリーム・フェアリーズ]",1006,"デイドリーム・フェアリーズ","奏坂学園","Aqua","高校3年生",3,"65,270,291,304,314,322,0,0,0,327",100040,100081,"[O.N.G.E.K.I.]1.20-0005" +4,101048,"【SSR】桜井 春菜[デイドリーム・フェアリーズ]",1007,"デイドリーム・フェアリーズ","奏坂学園","Fire","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",100038,100079,"[O.N.G.E.K.I.]1.20-0007" +4,101049,"【N】皇城 セツナ",1016,"","奏坂学園","Leaf","高校3年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"" +4,101050,"【R】皇城 セツナ[シュータードレス]",1016,"シュータードレス","奏坂学園","Leaf","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" +3,101051,"【R】歳納 京子[対戦相手専用]",30001,"対戦相手専用","ゆるゆり♪♪","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100000,"" +3,101052,"【R】杉浦 綾乃[対戦相手専用]",30002,"対戦相手専用","ゆるゆり♪♪","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100000,"" +3,101053,"【R】リムル=テンペスト[人型][対戦相手専用]",29001,"対戦相手専用","転生したらスライムだった件","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100000,"" +3,101054,"【R】ミリム・ナーヴァ[対戦相手専用]",29003,"対戦相手専用","転生したらスライムだった件","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100000,"" +4,101055,"【R】涼風 青葉[イーグルジャンプ]",39001,"イーグルジャンプ","NEW GAME!!","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0165" +4,101056,"【R】八神 コウ[イーグルジャンプ]",39002,"イーグルジャンプ","NEW GAME!!","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0168" +4,101057,"【R】遠山 りん[イーグルジャンプ]",39006,"イーグルジャンプ","NEW GAME!!","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0176" +4,101058,"【R】滝本 ひふみ[イーグルジャンプ]",39003,"イーグルジャンプ","NEW GAME!!","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.20-E-0170" +4,101059,"【R】篠田 はじめ[イーグルジャンプ]",39004,"イーグルジャンプ","NEW GAME!!","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0172" +4,101060,"【R】飯島 ゆん[イーグルジャンプ]",39005,"イーグルジャンプ","NEW GAME!!","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110084,110085,"[O.N.G.E.K.I.]1.20-E-0174" +4,101061,"【R】桜 ねね[イーグルジャンプ]",39009,"イーグルジャンプ","NEW GAME!!","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0182" +4,101062,"【R】望月 紅葉[イーグルジャンプ]",39007,"イーグルジャンプ","NEW GAME!!","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0178" +4,101063,"【R】鳴海 ツバメ[イーグルジャンプ]",39008,"イーグルジャンプ","NEW GAME!!","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",115146,115147,"[O.N.G.E.K.I.]1.20-E-0180" +4,101064,"【SR】涼風 青葉[祭と言えばりんご飴です!]",39001,"祭と言えばりんご飴です!","NEW GAME!!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0166" +4,101065,"【SR】八神 コウ[こうすると簡単だよ]",39002,"こうすると簡単だよ","NEW GAME!!","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0169" +4,101066,"【SR】遠山 りん[相談はまず私にしてね]",39006,"相談はまず私にしてね","NEW GAME!!","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110088,110089,"[O.N.G.E.K.I.]1.20-E-0177" +4,101067,"【SR】滝本 ひふみ[……似合う?]",39003,"……似合う?","NEW GAME!!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110138,110139,"[O.N.G.E.K.I.]1.20-E-0171" +4,101068,"【SR】篠田 はじめ[メガ粒子レクエイムシュート!]",39004,"メガ粒子レクエイムシュート!","NEW GAME!!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0173" +4,101069,"【SR】飯島 ゆん[ええ後輩持てて幸せや]",39005,"ええ後輩持てて幸せや","NEW GAME!!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110114,110115,"[O.N.G.E.K.I.]1.20-E-0175" +4,101070,"【SR】桜 ねね[あおっち大人っぽい!]",39009,"あおっち大人っぽい!","NEW GAME!!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0183" +4,101071,"【SR】望月 紅葉[私も負けていられません]",39007,"私も負けていられません","NEW GAME!!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0179" +4,101072,"【SR】鳴海 ツバメ[今日の夕飯はお餅です!]",39008,"今日の夕飯はお餅です!","NEW GAME!!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110086,110087,"[O.N.G.E.K.I.]1.20-E-0181" +4,101073,"【SR】涼風 青葉[やっぱり海は最高です!]",39001,"やっぱり海は最高です!","NEW GAME!!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115022,115023,"[O.N.G.E.K.I.]1.20-E-0167" +4,101074,"【R】光[Arcaea]",36001,"Arcaea","Arcaea","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0077" +4,101075,"【R】対立[Arcaea]",36001,"Arcaea","Arcaea","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0081" +4,101076,"【R】紅[Arcaea]",36001,"Arcaea","Arcaea","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0085" +4,101077,"【R】レーテー[Arcaea]",36001,"Arcaea","Arcaea","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0086" +4,101078,"【R】エト[Arcaea]",36001,"Arcaea","Arcaea","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",115146,115147,"[O.N.G.E.K.I.]1.20-E-0087" +4,101079,"【R】ルナ[Arcaea]",36001,"Arcaea","Arcaea","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110014,110015,"[O.N.G.E.K.I.]1.20-E-0088" +4,101080,"【R】サヤ[Arcaea]",36001,"Arcaea","Arcaea","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110084,110085,"[O.N.G.E.K.I.]1.20-E-0089" +4,101081,"【R】叶永[Arcaea]",36001,"Arcaea","Arcaea","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0090" +4,101082,"【SR】光[Zero]",36001,"Zero","Arcaea","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115064,115065,"[O.N.G.E.K.I.]1.20-E-0078" +4,101083,"【SR】対立[Axium]",36001,"Axium","Arcaea","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0082" +4,101084,"【SR】光[Fracture]",36001,"Fracture","Arcaea","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.20-E-0079" +4,101085,"【SR】対立[Grievous Lady]",36001,"Grievous Lady","Arcaea","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",115036,115037,"[O.N.G.E.K.I.]1.20-E-0083" +4,101086,"【SSR】光&対立[Summer]",36001,"Summer","Arcaea","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",110070,110071,"[O.N.G.E.K.I.]1.20-E-0080" +4,101087,"【SSR】対立[Tempest]",36001,"Tempest","Arcaea","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.20-E-0084" +4,101088,"【R】パトリシア・オブ・エンド[冥界三姉妹]",41001,"冥界三姉妹","ノラと皇女と野良猫ハート","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0139" +4,101089,"【R】黒木 未知[桜ヶ淵学園2年生]",41002,"桜ヶ淵学園2年生","ノラと皇女と野良猫ハート","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100003,100044,"[O.N.G.E.K.I.]1.20-E-0143" +4,101090,"【R】夕莉 シャチ[桜ヶ淵学園2年生]",41003,"桜ヶ淵学園2年生","ノラと皇女と野良猫ハート","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.20-E-0146" +4,101091,"【R】明日原 ユウキ[桜ヶ淵学園1年生]",41004,"桜ヶ淵学園1年生","ノラと皇女と野良猫ハート","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0149" +4,101092,"【R】高田 ノブチナ[桜ヶ淵学園2年生]",41005,"桜ヶ淵学園2年生","ノラと皇女と野良猫ハート","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0152" +4,101093,"【R】ユウラシア・オブ・エンド[冥界三姉妹]",41006,"冥界三姉妹","ノラと皇女と野良猫ハート","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0155" +4,101094,"【R】ルーシア・オブ・エンド[冥界三姉妹]",41007,"冥界三姉妹","ノラと皇女と野良猫ハート","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.20-E-0158" +4,101095,"【R】アイリス・ディセンバー・アンクライ[忘国の皇女]",41008,"忘国の皇女","ノラと皇女と野良猫ハート","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0162" +4,101096,"【SR】パトリシア・オブ・エンド[海が好き!でもわたしは、音ゲーも好きなんだい!]",41001,"海が好き!でもわたしは、音ゲーも好きなんだい!","ノラと皇女と野良猫ハート","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0140" +4,101097,"【SR】黒木 未知[海が好き!でもわたしは、音ゲーも好きなんだい!]",41002,"海が好き!でもわたしは、音ゲーも好きなんだい!","ノラと皇女と野良猫ハート","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100018,100059,"[O.N.G.E.K.I.]1.20-E-0144" +4,101098,"【SR】夕莉 シャチ[海が好き!でもわたしは、音ゲーも好きなんだい!]",41003,"海が好き!でもわたしは、音ゲーも好きなんだい!","ノラと皇女と野良猫ハート","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110086,110087,"[O.N.G.E.K.I.]1.20-E-0147" +4,101099,"【SR】明日原 ユウキ[海が好き!でもわたしは、音ゲーも好きなんだい!]",41004,"海が好き!でもわたしは、音ゲーも好きなんだい!","ノラと皇女と野良猫ハート","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0150" +4,101100,"【SR】高田 ノブチナ[海が好き!でもわたしは、音ゲーも好きなんだい!]",41005,"海が好き!でもわたしは、音ゲーも好きなんだい!","ノラと皇女と野良猫ハート","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0153" +4,101101,"【SR】ユウラシア・オブ・エンド[海が好き!でもわたしは、音ゲーも好きなんだい!]",41006,"海が好き!でもわたしは、音ゲーも好きなんだい!","ノラと皇女と野良猫ハート","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110140,110141,"[O.N.G.E.K.I.]1.20-E-0156" +4,101102,"【SR】ルーシア・オブ・エンド[海が好き!でもわたしは、音ゲーも好きなんだい!]",41007,"海が好き!でもわたしは、音ゲーも好きなんだい!","ノラと皇女と野良猫ハート","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0159" +4,101103,"【SR】アイリス・ディセンバー・アンクライ[海が好き!でもわたしは、音ゲーも好きなんだい!]",41008,"海が好き!でもわたしは、音ゲーも好きなんだい!","ノラと皇女と野良猫ハート","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0163" +4,101104,"【SR】パトリシア・オブ・エンド[知りたがりの皇女様]",41001,"知りたがりの皇女様","ノラと皇女と野良猫ハート","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",120068,120069,"[O.N.G.E.K.I.]1.20-E-0141" +4,101105,"【SR】黒木 未知[恋に恋する風紀委員]",41002,"恋に恋する風紀委員","ノラと皇女と野良猫ハート","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.20-E-0145" +4,101106,"【SR】夕莉 シャチ[恋する環境装置]",41003,"恋する環境装置","ノラと皇女と野良猫ハート","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",120070,120071,"[O.N.G.E.K.I.]1.20-E-0148" +4,101107,"【SR】明日原 ユウキ[天真爛漫バイトギャル]",41004,"天真爛漫バイトギャル","ノラと皇女と野良猫ハート","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",120074,120075,"[O.N.G.E.K.I.]1.20-E-0151" +4,101108,"【SR】高田 ノブチナ[任侠はぐれオオカミ]",41005,"任侠はぐれオオカミ","ノラと皇女と野良猫ハート","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",120018,120019,"[O.N.G.E.K.I.]1.20-E-0154" +4,101109,"【SR】ユウラシア・オブ・エンド[みんなのアイドル]",41006,"みんなのアイドル","ノラと皇女と野良猫ハート","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",105052,105053,"[O.N.G.E.K.I.]1.20-E-0157" +4,101110,"【SR】ルーシア・オブ・エンド[武闘派お姉さま]",41007,"武闘派お姉さま","ノラと皇女と野良猫ハート","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110094,110095,"[O.N.G.E.K.I.]1.20-E-0160" +4,101111,"【SR】アイリス・ディセンバー・アンクライ[忘却を追い越す一歩]",41008,"忘却を追い越す一歩","ノラと皇女と野良猫ハート","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",120074,120075,"[O.N.G.E.K.I.]1.20-E-0164" +4,101112,"【SSR】パトリシア・オブ・エンド[臆病で勇敢なブレイブハート]",41001,"臆病で勇敢なブレイブハート","ノラと皇女と野良猫ハート","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",115030,115031,"[O.N.G.E.K.I.]1.20-E-0142" +4,101113,"【R】白鳥 天葉[トロワアンジュ]",32001,"トロワアンジュ","Re:ステージ!プリズムステップ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0116" +4,101114,"【R】帆風 奏[トロワアンジュ]",32001,"トロワアンジュ","Re:ステージ!プリズムステップ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0118" +4,101115,"【R】緋村 那岐咲[トロワアンジュ]",32001,"トロワアンジュ","Re:ステージ!プリズムステップ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.20-E-0120" +4,101116,"【SR】白鳥 天葉[On:STAGE!!]",32001,"On:STAGE!!","Re:ステージ!プリズムステップ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0117" +4,101117,"【SR】帆風 奏[On:STAGE!!]",32001,"On:STAGE!!","Re:ステージ!プリズムステップ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0119" +4,101118,"【SR】緋村 那岐咲[On:STAGE!!]",32001,"On:STAGE!!","Re:ステージ!プリズムステップ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110086,110087,"[O.N.G.E.K.I.]1.20-E-0121" +4,101119,"【R】坂東 美久龍[テトラルキア]",32001,"テトラルキア","Re:ステージ!プリズムステップ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0122" +4,101120,"【R】西館 ハク[テトラルキア]",32001,"テトラルキア","Re:ステージ!プリズムステップ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0124" +4,101121,"【R】南風野 朱莉[テトラルキア]",32001,"テトラルキア","Re:ステージ!プリズムステップ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0126" +4,101122,"【R】城北 玄刃[テトラルキア]",32001,"テトラルキア","Re:ステージ!プリズムステップ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0128" +4,101123,"【SR】坂東 美久龍[Next:STAGE!!]",32001,"Next:STAGE!!","Re:ステージ!プリズムステップ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0123" +4,101124,"【SR】西館 ハク[On:STAGE!!]",32001,"On:STAGE!!","Re:ステージ!プリズムステップ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0125" +4,101125,"【SR】南風野 朱莉[Make My Day]",32001,"Make My Day","Re:ステージ!プリズムステップ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0127" +4,101126,"【SR】城北 玄刃[On:STAGE!!]",32001,"On:STAGE!!","Re:ステージ!プリズムステップ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0129" +4,101127,"【SR】式宮 舞菜[PJs:PARTY!!]",32001,"PJs:PARTY!!","Re:ステージ!プリズムステップ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.20-E-0130" +4,101128,"【SR】月坂 紗由[PJs:PARTY!!]",32001,"PJs:PARTY!!","Re:ステージ!プリズムステップ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110094,110095,"[O.N.G.E.K.I.]1.20-E-0132" +4,101129,"【SR】市杵島 瑞葉[PJs:PARTY!!]",32001,"PJs:PARTY!!","Re:ステージ!プリズムステップ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110072,110073,"[O.N.G.E.K.I.]1.20-E-0133" +4,101130,"【SR】柊 かえ[PJs:PARTY!!]",32001,"PJs:PARTY!!","Re:ステージ!プリズムステップ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115052,115053,"[O.N.G.E.K.I.]1.20-E-0134" +4,101131,"【SR】本城 香澄[PJs:PARTY!!]",32001,"PJs:PARTY!!","Re:ステージ!プリズムステップ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.20-E-0135" +4,101132,"【SR】長谷川 みい[PJs:PARTY!!]",32001,"PJs:PARTY!!","Re:ステージ!プリズムステップ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110094,110095,"[O.N.G.E.K.I.]1.20-E-0136" +4,101133,"【SR+】式宮 舞菜[ハッピータイフーン]",32001,"ハッピータイフーン","Re:ステージ!プリズムステップ","Leaf","-",12,"53,240,263,278,290,300,0,0,0,300",115022,115023,"[O.N.G.E.K.I.]1.20-E-0131" +4,101146,"【R】六石 陽菜[Flower]",42001,"Flower","CUE!","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.20-E-0001" +4,101147,"【R】天童 悠希[Bird]",42002,"Bird","CUE!","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0005" +4,101148,"【R】夜峰 美晴[Wind]",42003,"Wind","CUE!","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0008" +4,101149,"【R】丸山 利恵[Moon]",42004,"Moon","CUE!","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0011" +4,101150,"【SR】六石 陽菜[Forever Friends]",42001,"Forever Friends","CUE!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100018,100059,"[O.N.G.E.K.I.]1.20-E-0002" +4,101151,"【SR】天童 悠希[Forever Friends]",42002,"Forever Friends","CUE!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0006" +4,101152,"【SR】夜峰 美晴[Forever Friends]",42003,"Forever Friends","CUE!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0009" +4,101153,"【SR】丸山 利恵[Forever Friends]",42004,"Forever Friends","CUE!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0012" +4,101154,"【SR】六石 陽菜[桜の花舞う教室で]",42001,"桜の花舞う教室で","CUE!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110086,110087,"[O.N.G.E.K.I.]1.20-E-0003" +4,101155,"【SR】天童 悠希[三兵衛、秋の感謝フェア]",42002,"三兵衛、秋の感謝フェア","CUE!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110094,110095,"[O.N.G.E.K.I.]1.20-E-0007" +4,101156,"【SR】夜峰 美晴[ゆっくり消える虹を見て]",42003,"ゆっくり消える虹を見て","CUE!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",120074,120075,"[O.N.G.E.K.I.]1.20-E-0010" +4,101157,"【SR】丸山 利恵[カストールの安息日]",42004,"カストールの安息日","CUE!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.20-E-0013" +4,101158,"【SSR】六石 陽菜[See You Everyday]",42001,"See You Everyday","CUE!","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",110070,110071,"[O.N.G.E.K.I.]1.20-E-0004" +4,101159,"【R】中野 一花[中野家の五つ子]",38001,"中野家の五つ子","五等分の花嫁","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0056" +4,101160,"【R】中野 二乃[中野家の五つ子]",38002,"中野家の五つ子","五等分の花嫁","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0059" +4,101161,"【R】中野 三玖[中野家の五つ子]",38003,"中野家の五つ子","五等分の花嫁","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100002,100043,"[O.N.G.E.K.I.]1.20-E-0062" +4,101162,"【R】中野 四葉[中野家の五つ子]",38004,"中野家の五つ子","五等分の花嫁","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110142,110143,"[O.N.G.E.K.I.]1.20-E-0065" +4,101163,"【R】中野 五月[中野家の五つ子]",38005,"中野家の五つ子","五等分の花嫁","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.20-E-0068" +4,101164,"【SR】中野 一花[頼れる長女]",38001,"頼れる長女","五等分の花嫁","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0057" +4,101165,"【SR】中野 二乃[まっすぐ次女]",38002,"まっすぐ次女","五等分の花嫁","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0060" +4,101166,"【SR】中野 三玖[ミステリアス三女]",38003,"ミステリアス三女","五等分の花嫁","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100017,100058,"[O.N.G.E.K.I.]1.20-E-0063" +4,101167,"【SR】中野 四葉[笑顔の四女]",38004,"笑顔の四女","五等分の花嫁","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0066" +4,101168,"【SR】中野 五月[しっかり五女]",38005,"しっかり五女","五等分の花嫁","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0069" +4,101169,"【SSR】中野 一花[五等分の花嫁]",38001,"五等分の花嫁","五等分の花嫁","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",105000,105001,"[O.N.G.E.K.I.]1.20-E-0058" +4,101170,"【SSR】中野 二乃[五等分の花嫁]",38002,"五等分の花嫁","五等分の花嫁","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",105000,105001,"[O.N.G.E.K.I.]1.20-E-0061" +4,101171,"【SSR】中野 三玖[五等分の花嫁]",38003,"五等分の花嫁","五等分の花嫁","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",105000,105001,"[O.N.G.E.K.I.]1.20-E-0064" +4,101172,"【SSR】中野 四葉[五等分の花嫁]",38004,"五等分の花嫁","五等分の花嫁","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",105000,105001,"[O.N.G.E.K.I.]1.20-E-0067" +4,101173,"【SSR】中野 五月[五等分の花嫁]",38005,"五等分の花嫁","五等分の花嫁","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",105000,105001,"[O.N.G.E.K.I.]1.20-E-0070" +4,101176,"【R】星乃 一歌[プロジェクトセカイ]",43001,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0097" +4,101177,"【R】天馬 咲希[プロジェクトセカイ]",43002,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100003,100044,"[O.N.G.E.K.I.]1.20-E-0099" +4,101178,"【R】望月 穂波[プロジェクトセカイ]",43003,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.20-E-0101" +4,101179,"【R】日野森 志歩[プロジェクトセカイ]",43004,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0103" +4,101180,"【R】桐谷 遥[プロジェクトセカイ]",43005,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0105" +4,101181,"【R】花里 みのり[プロジェクトセカイ]",43006,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0107" +4,101182,"【R】日野森 雫[プロジェクトセカイ]",43007,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.20-E-0109" +4,101183,"【R】桃井 愛莉[プロジェクトセカイ]",43008,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100002,100043,"[O.N.G.E.K.I.]1.20-E-0111" +4,101184,"【SR】星乃 一歌[Leo/need]",43001,"Leo/need","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0098" +4,101185,"【SR】天馬 咲希[Leo/need]",43002,"Leo/need","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0100" +4,101186,"【SR】望月 穂波[Leo/need]",43003,"Leo/need","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",120074,120075,"[O.N.G.E.K.I.]1.20-E-0102" +4,101187,"【SR】日野森 志歩[Leo/need]",43004,"Leo/need","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0104" +4,101188,"【SR】桐谷 遥[MORE MORE JUMP!]",43005,"MORE MORE JUMP!","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0106" +4,101189,"【SR】花里 みのり[MORE MORE JUMP!]",43006,"MORE MORE JUMP!","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",120074,120075,"[O.N.G.E.K.I.]1.20-E-0108" +4,101190,"【SR】日野森 雫[MORE MORE JUMP!]",43007,"MORE MORE JUMP!","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0110" +4,101191,"【SR】桃井 愛莉[MORE MORE JUMP!]",43008,"MORE MORE JUMP!","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0112" +4,101192,"【SR】初音ミク[Leo/need]",5000,"Leo/need","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110086,110087,"[O.N.G.E.K.I.]1.20-E-0113" +4,101193,"【SR】初音ミク[MORE MORE JUMP!]",5000,"MORE MORE JUMP!","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110114,110115,"[O.N.G.E.K.I.]1.20-E-0114" +4,101197,"【R】サンドリヨン[真夜中の戦姫]",44001,"真夜中の戦姫","Wonderland Wars","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0091" +4,101198,"【R】シュネーヴィッツェン[不滅の白雪]",44002,"不滅の白雪","Wonderland Wars","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0093" +4,101199,"【R】リトル・アリス[不思議の国の少女]",44003,"不思議の国の少女","Wonderland Wars","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0095" +4,101200,"【SR】サンドリヨン[「タイムオブプリンセス」]",44001,"「タイムオブプリンセス」","Wonderland Wars","Aqua","-",2,"55,227,242,257,272,287,0,0,0,287",100033,100074,"[O.N.G.E.K.I.]1.20-E-0092" +4,101201,"【SR】シュネーヴィッツェン[「ホワイトライトブレス」]",44002,"「ホワイトライトブレス」","Wonderland Wars","Fire","-",2,"55,227,242,257,272,287,0,0,0,287",115026,115027,"[O.N.G.E.K.I.]1.20-E-0094" +4,101202,"【SR】リトル・アリス[「おおきくなるよ!」]",44003,"「おおきくなるよ!」","Wonderland Wars","Aqua","-",2,"55,227,242,257,272,287,0,0,0,287",115026,115027,"[O.N.G.E.K.I.]1.20-E-0096" +4,101203,"【R】鹿目 まどか[魔法少女]",37001,"魔法少女","魔法少女まどか☆マギカ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0014" +4,101204,"【R】暁美 ほむら[魔法少女]",37002,"魔法少女","魔法少女まどか☆マギカ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0019" +4,101205,"【R】美樹 さやか[魔法少女]",37003,"魔法少女","魔法少女まどか☆マギカ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0027" +4,101206,"【R】巴 マミ[魔法少女]",37004,"魔法少女","魔法少女まどか☆マギカ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.20-E-0024" +4,101207,"【R】佐倉 杏子[魔法少女]",37005,"魔法少女","魔法少女まどか☆マギカ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110014,110015,"[O.N.G.E.K.I.]1.20-E-0030" +4,101208,"【SR】鹿目 まどか[見滝原の夏休み]",37001,"見滝原の夏休み","魔法少女まどか☆マギカ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0015" +4,101209,"【SR】暁美 ほむら[見滝原の夏休み]",37002,"見滝原の夏休み","魔法少女まどか☆マギカ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100016,100057,"[O.N.G.E.K.I.]1.20-E-0020" +4,101210,"【SR】美樹 さやか[見滝原の夏休み]",37003,"見滝原の夏休み","魔法少女まどか☆マギカ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100018,100059,"[O.N.G.E.K.I.]1.20-E-0028" +4,101211,"【SR】巴 マミ[見滝原の夏休み]",37004,"見滝原の夏休み","魔法少女まどか☆マギカ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0025" +4,101212,"【SR】佐倉 杏子[見滝原の夏休み]",37005,"見滝原の夏休み","魔法少女まどか☆マギカ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0031" +4,101213,"【SR】鹿目 まどか[弓道場にて]",37001,"弓道場にて","魔法少女まどか☆マギカ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.20-E-0016" +4,101214,"【SR】暁美 ほむら[夕焼けの丘で]",37002,"夕焼けの丘で","魔法少女まどか☆マギカ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",120062,120063,"[O.N.G.E.K.I.]1.20-E-0021" +4,101215,"【SR】美樹 さやか[パジャマパーティー]",37003,"パジャマパーティー","魔法少女まどか☆マギカ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.20-E-0029" +4,101216,"【SR】巴 マミ[氷上の先輩魔法少女]",37004,"氷上の先輩魔法少女","魔法少女まどか☆マギカ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115022,115023,"[O.N.G.E.K.I.]1.20-E-0026" +4,101217,"【SR】佐倉 杏子[分け合いっこ]",37005,"分け合いっこ","魔法少女まどか☆マギカ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",105052,105053,"[O.N.G.E.K.I.]1.20-E-0032" +4,101218,"【SR】鹿目 まどか[マジカル・アロー]",37001,"マジカル・アロー","魔法少女まどか☆マギカ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",120064,120065,"[O.N.G.E.K.I.]1.20-E-0017" +4,101219,"【SR】暁美 ほむら[繰り返す運命]",37002,"繰り返す運命","魔法少女まどか☆マギカ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",120066,120067,"[O.N.G.E.K.I.]1.20-E-0022" +4,101220,"【SSR】鹿目 まどか[円環の理]",37001,"円環の理","魔法少女まどか☆マギカ","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",115030,115031,"[O.N.G.E.K.I.]1.20-E-0018" +4,101221,"【SSR】暁美 ほむら[懐かしい笑顔]",37002,"懐かしい笑顔","魔法少女まどか☆マギカ","Fire","-",3,"60,265,286,299,309,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]1.20-E-0023" +4,101222,"【R】衛藤 可奈美[刀使]",45001,"刀使","刀使の巫女","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0041" +4,101223,"【R】十条 姫和[刀使]",45002,"刀使","刀使の巫女","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.20-E-0044" +4,101224,"【R】柳瀬 舞衣[刀使]",45003,"刀使","刀使の巫女","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.20-E-0046" +4,101225,"【R】糸見 沙耶香[刀使]",45004,"刀使","刀使の巫女","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0048" +4,101226,"【R】益子 薫[刀使]",45005,"刀使","刀使の巫女","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100001,100042,"[O.N.G.E.K.I.]1.20-E-0050" +4,101227,"【R】古波蔵 エレン[刀使]",45006,"刀使","刀使の巫女","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0052" +4,101228,"【R】燕 結芽[刀使]",45007,"刀使","刀使の巫女","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0054" +4,101229,"【SR】衛藤 可奈美[柳生新陰流]",45001,"柳生新陰流","刀使の巫女","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0042" +4,101230,"【SR】十条 姫和[鹿島新當流]",45002,"鹿島新當流","刀使の巫女","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110136,110137,"[O.N.G.E.K.I.]1.20-E-0045" +4,101231,"【SR】柳瀬 舞衣[北辰一刀流]",45003,"北辰一刀流","刀使の巫女","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110088,110089,"[O.N.G.E.K.I.]1.20-E-0047" +4,101232,"【SR】糸見 沙耶香[小野派一刀流]",45004,"小野派一刀流","刀使の巫女","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0049" +4,101233,"【SR】益子 薫[薬丸自顕流]",45005,"薬丸自顕流","刀使の巫女","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100016,100057,"[O.N.G.E.K.I.]1.20-E-0051" +4,101234,"【SR】古波蔵 エレン[タイ捨流]",45006,"タイ捨流","刀使の巫女","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",120074,120075,"[O.N.G.E.K.I.]1.20-E-0053" +4,101235,"【SR】燕 結芽[天然理心流]",45007,"天然理心流","刀使の巫女","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.20-E-0055" +4,101237,"【SSR】衛藤 可奈美[受け継いだ絆]",45001,"受け継いだ絆","刀使の巫女","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.20-E-0043" +4,101239,"【SR+】柏木 咲姫[ONGEKI Memorial Soundtrack Yuki]",1009,"ONGEKI Memorial Soundtrack Yuki","奏坂学園","Aqua","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",120058,120059,"[O.N.G.E.K.I.]Special Card" +4,101240,"【SR+】星咲 あかり[ONGEKI Vocal Party 03]",1000,"ONGEKI Vocal Party 03","プロモーション","Fire","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",100031,100072,"[O.N.G.E.K.I.]VP03-0002" +4,101241,"【SR+】高瀬 梨緒[ONGEKI Vocal Party 03]",1003,"ONGEKI Vocal Party 03","プロモーション","Aqua","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",100031,100072,"[O.N.G.E.K.I.]VP03-0001" +4,101242,"【SR+】桜井 春菜[ONGEKI Vocal Party 03]",1007,"ONGEKI Vocal Party 03","プロモーション","Fire","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",100031,100072,"[O.N.G.E.K.I.]VP03-0003" +4,101243,"【SR+】東雲 つむぎ[ONGEKI Vocal Party 04]",1015,"ONGEKI Vocal Party 04","プロモーション","Aqua","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",120076,120077,"[O.N.G.E.K.I.]VP04-0001" +4,101244,"【SR+】三角 葵[ONGEKI Vocal Party 04]",1002,"ONGEKI Vocal Party 04","プロモーション","Aqua","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",120076,120077,"[O.N.G.E.K.I.]VP04-0002" +4,101245,"【SR+】九條 楓[ONGEKI Vocal Party 04]",1008,"ONGEKI Vocal Party 04","プロモーション","Leaf","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",120076,120077,"[O.N.G.E.K.I.]VP04-0003" +5,101246,"【SR】星咲 あかり[最強 the Splash Dance!!]",1000,"最強 the Splash Dance!!","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",120060,120061,"" +5,101247,"【SR】藤沢 柚子[最強 the Splash Dance!!]",1001,"最強 the Splash Dance!!","奏坂学園","Leaf","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",120060,120061,"" +5,101248,"【SR】三角 葵[最強 the Splash Dance!!]",1002,"最強 the Splash Dance!!","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",120060,120061,"" +5,101249,"【SR】高瀬 梨緒[最強 the Splash Dance!!]",1003,"最強 the Splash Dance!!","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",120060,120061,"" +5,101250,"【SR】結城 莉玖[最強 the Splash Dance!!]",1004,"最強 the Splash Dance!!","奏坂学園","Fire","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",120060,120061,"" +5,101251,"【SR】藍原 椿[最強 the Splash Dance!!]",1005,"最強 the Splash Dance!!","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",120060,120061,"" +5,101252,"【SR】早乙女 彩華[最強 the Splash Dance!!]",1006,"最強 the Splash Dance!!","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",120060,120061,"" +5,101253,"【SR】桜井 春菜[最強 the Splash Dance!!]",1007,"最強 the Splash Dance!!","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",120060,120061,"" +5,101254,"【SR】九條 楓[最強 the Splash Dance!!]",1008,"最強 the Splash Dance!!","奏坂学園","Leaf","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",120060,120061,"" +5,101255,"【SR】柏木 咲姫[最強 the Splash Dance!!]",1009,"最強 the Splash Dance!!","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",120060,120061,"" +5,101256,"【SR】井之原 小星[最強 the Splash Dance!!]",1010,"最強 the Splash Dance!!","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",120060,120061,"" +5,101257,"【SR】逢坂 茜[最強 the Splash Dance!!]",1011,"最強 the Splash Dance!!","奏坂学園","Fire","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",120060,120061,"" +5,101258,"【SR】珠洲島 有栖[最強 the Splash Dance!!]",1012,"最強 the Splash Dance!!","奏坂学園","Aqua","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",120060,120061,"" +5,101259,"【SR】柏木 美亜[最強 the Splash Dance!!]",1013,"最強 the Splash Dance!!","奏坂学園","Fire","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",120060,120061,"" +5,101260,"【SR】日向 千夏[最強 the Splash Dance!!]",1014,"最強 the Splash Dance!!","奏坂学園","Leaf","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",120060,120061,"" +5,101261,"【SR】東雲 つむぎ[最強 the Splash Dance!!]",1015,"最強 the Splash Dance!!","奏坂学園","Aqua","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",120060,120061,"" +5,101262,"【SSR】星咲 あかり[「オンゲキ」LIVE vol.2]",1000,"「オンゲキ」LIVE vol.2","プロモーション","Fire","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",115030,115031,"[O.N.G.E.K.I.]Special Card" +4,101263,"【N】皇城 セツナ",1016,"対戦相手専用","奏坂学園","Fire","高校3年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"" +4,101264,"【R】皇城 セツナ[対戦相手専用]",1016,"対戦相手専用","奏坂学園","Fire","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100000,100000,"" +4,101265,"【N】皇城 セツナ",1016,"対戦相手専用","奏坂学園","Aqua","高校3年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"" +4,101266,"【R】皇城 セツナ[対戦相手専用]",1016,"対戦相手専用","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100000,100000,"" +4,101267,"【SSR】ルーシア・オブ・エンド[動き出した鼓動]",41007,"動き出した鼓動","ノラと皇女と野良猫ハート","Aqua","-",3,"60,265,286,299,309,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]1.20-E-0161" +4,101269,"【R】初音ミク[対戦相手専用]",5000,"対戦相手専用","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100000,"" +4,101270,"【R】初音ミク[対戦相手専用]",5000,"対戦相手専用","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100000,"" +4,101271,"【SSR】初音ミク[一緒に歌おう!]",5000,"一緒に歌おう!","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.20-E-0115" +4,101272,"【R】吉田 優子[まぞく]",45008,"まぞく","まちカドまぞく","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0033" +4,101273,"【R】千代田 桃[魔法少女]",45009,"魔法少女","まちカドまぞく","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0035" +4,101274,"【R】陽夏木 ミカン[魔法少女]",45010,"魔法少女","まちカドまぞく","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0037" +4,101275,"【R】リリス[まぞく]",45011,"まぞく","まちカドまぞく","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.20-E-0039" +4,101276,"【SR】吉田 優子[これで勝ったと思うなよ~!]",45008,"これで勝ったと思うなよ~!","まちカドまぞく","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0034" +4,101277,"【SR】千代田 桃[フレッシュピーチハートシャワー]",45009,"フレッシュピーチハートシャワー","まちカドまぞく","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0036" +4,101278,"【SR】陽夏木 ミカン[魔法少女の休日]",45010,"魔法少女の休日","まちカドまぞく","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",120088,120089,"[O.N.G.E.K.I.]1.20-E-0038" +4,101279,"【SR】リリス[偉大なるごせんぞ様]",45011,"偉大なるごせんぞ様","まちカドまぞく","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100023,100064,"[O.N.G.E.K.I.]1.20-E-0040" +5,101293,"【SSR】柏木 咲姫[Daybreak Angels]",1009,"Daybreak Angels","奏坂学園","Aqua","高校3年生",3,"65,270,291,304,314,322,0,0,0,327",125032,125033,"[O.N.G.E.K.I.]1.25-0002" +5,101294,"【SSR】逢坂 茜[Daybreak Angels]",1011,"Daybreak Angels","奏坂学園","Fire","高校3年生",3,"65,270,291,304,314,322,0,0,0,327",125034,125035,"[O.N.G.E.K.I.]1.25-0003" +5,101295,"【SSR】三角 葵[秘宝を求めて]",1002,"秘宝を求めて","奏坂学園","Aqua","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",125036,125037,"[O.N.G.E.K.I.]1.25-0012" +5,101296,"【SSR】藤沢 柚子[ファンシー・ドリーミング]",1001,"ファンシー・ドリーミング","奏坂学園","Leaf","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",125060,125061,"[O.N.G.E.K.I.]1.25-0030" +5,101297,"【SSR】柏木 美亜[フルーツ・ブライダル]",1013,"フルーツ・ブライダル","奏坂学園","Fire","中学2年生",3,"65,270,291,304,314,322,0,0,0,327",125044,125045,"[O.N.G.E.K.I.]1.25-0023" +5,101298,"【SR】結城 莉玖[夜明けの缶コーヒー]",1004,"夜明けの缶コーヒー","奏坂学園","Fire","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",110066,110067,"[O.N.G.E.K.I.]1.25-0001" +5,101299,"【SR】珠洲島 有栖[進め!有栖隊員]",1012,"進め!有栖隊員","奏坂学園","Aqua","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",125038,125039,"[O.N.G.E.K.I.]1.25-0014" +5,101300,"【SR】井之原 小星[もくもくミッドナイト]",1010,"もくもくミッドナイト","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",125062,125063,"[O.N.G.E.K.I.]1.25-0031" +5,101301,"【SR】高瀬 梨緒[これからも、キミと]",1003,"これからも、キミと","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",125062,125063,"[O.N.G.E.K.I.]1.25-0018" +5,101302,"【SSR】日向 千夏[おいでよ!ジャングルツアーズ]",1014,"おいでよ!ジャングルツアーズ","奏坂学園","Leaf","中学2年生",3,"65,270,291,304,314,322,0,0,0,327",125036,125037,"[O.N.G.E.K.I.]1.25-0015" +5,101303,"【SSR】九條 楓[おいでよ!ジャングルツアーズ]",1008,"おいでよ!ジャングルツアーズ","奏坂学園","Leaf","高校3年生",3,"65,270,291,304,314,322,0,0,0,327",125040,125041,"[O.N.G.E.K.I.]1.25-0013" +5,101304,"【SSR】早乙女 彩華[Will you choose me?]",1006,"Will you choose me?","奏坂学園","Aqua","高校3年生",3,"65,270,291,304,314,322,0,0,0,327",125046,125047,"[O.N.G.E.K.I.]1.25-0021" +5,101305,"【SSR】藍原 椿[Will you choose me?]",1005,"Will you choose me?","奏坂学園","Leaf","高校1年生",3,"65,270,291,304,314,322,0,0,0,327",125048,125049,"[O.N.G.E.K.I.]1.25-0019" +5,101306,"【SR+】日向 千夏[くるりんスマイル]",1014,"くるりんスマイル","奏坂学園","Leaf","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",125066,125067,"[O.N.G.E.K.I.]1.25-0016" +5,101307,"【SR+】東雲 つむぎ[おさかなホリデー]",1015,"おさかなホリデー","奏坂学園","Aqua","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",125068,125069,"[O.N.G.E.K.I.]1.25-0029" +5,101308,"【SR+】柏木 美亜[sweets & me]",1013,"sweets & me","奏坂学園","Fire","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",125070,125071,"[O.N.G.E.K.I.]1.25-0005" +5,101309,"【SR+】藤沢 柚子[お願い!てるてる坊主]",1001,"お願い!てるてる坊主","奏坂学園","Leaf","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",100031,100072,"[O.N.G.E.K.I.]1.25-0017" +5,101310,"【SR+】早乙女 彩華[ぐらぐらエクササイズ]",1006,"ぐらぐらエクササイズ","奏坂学園","Aqua","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",125094,125095,"[O.N.G.E.K.I.]1.25-0024" +5,101311,"【SR+】藍原 椿[おでかけReady?]",1005,"おでかけReady?","奏坂学園","Leaf","高校1年生",12,"53,240,263,278,290,300,0,0,0,300",125096,125097,"[O.N.G.E.K.I.]1.25-0032" +5,101312,"【SSR】日向 千夏[デイドリーム・フェアリーズ]",1014,"デイドリーム・フェアリーズ","奏坂学園","Leaf","中学2年生",3,"65,270,291,304,314,322,0,0,0,327",100039,100080,"[O.N.G.E.K.I.]1.25-0006" +5,101313,"【SSR】柏木 美亜[デイドリーム・フェアリーズ]",1013,"デイドリーム・フェアリーズ","奏坂学園","Fire","中学2年生",3,"65,270,291,304,314,322,0,0,0,327",100038,100079,"[O.N.G.E.K.I.]1.25-0004" +5,101314,"【SSR】東雲 つむぎ[デイドリーム・フェアリーズ]",1015,"デイドリーム・フェアリーズ","奏坂学園","Aqua","中学2年生",3,"65,270,291,304,314,322,0,0,0,327",100040,100081,"[O.N.G.E.K.I.]1.25-0007" +5,101315,"【R】藍原 椿[ウエディングドレス]",1005,"ウエディングドレス","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",125050,125051,"[O.N.G.E.K.I.]1.25-0020" +5,101316,"【R】早乙女 彩華[ウエディングドレス]",1006,"ウエディングドレス","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",125050,125051,"[O.N.G.E.K.I.]1.25-0022" +4,101317,"【SSR】星咲 あかり[ONGEKI Sound Collection 04]",1000,"ONGEKI Sound Collection 04","プロモーション","Fire","高校2年生",3,"65,262,285,300,312,322,0,0,0,327",105020,105021,"[O.N.G.E.K.I.]SC-0004" +5,101318,"【SSR】星咲 あかり[ONGEKI Sound Collection 05]",1000,"ONGEKI Sound Collection 05","プロモーション","Fire","高校2年生",3,"65,262,285,300,312,322,0,0,0,327",125082,125083,"[O.N.G.E.K.I.]SC-0005" +5,101319,"【SSR】高瀬 梨緒[自己評価が高い伝説の幽霊]",1003,"自己評価が高い伝説の幽霊","奏坂学園","Aqua","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",125058,125059,"[O.N.G.E.K.I.]1.25-0025" +5,101320,"【SSR】結城 莉玖[神に仇なす者]",1004,"神に仇なす者","奏坂学園","Fire","高校1年生",3,"65,270,291,304,314,322,0,0,0,327",125052,125053,"[O.N.G.E.K.I.]1.25-0026" +5,101321,"【SR】桜井 春菜[ごはんにする?]",1007,"ごはんにする?","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",125054,125055,"[O.N.G.E.K.I.]1.25-0027" +5,101340,"【SR+】柏木 咲姫[ONGEKI Vocal Party 05]",1009,"ONGEKI Vocal Party 05","プロモーション","Aqua","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",125084,125085,"[O.N.G.E.K.I.]VP05-0001" +5,101341,"【SR+】早乙女 彩華[ONGEKI Vocal Party 05]",1006,"ONGEKI Vocal Party 05","プロモーション","Aqua","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",125084,125085,"[O.N.G.E.K.I.]VP05-0002" +5,101342,"【SR+】日向 千夏[ONGEKI Vocal Party 05]",1014,"ONGEKI Vocal Party 05","プロモーション","Leaf","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",125084,125085,"[O.N.G.E.K.I.]VP05-0003" +6,101343,"【SR+】高瀬 梨緒[ONGEKI Vocal Party 06]",1003,"ONGEKI Vocal Party 06","プロモーション","Aqua","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",115126,115127,"[O.N.G.E.K.I.]VP06-0001" +6,101344,"【SR+】藍原 椿[ONGEKI Vocal Party 06]",1005,"ONGEKI Vocal Party 06","プロモーション","Leaf","高校1年生",12,"53,240,263,278,290,300,0,0,0,300",125086,125087,"[O.N.G.E.K.I.]VP06-0002" +6,101345,"【SR+】桜井 春菜[ONGEKI Vocal Party 06]",1007,"ONGEKI Vocal Party 06","プロモーション","Fire","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",125088,125089,"[O.N.G.E.K.I.]VP06-0003" +5,101346,"【SSR】東雲 つむぎ[気弱な新米防衛プログラム]",1015,"気弱な新米防衛プログラム","奏坂学園","Aqua","中学2年生",3,"65,270,291,304,314,322,0,0,0,327",125056,125057,"[O.N.G.E.K.I.]1.25-0028" +5,101347,"【R】ココア[Is the order a rabbit?]",45012,"Is the order a rabbit?","ご注文はうさぎですか?BLOOM","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0003" +5,101348,"【R】チノ[Is the order a rabbit?]",45013,"Is the order a rabbit?","ご注文はうさぎですか?BLOOM","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0007" +5,101349,"【R】リゼ[Is the order a rabbit?]",45014,"Is the order a rabbit?","ご注文はうさぎですか?BLOOM","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0010" +5,101350,"【R】千夜[Is the order a rabbit?]",45015,"Is the order a rabbit?","ご注文はうさぎですか?BLOOM","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",105004,105005,"[O.N.G.E.K.I.]1.25-E-0013" +5,101351,"【R】シャロ[Is the order a rabbit?]",45016,"Is the order a rabbit?","ご注文はうさぎですか?BLOOM","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",105004,105005,"[O.N.G.E.K.I.]1.25-E-0016" +5,101352,"【R】マヤ[Is the order a rabbit?]",45017,"Is the order a rabbit?","ご注文はうさぎですか?BLOOM","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110084,110085,"[O.N.G.E.K.I.]1.25-E-0019" +5,101353,"【R】メグ[Is the order a rabbit?]",45018,"Is the order a rabbit?","ご注文はうさぎですか?BLOOM","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110012,110013,"[O.N.G.E.K.I.]1.25-E-0022" +5,101354,"【SR】ココア[Dear My Sister]",45012,"Dear My Sister","ご注文はうさぎですか?BLOOM","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0001" +5,101355,"【SR】チノ[Dear My Sister]",45013,"Dear My Sister","ご注文はうさぎですか?BLOOM","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0005" +5,101356,"【SR】リゼ[Dear My Sister]",45014,"Dear My Sister","ご注文はうさぎですか?BLOOM","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.25-E-0008" +5,101357,"【SR】千夜[Dear My Sister]",45015,"Dear My Sister","ご注文はうさぎですか?BLOOM","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110114,110115,"[O.N.G.E.K.I.]1.25-E-0011" +5,101358,"【SR】シャロ[Dear My Sister]",45016,"Dear My Sister","ご注文はうさぎですか?BLOOM","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110072,110073,"[O.N.G.E.K.I.]1.25-E-0014" +5,101359,"【SR】マヤ[Dear My Sister]",45017,"Dear My Sister","ご注文はうさぎですか?BLOOM","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100017,100058,"[O.N.G.E.K.I.]1.25-E-0017" +5,101360,"【SR】メグ[Dear My Sister]",45018,"Dear My Sister","ご注文はうさぎですか?BLOOM","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",125006,125007,"[O.N.G.E.K.I.]1.25-E-0020" +5,101361,"【SR】ココア[ラビットハウスへようこそ]",45012,"ラビットハウスへようこそ","ご注文はうさぎですか?BLOOM","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",120074,120075,"[O.N.G.E.K.I.]1.25-E-0002" +5,101362,"【SR】チノ[狙いを定めて]",45013,"狙いを定めて","ご注文はうさぎですか?BLOOM","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110094,110095,"[O.N.G.E.K.I.]1.25-E-0006" +5,101363,"【SR】リゼ[うさみみ営業中]",45014,"うさみみ営業中","ご注文はうさぎですか?BLOOM","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.25-E-0009" +5,101364,"【SR】千夜[和洋衣装交換]",45015,"和洋衣装交換","ご注文はうさぎですか?BLOOM","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115022,115023,"[O.N.G.E.K.I.]1.25-E-0012" +5,101365,"【SR】シャロ[あなたはどうしてリゼなの]",45016,"あなたはどうしてリゼなの","ご注文はうさぎですか?BLOOM","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115028,115029,"[O.N.G.E.K.I.]1.25-E-0015" +5,101366,"【SR】マヤ[チマメ隊三人官女]",45017,"チマメ隊三人官女","ご注文はうさぎですか?BLOOM","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",125008,125009,"[O.N.G.E.K.I.]1.25-E-0018" +5,101367,"【SR】メグ[chimame march]",45018,"chimame march","ご注文はうさぎですか?BLOOM","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",125010,125011,"[O.N.G.E.K.I.]1.25-E-0021" +5,101368,"【SSR】チノ[お泊まりラビット]",45013,"お泊まりラビット","ご注文はうさぎですか?BLOOM","Leaf","-",3,"60,265,286,299,309,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]1.25-E-0046" +5,101369,"【SSR】ココア[かわいさだけを、ブレンドしました。]",45012,"かわいさだけを、ブレンドしました。","ご注文はうさぎですか?BLOOM","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",110068,110069,"[O.N.G.E.K.I.]1.25-E-0045" +5,101370,"【SSR】チノ[もふもふパジャマ]",45013,"もふもふパジャマ","ご注文はうさぎですか?BLOOM","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",110070,110071,"[O.N.G.E.K.I.]1.25-E-0004" +5,101371,"【R】Two for all[モンソニ!]",45019,"モンソニ!","モンソニ!","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0025" +5,101372,"【R】Angely Diva[モンソニ!]",45020,"モンソニ!","モンソニ!","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.25-E-0028" +5,101373,"【R】白雪姫リボン[モンソニ!]",45021,"モンソニ!","モンソニ!","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0031" +5,101374,"【SR】Two for all[全力アイドル]",45019,"全力アイドル","モンソニ!","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110088,110089,"[O.N.G.E.K.I.]1.25-E-0023" +5,101375,"【SR】Angely Diva[天界の歌姫]",45020,"天界の歌姫","モンソニ!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.25-E-0026" +5,101376,"【SR】白雪姫リボン[森の歌姫]",45021,"森の歌姫","モンソニ!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0029" +5,101377,"【SR】Two for all[二人のキズナ]",45019,"二人のキズナ","モンソニ!","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0024" +5,101378,"【SR】Angely Diva[祝福の歌声]",45020,"祝福の歌声","モンソニ!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0027" +5,101379,"【SR】白雪姫リボン[永遠不滅アイドル]",45021,"永遠不滅アイドル","モンソニ!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110114,110115,"[O.N.G.E.K.I.]1.25-E-0030" +5,101380,"【SSR】Two for all[LINK]",45019,"LINK","モンソニ!","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.25-E-0125" +5,101381,"【SSR】Angely Diva[キズナ Sparkling World]",45020,"キズナ Sparkling World","モンソニ!","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.25-E-0126" +5,101382,"【SSR】白雪姫リボン[初恋は君色メモリー]",45021,"初恋は君色メモリー","モンソニ!","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.25-E-0127" +5,101383,"【R】春音 あいら[オーロラドリーム]",45022,"オーロラドリーム","プリティーシリーズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0049" +5,101384,"【R】天宮 りずむ[オーロラドリーム]",45023,"オーロラドリーム","プリティーシリーズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.25-E-0051" +5,101385,"【R】高峰 みおん[オーロラドリーム]",45024,"オーロラドリーム","プリティーシリーズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0053" +5,101386,"【R】上葉 みあ[ディアマイフューチャー]",45025,"ディアマイフューチャー","プリティーシリーズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0055" +5,101387,"【R】深山 れいな[ディアマイフューチャー]",45026,"ディアマイフューチャー","プリティーシリーズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0057" +5,101388,"【R】志々美 かりん[ディアマイフューチャー]",45027,"ディアマイフューチャー","プリティーシリーズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.25-E-0059" +5,101389,"【R】大瑠璃 あやみ[ディアマイフューチャー]",45028,"ディアマイフューチャー","プリティーシリーズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",115146,115147,"[O.N.G.E.K.I.]1.25-E-0061" +5,101390,"【R】彩瀬 なる[レインボーライブ]",45029,"レインボーライブ","プリティーシリーズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0063" +5,101391,"【R】涼野 いと[レインボーライブ]",45030,"レインボーライブ","プリティーシリーズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0065" +5,101392,"【R】福原 あん[レインボーライブ]",45031,"レインボーライブ","プリティーシリーズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.25-E-0067" +5,101393,"【R】真中 らぁら[プリパラ]",45032,"プリパラ","プリティーシリーズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0085" +5,101394,"【R】南 みれぃ[プリパラ]",45033,"プリパラ","プリティーシリーズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0087" +5,101395,"【R】北条 そふぃ[プリパラ]",45034,"プリパラ","プリティーシリーズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.25-E-0089" +5,101396,"【R】夢川 ゆい[アイドルタイムプリパラ]",45035,"アイドルタイムプリパラ","プリティーシリーズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.25-E-0091" +5,101397,"【R】幸多 みちる[アイドルタイムプリパラ]",45037,"アイドルタイムプリパラ","プリティーシリーズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0093" +5,101398,"【R】虹色 にの[アイドルタイムプリパラ]",45036,"アイドルタイムプリパラ","プリティーシリーズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0095" +5,101399,"【R】桃山 みらい[キラッとプリ☆チャン]",45038,"キラッとプリ☆チャン","プリティーシリーズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0099" +5,101400,"【R】萌黄 えも[キラッとプリ☆チャン]",45039,"キラッとプリ☆チャン","プリティーシリーズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0101" +5,101401,"【R】青葉 りんか[キラッとプリ☆チャン]",45040,"キラッとプリ☆チャン","プリティーシリーズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.25-E-0103" +5,101402,"【SR】春音 あいら[MARs]",45022,"MARs","プリティーシリーズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0048" +5,101403,"【SR】天宮 りずむ[MARs]",45023,"MARs","プリティーシリーズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110086,110087,"[O.N.G.E.K.I.]1.25-E-0050" +5,101404,"【SR】高峰 みおん[MARs]",45024,"MARs","プリティーシリーズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0052" +5,101405,"【SR】上葉 みあ[Prizmmy☆]",45025,"Prizmmy☆","プリティーシリーズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.25-E-0054" +5,101406,"【SR】深山 れいな[Prizmmy☆]",45026,"Prizmmy☆","プリティーシリーズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.25-E-0056" +5,101407,"【SR】志々美 かりん[Prizmmy☆]",45027,"Prizmmy☆","プリティーシリーズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110140,110141,"[O.N.G.E.K.I.]1.25-E-0058" +5,101408,"【SR】大瑠璃 あやみ[Prizmmy☆]",45028,"Prizmmy☆","プリティーシリーズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100018,100059,"[O.N.G.E.K.I.]1.25-E-0060" +5,101409,"【SR】彩瀬 なる[ハッピーレイン♪]",45029,"ハッピーレイン♪","プリティーシリーズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.25-E-0062" +5,101410,"【SR】涼野 いと[ハッピーレイン♪]",45030,"ハッピーレイン♪","プリティーシリーズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.25-E-0064" +5,101411,"【SR】福原 あん[ハッピーレイン♪]",45031,"ハッピーレイン♪","プリティーシリーズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",115038,115039,"[O.N.G.E.K.I.]1.25-E-0066" +5,101412,"【SR】真中 らぁら[SoLaMi♡SMILE]",45032,"SoLaMi♡SMILE","プリティーシリーズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",105042,105043,"[O.N.G.E.K.I.]1.25-E-0084" +5,101413,"【SR】南 みれぃ[SoLaMi♡SMILE]",45033,"SoLaMi♡SMILE","プリティーシリーズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",105042,105043,"[O.N.G.E.K.I.]1.25-E-0086" +5,101414,"【SR】北条 そふぃ[SoLaMi♡SMILE]",45034,"SoLaMi♡SMILE","プリティーシリーズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115028,115029,"[O.N.G.E.K.I.]1.25-E-0088" +5,101415,"【SR】夢川 ゆい[MY☆DREAM]",45035,"MY☆DREAM","プリティーシリーズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110138,110139,"[O.N.G.E.K.I.]1.25-E-0090" +5,101416,"【SR】幸多 みちる[MY☆DREAM]",45037,"MY☆DREAM","プリティーシリーズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0092" +5,101417,"【SR】虹色 にの[MY☆DREAM]",45036,"MY☆DREAM","プリティーシリーズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0094" +5,101418,"【SR】桃山 みらい[Miracle☆Kiratts]",45038,"Miracle☆Kiratts","プリティーシリーズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0098" +5,101419,"【SR】萌黄 えも[Miracle☆Kiratts]",45039,"Miracle☆Kiratts","プリティーシリーズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0100" +5,101420,"【SR】青葉 りんか[Miracle☆Kiratts]",45040,"Miracle☆Kiratts","プリティーシリーズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110088,110089,"[O.N.G.E.K.I.]1.25-E-0102" +5,101421,"【SSR】春音 あいら[なりたい自分にプリズムジャンプ!]",45022,"なりたい自分にプリズムジャンプ!","プリティーシリーズ","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.25-E-0047" +5,101422,"【SSR】真中 らぁら[み~んなトモダチ!!み~んなアイドル!!]",45032,"み~んなトモダチ!!み~んなアイドル!!","プリティーシリーズ","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.25-E-0083" +5,101423,"【SSR】桃山 みらい[キラッと輝け!]",45038,"キラッと輝け!","プリティーシリーズ","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.25-E-0096" +5,101424,"【R】絢辻 詞[輝日東高校2年]",45049,"輝日東高校2年","アマガミSS","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0070" +5,101425,"【R】桜井 梨穂子[輝日東高校2年]",45050,"輝日東高校2年","アマガミSS","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100003,100044,"[O.N.G.E.K.I.]1.25-E-0072" +5,101426,"【R】棚町 薫[輝日東高校2年]",45051,"輝日東高校2年","アマガミSS","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0074" +5,101427,"【R】中多 紗江[輝日東高校1年]",45052,"輝日東高校1年","アマガミSS","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",105004,105005,"[O.N.G.E.K.I.]1.25-E-0076" +5,101428,"【R】七咲 逢[輝日東高校1年]",45053,"輝日東高校1年","アマガミSS","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0078" +5,101429,"【R】森島 はるか[輝日東高校3年]",45054,"輝日東高校3年","アマガミSS","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0080" +5,101430,"【R】橘 美也[輝日東高校1年]",45055,"輝日東高校1年","アマガミSS","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.25-E-0082" +5,101431,"【SR】絢辻 詞[天下無敵の仮面優等生]",45049,"天下無敵の仮面優等生","アマガミSS","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",115006,115007,"[O.N.G.E.K.I.]1.25-E-0069" +5,101432,"【SR】桜井 梨穂子[ぽっちゃり幼馴染]",45050,"ぽっちゃり幼馴染","アマガミSS","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100018,100059,"[O.N.G.E.K.I.]1.25-E-0071" +5,101433,"【SR】棚町 薫[気ままでさばさばした悪友]",45051,"気ままでさばさばした悪友","アマガミSS","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.25-E-0073" +5,101434,"【SR】中多 紗江[ふかふかボディの純情少女]",45052,"ふかふかボディの純情少女","アマガミSS","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",115064,115065,"[O.N.G.E.K.I.]1.25-E-0075" +5,101435,"【SR】七咲 逢[面倒見のよいクールな後輩]",45053,"面倒見のよいクールな後輩","アマガミSS","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0077" +5,101436,"【SR】森島 はるか[男殺しの天然女王]",45054,"男殺しの天然女王","アマガミSS","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.25-E-0079" +5,101437,"【SR】橘 美也[きまぐれ我侭甘えん坊]",45055,"きまぐれ我侭甘えん坊","アマガミSS","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110086,110087,"[O.N.G.E.K.I.]1.25-E-0081" +5,101438,"【SSR】絢辻 詞[私を見つけて]",45049,"私を見つけて","アマガミSS","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",105056,105057,"[O.N.G.E.K.I.]1.25-E-0068" +5,101439,"【R】水原 千鶴[レンカノ]",45041,"レンカノ","彼女、お借りします","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.25-E-0115" +5,101440,"【R】七海 麻美[元カノ]",45042,"元カノ","彼女、お借りします","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0118" +5,101441,"【R】更科 瑠夏[レンカノ]",45043,"レンカノ","彼女、お借りします","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0121" +5,101442,"【R】桜沢 墨[レンカノ]",45044,"レンカノ","彼女、お借りします","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",115148,115149,"[O.N.G.E.K.I.]1.25-E-0124" +5,101443,"【SR】水原 千鶴[遊園地デート]",45041,"遊園地デート","彼女、お借りします","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",125012,125013,"[O.N.G.E.K.I.]1.25-E-0113" +5,101444,"【SR】七海 麻美[砂浜デート]",45042,"砂浜デート","彼女、お借りします","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",120074,120075,"[O.N.G.E.K.I.]1.25-E-0116" +5,101445,"【SR】更科 瑠夏[ボルダリングデート]",45043,"ボルダリングデート","彼女、お借りします","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.25-E-0119" +5,101446,"【SR】桜沢 墨[ボウリングデート]",45044,"ボウリングデート","彼女、お借りします","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100016,100057,"[O.N.G.E.K.I.]1.25-E-0122" +5,101447,"【SR】水原 千鶴[清楚可憐な理想の彼女]",45041,"清楚可憐な理想の彼女","彼女、お借りします","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0114" +5,101448,"【SR】七海 麻美[ゆるふわ系小悪魔彼女]",45042,"ゆるふわ系小悪魔彼女","彼女、お借りします","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.25-E-0117" +5,101449,"【SR】更科 瑠夏[超積極的妹系アイドル彼女]",45043,"超積極的妹系アイドル彼女","彼女、お借りします","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110094,110095,"[O.N.G.E.K.I.]1.25-E-0120" +5,101450,"【SR】桜沢 墨[笑顔がかわいい頑張りや彼女]",45044,"笑顔がかわいい頑張りや彼女","彼女、お借りします","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",115064,115065,"[O.N.G.E.K.I.]1.25-E-0123" +5,101451,"【SSR】水原 千鶴[たった一度のレンタルで、輝き出すリアルがある!]",45041,"たった一度のレンタルで、輝き出すリアルがある!","彼女、お借りします","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",100138,100139,"[O.N.G.E.K.I.]1.25-E-0112" +5,101452,"【R】エミリア[王選候補者]",45056,"王選候補者","Re:ゼロから始める異世界生活","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.25-E-0144" +5,101453,"【R】レム[ロズワール邸メイド]",45057,"ロズワール邸メイド","Re:ゼロから始める異世界生活","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0148" +5,101454,"【R】ラム[ロズワール邸メイド]",45058,"ロズワール邸メイド","Re:ゼロから始める異世界生活","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0152" +5,101455,"【R】ベアトリス[禁書庫の司書]",45059,"禁書庫の司書","Re:ゼロから始める異世界生活","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0153" +5,101456,"【R】フェルト[王選候補者]",45060,"王選候補者","Re:ゼロから始める異世界生活","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0154" +5,101457,"【R】アナスタシア[王選候補者]",45061,"王選候補者","Re:ゼロから始める異世界生活","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110106,110107,"[O.N.G.E.K.I.]1.25-E-0155" +5,101458,"【R】プリシラ[王選候補者]",45062,"王選候補者","Re:ゼロから始める異世界生活","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110106,110107,"[O.N.G.E.K.I.]1.25-E-0156" +5,101459,"【R】クルシュ[王選候補者]",45063,"王選候補者","Re:ゼロから始める異世界生活","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110106,110107,"[O.N.G.E.K.I.]1.25-E-0157" +5,101460,"【SR】エミリア[ハーフエルフの少女]",45056,"ハーフエルフの少女","Re:ゼロから始める異世界生活","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115064,115065,"[O.N.G.E.K.I.]1.25-E-0141" +5,101461,"【SR】レム[鬼族のメイド]",45057,"鬼族のメイド","Re:ゼロから始める異世界生活","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0146" +5,101462,"【SR】ラム[鬼族のメイド]",45058,"鬼族のメイド","Re:ゼロから始める異世界生活","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.25-E-0149" +5,101463,"【SR】エミリア[お団子食べる?]",45056,"お団子食べる?","Re:ゼロから始める異世界生活","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",105008,105009,"[O.N.G.E.K.I.]1.25-E-0142" +5,101464,"【SR】レム[三人でお出かけ]",45057,"三人でお出かけ","Re:ゼロから始める異世界生活","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110094,110095,"[O.N.G.E.K.I.]1.25-E-0147" +5,101465,"【SR】ラム[姉妹の初詣]",45058,"姉妹の初詣","Re:ゼロから始める異世界生活","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",120074,120075,"[O.N.G.E.K.I.]1.25-E-0150" +5,101466,"【SR】エミリア[天使の休息]",45056,"天使の休息","Re:ゼロから始める異世界生活","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115036,115037,"[O.N.G.E.K.I.]1.25-E-0143" +5,101468,"【SR】ラム[制服姉妹]",45058,"制服姉妹","Re:ゼロから始める異世界生活","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",120088,120089,"[O.N.G.E.K.I.]1.25-E-0151" +5,101469,"【SSR】レム[純白の花嫁]",45057,"純白の花嫁","Re:ゼロから始める異世界生活","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",110070,110071,"[O.N.G.E.K.I.]1.25-E-0145" +5,101470,"【SSR】レム[常夏の異世界]",45057,"常夏の異世界","Re:ゼロから始める異世界生活","Aqua","-",3,"60,265,286,299,309,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]1.25-E-0159" +5,101471,"【SSR】エミリア[満開の花の下で]",45056,"満開の花の下で","Re:ゼロから始める異世界生活","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",125042,125043,"[O.N.G.E.K.I.]1.25-E-0158" +5,101472,"【R】ハチロク[レイルロオド]",45064,"レイルロオド","まいてつ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0164" +5,101473,"【R】右田 日々姫[駆け出しデザイナー]",45065,"駆け出しデザイナー","まいてつ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0167" +5,101474,"【R】雛衣 ポーレット[御一夜市長]",45066,"御一夜市長","まいてつ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.25-E-0170" +5,101475,"【R】れいな[レイルロオド]",45067,"レイルロオド","まいてつ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0172" +5,101476,"【R】蓑笠 凪[蓑笠鍛冶店の看板娘]",45068,"蓑笠鍛冶店の看板娘","まいてつ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0174" +5,101477,"【R】早瀬 ふかみ[クマ川下りの看板娘]",45069,"クマ川下りの看板娘","まいてつ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.25-E-0176" +5,101478,"【SR】ハチロク[絵に描いたような日ノ本撫子]",45064,"絵に描いたような日ノ本撫子","まいてつ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0161" +5,101479,"【SR】右田 日々姫[にぃにに恋する絵描きの少女]",45065,"にぃにに恋する絵描きの少女","まいてつ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.25-E-0165" +5,101480,"【SR】雛衣 ポーレット[市長で社長ながんばりやさん]",45066,"市長で社長ながんばりやさん","まいてつ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110086,110087,"[O.N.G.E.K.I.]1.25-E-0168" +5,101481,"【SR】れいな[静かなる開戦]",45067,"静かなる開戦","まいてつ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0171" +5,101482,"【SR】蓑笠 凪[凪の欲しいもの]",45068,"凪の欲しいもの","まいてつ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0173" +5,101483,"【SR】早瀬 ふかみ[クマ川から見上げて]",45069,"クマ川から見上げて","まいてつ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100018,100059,"[O.N.G.E.K.I.]1.25-E-0175" +5,101484,"【SR】ハチロク[8620形蒸気機関車]",45064,"8620形蒸気機関車","まいてつ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",125018,125019,"[O.N.G.E.K.I.]1.25-E-0163" +5,101485,"【SR】ハチロク[御一夜市物語]",45064,"御一夜市物語","まいてつ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.25-E-0162" +5,101486,"【SR】右田 日々姫[日ノ本を代表する車両デザイナー]",45065,"日ノ本を代表する車両デザイナー","まいてつ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",105042,105043,"[O.N.G.E.K.I.]1.25-E-0166" +5,101487,"【SR】雛衣 ポーレット[家族で足湯体験]",45066,"家族で足湯体験","まいてつ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",105042,105043,"[O.N.G.E.K.I.]1.25-E-0169" +5,101488,"【SSR】ハチロク[もう一度、出発進行!!]",45064,"もう一度、出発進行!!","まいてつ","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",110070,110071,"[O.N.G.E.K.I.]1.25-E-0160" +5,101489,"【SR】一筒[老頭牌]",12,"老頭牌","奏坂学園","Aqua","-",2,"55,227,242,257,272,287,0,0,0,287",125072,125073,"[O.N.G.E.K.I.]1.25-E-0032" +5,101490,"【SR】九筒[老頭牌]",12,"老頭牌","奏坂学園","Aqua","-",2,"55,227,242,257,272,287,0,0,0,287",125072,125073,"[O.N.G.E.K.I.]1.25-E-0033" +5,101491,"【SR】一萬[老頭牌]",12,"老頭牌","奏坂学園","Fire","-",2,"55,227,242,257,272,287,0,0,0,287",125072,125073,"[O.N.G.E.K.I.]1.25-E-0034" +5,101492,"【SR】九萬[老頭牌]",12,"老頭牌","奏坂学園","Fire","-",2,"55,227,242,257,272,287,0,0,0,287",125072,125073,"[O.N.G.E.K.I.]1.25-E-0035" +5,101493,"【SR】一索[老頭牌]",12,"老頭牌","奏坂学園","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",125072,125073,"[O.N.G.E.K.I.]1.25-E-0036" +5,101494,"【SR】九索[老頭牌]",12,"老頭牌","奏坂学園","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",125072,125073,"[O.N.G.E.K.I.]1.25-E-0037" +5,101495,"【SR】東[風牌]",13,"風牌","奏坂学園","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",125074,125075,"[O.N.G.E.K.I.]1.25-E-0038" +5,101496,"【SR】南 [風牌]",13,"風牌","奏坂学園","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",125074,125075,"[O.N.G.E.K.I.]1.25-E-0039" +5,101497,"【SR】西[風牌]",13,"風牌","奏坂学園","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",125074,125075,"[O.N.G.E.K.I.]1.25-E-0040" +5,101498,"【SR】北[風牌]",13,"風牌","奏坂学園","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",125074,125075,"[O.N.G.E.K.I.]1.25-E-0041" +5,101499,"【SR】白[三元牌]",14,"三元牌","奏坂学園","Aqua","-",2,"55,227,242,257,272,287,0,0,0,287",125076,125077,"[O.N.G.E.K.I.]1.25-E-0042" +5,101500,"【SR】發[三元牌]",14,"三元牌","奏坂学園","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",125076,125077,"[O.N.G.E.K.I.]1.25-E-0043" +5,101501,"【SR】中[三元牌]",14,"三元牌","奏坂学園","Fire","-",2,"55,227,242,257,272,287,0,0,0,287",125076,125077,"[O.N.G.E.K.I.]1.25-E-0044" +5,101502,"【SR】九條 楓[ツモ!]",1008,"ツモ!","奏坂学園","Leaf","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",125090,125091,"[O.N.G.E.K.I.]1.25-0008" +5,101503,"【SR】逢坂 茜[奏坂高校三年生]",1011,"奏坂高校三年生","奏坂学園","Fire","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",125092,125093,"[O.N.G.E.K.I.]1.25-0010" +5,101504,"【SR】珠洲島 有栖[奏坂高校一年生]",1012,"奏坂高校一年生","奏坂学園","Aqua","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",125092,125093,"[O.N.G.E.K.I.]1.25-0011" +5,101505,"【SR】九條 楓[奏坂高校三年生]",1008,"奏坂高校三年生","奏坂学園","Leaf","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",125092,125093,"[O.N.G.E.K.I.]1.25-0009" +5,101506,"【R】Two for all[対戦相手専用]",45019,"対戦相手専用","モンソニ!","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100000,"" +5,101507,"【R】Angely Diva[対戦相手専用]",45020,"対戦相手専用","モンソニ!","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100000,"" +5,101508,"【R】白雪姫リボン[対戦相手専用]",45021,"対戦相手専用","モンソニ!","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100000,"" +5,101509,"【R】メグメグ[ガンナー]",45045,"ガンナー","#コンパス","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0105" +5,101510,"【R】双挽 乃保[アタッカー]",45046,"アタッカー","#コンパス","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0107" +5,101511,"【R】魔法少女リリカ[ガンナー]",45047,"ガンナー","#コンパス","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0109" +5,101512,"【R】コクリコット ブランシュ[スプリンター]",45048,"スプリンター","#コンパス","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0111" +5,101513,"【SR】メグメグ[バイオレンストリガー]",45045,"バイオレンストリガー","#コンパス","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.25-E-0104" +5,101514,"【SR】双挽 乃保[キレキャリオン]",45046,"キレキャリオン","#コンパス","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0106" +5,101515,"【SR】魔法少女リリカ[アルカリレットウセイ]",45047,"アルカリレットウセイ","#コンパス","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110136,110137,"[O.N.G.E.K.I.]1.25-E-0108" +5,101516,"【SR】コクリコット ブランシュ[撥条少女時計]",45048,"撥条少女時計","#コンパス","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0110" +5,101517,"【SR】草津 結衣奈[湯夢色バトン]",25001,"湯夢色バトン","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125020,125021,"[O.N.G.E.K.I.]1.25-E-0177" +5,101518,"【SR】箱根 彩耶[湯夢色バトン]",25001,"湯夢色バトン","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125020,125021,"[O.N.G.E.K.I.]1.25-E-0179" +5,101519,"【SR】秋保 那菜子[湯夢色バトン]",25001,"湯夢色バトン","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125024,125025,"[O.N.G.E.K.I.]1.25-E-0181" +5,101520,"【SR】有馬 輪花[湯夢色バトン]",25001,"湯夢色バトン","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125022,125023,"[O.N.G.E.K.I.]1.25-E-0183" +5,101521,"【SR】道後 泉海[湯夢色バトン]",25001,"湯夢色バトン","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125026,125027,"[O.N.G.E.K.I.]1.25-E-0185" +5,101522,"【SR】登別 綾瀬[湯夢色バトン]",25001,"湯夢色バトン","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125022,125023,"[O.N.G.E.K.I.]1.25-E-0187" +5,101523,"【SR】下呂 美月[湯夢色バトン]",25001,"湯夢色バトン","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125024,125025,"[O.N.G.E.K.I.]1.25-E-0189" +5,101524,"【SR】有馬 楓花[湯夢色バトン]",25001,"湯夢色バトン","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125026,125027,"[O.N.G.E.K.I.]1.25-E-0192" +5,101525,"【SR】奏・バーデン・由布院[湯夢色バトン]",25001,"湯夢色バトン","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125020,125021,"[O.N.G.E.K.I.]1.25-E-0194" +5,101526,"【SSR】有馬 楓花[有馬涼風川座敷]",25001,"有馬涼風川座敷","温泉むすめ","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",125028,125029,"[O.N.G.E.K.I.]1.25-E-0191" +5,101527,"【SR】草津 結衣奈[草津温泉]",25001,"草津温泉","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125030,125031,"[O.N.G.E.K.I.]1.25-E-0178" +5,101528,"【SR】箱根 彩耶[箱根温泉]",25001,"箱根温泉","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125030,125031,"[O.N.G.E.K.I.]1.25-E-0180" +5,101529,"【SR】秋保 那菜子[秋保温泉]",25001,"秋保温泉","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125030,125031,"[O.N.G.E.K.I.]1.25-E-0182" +5,101530,"【SR】有馬 輪花[有馬温泉]",25001,"有馬温泉","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125030,125031,"[O.N.G.E.K.I.]1.25-E-0184" +5,101531,"【SR】道後 泉海[道後温泉]",25001,"道後温泉","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125030,125031,"[O.N.G.E.K.I.]1.25-E-0186" +5,101532,"【SR】登別 綾瀬[登別温泉]",25001,"登別温泉","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125030,125031,"[O.N.G.E.K.I.]1.25-E-0188" +5,101533,"【SR】下呂 美月[下呂温泉]",25001,"下呂温泉","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125030,125031,"[O.N.G.E.K.I.]1.25-E-0190" +5,101534,"【SR】有馬 楓花[有馬温泉]",25001,"有馬温泉","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125030,125031,"[O.N.G.E.K.I.]1.25-E-0193" +5,101535,"【SR】奏・バーデン・由布院[由布院温泉]",25001,"由布院温泉","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125030,125031,"[O.N.G.E.K.I.]1.25-E-0195" +5,101536,"【SR】星咲 あかり[No Limit STARRED HEART]",1000,"No Limit STARRED HEART","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"" +5,101537,"【SR】藤沢 柚子[No Limit STARRED HEART]",1001,"No Limit STARRED HEART","奏坂学園","Leaf","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"[O.N.G.E.K.I.]Special Card" +5,101538,"【SR】三角 葵[No Limit STARRED HEART]",1002,"No Limit STARRED HEART","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"" +5,101539,"【SR】高瀬 梨緒[No Limit STARRED HEART]",1003,"No Limit STARRED HEART","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"" +5,101540,"【SR】結城 莉玖[No Limit STARRED HEART]",1004,"No Limit STARRED HEART","奏坂学園","Fire","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"" +5,101541,"【SR】藍原 椿[No Limit STARRED HEART]",1005,"No Limit STARRED HEART","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"" +5,101542,"【SR】早乙女 彩華[No Limit STARRED HEART]",1006,"No Limit STARRED HEART","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"" +5,101543,"【SR】桜井 春菜[No Limit STARRED HEART]",1007,"No Limit STARRED HEART","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"" +5,101544,"【SR】九條 楓[No Limit STARRED HEART]",1008,"No Limit STARRED HEART","奏坂学園","Leaf","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"" +5,101545,"【SR】柏木 咲姫[No Limit STARRED HEART]",1009,"No Limit STARRED HEART","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"" +5,101546,"【SR】井之原 小星[No Limit STARRED HEART]",1010,"No Limit STARRED HEART","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"" +5,101547,"【SR】逢坂 茜[No Limit STARRED HEART]",1011,"No Limit STARRED HEART","奏坂学園","Fire","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"" +5,101548,"【SR】珠洲島 有栖[No Limit STARRED HEART]",1012,"No Limit STARRED HEART","奏坂学園","Aqua","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"" +5,101549,"【SR】柏木 美亜[No Limit STARRED HEART]",1013,"No Limit STARRED HEART","奏坂学園","Fire","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"" +5,101550,"【SR】日向 千夏[No Limit STARRED HEART]",1014,"No Limit STARRED HEART","奏坂学園","Leaf","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"" +5,101551,"【SR】東雲 つむぎ[No Limit STARRED HEART]",1015,"No Limit STARRED HEART","奏坂学園","Aqua","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"" +5,101552,"【SR】皇城 セツナ[No Limit STARRED HEART]",1016,"No Limit STARRED HEART","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"" +5,101553,"【SSR】星咲 あかり[3rd Anniversary]",1000,"3rd Anniversary","プロモーション","Fire","高校2年生",3,"52,237,252,267,282,297,0,0,0,302",110070,110071,"[O.N.G.E.K.I.]Special Card" +5,101554,"【SSR】星咲 あかり[私たちは、負けない!]",1000,"私たちは、負けない!","プロモーション","Fire","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",105020,105021,"[O.N.G.E.K.I.]Special Card" +5,101555,"【SSR】藤沢 柚子[私たちは、負けない!]",1001,"私たちは、負けない!","プロモーション","Leaf","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",105020,105021,"" +5,101556,"【SSR】三角 葵[私たちは、負けない!]",1002,"私たちは、負けない!","プロモーション","Aqua","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",105020,105021,"" +5,101557,"【SSR】皇城 セツナ[私たちは、負けない!]",1016,"私たちは、負けない!","プロモーション","Leaf","高校3年生",3,"60,257,280,295,307,317,0,0,0,322",105020,105021,"" +5,101558,"【SR】星咲 あかり[STARRED HEART]",1000,"STARRED HEART","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",125078,125079,"[O.N.G.E.K.I.]Special Card" +5,101559,"【SR】結城 莉玖[STARRED HEART]",1004,"STARRED HEART","奏坂学園","Fire","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",125078,125079,"[O.N.G.E.K.I.]Special Card" +5,101560,"【SR】九條 楓[STARRED HEART]",1008,"STARRED HEART","奏坂学園","Leaf","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",125078,125079,"[O.N.G.E.K.I.]Special Card" +5,101561,"【SR】珠洲島 有栖[STARRED HEART]",1012,"STARRED HEART","奏坂学園","Aqua","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",125078,125079,"[O.N.G.E.K.I.]Special Card" +5,101562,"【SR】東雲 つむぎ[STARRED HEART]",1015,"STARRED HEART","奏坂学園","Aqua","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",125078,125079,"[O.N.G.E.K.I.]Special Card" +5,101568,"【SSR】桃山 みらい[プリティーシリーズ 10th Anniversary]",45038,"プリティーシリーズ 10th Anniversary","プリティーシリーズ","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",100031,100072,"[O.N.G.E.K.I.]1.25-E-0097" +5,101578,"【SR】博麗 霊夢[TOUHOU MEGANE]",2000,"TOUHOU MEGANE","東方Project","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",105042,105043,"[O.N.G.E.K.I.]1.25-E-0130" +5,101579,"【SR】霧雨 魔理沙[TOUHOU MEGANE]",2001,"TOUHOU MEGANE","東方Project","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",105044,105045,"[O.N.G.E.K.I.]1.25-E-0131" +5,101580,"【SR】十六夜 咲夜[TOUHOU MEGANE]",2002,"TOUHOU MEGANE","東方Project","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.25-E-0133" +5,101581,"【SR】レミリア・スカーレット[TOUHOU MEGANE]",2003,"TOUHOU MEGANE","東方Project","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",115036,115037,"[O.N.G.E.K.I.]1.25-E-0135" +5,101582,"【SR】フランドール・スカーレット[TOUHOU MEGANE]",2004,"TOUHOU MEGANE","東方Project","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",105044,105045,"[O.N.G.E.K.I.]1.25-E-0136" +5,101583,"【SR】紅 美鈴[TOUHOU MEGANE]",2005,"TOUHOU MEGANE","東方Project","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0137" +5,101584,"【SR】パチュリー・ノーレッジ[TOUHOU MEGANE]",2006,"TOUHOU MEGANE","東方Project","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.25-E-0139" +5,101585,"【SR】チルノ[TOUHOU MEGANE]",2007,"TOUHOU MEGANE","東方Project","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.25-E-0140" +5,101586,"【SSR】十六夜 咲夜[完全で瀟洒な眼鏡従者]",2002,"完全で瀟洒な眼鏡従者","東方Project","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",110120,110121,"[O.N.G.E.K.I.]1.25-E-0132" +5,101587,"【SSR】パチュリー・ノーレッジ[知識と日陰の眼鏡少女]",2006,"知識と日陰の眼鏡少女","東方Project","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",120094,120095,"[O.N.G.E.K.I.]1.25-E-0138" +5,101588,"【SSR】レミリア・スカーレット[眼鏡の紅い悪魔姉妹]",2003,"眼鏡の紅い悪魔姉妹","東方Project","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",125014,125015,"[O.N.G.E.K.I.]1.25-E-0134" +5,101343,"【SR+】高瀬 梨緒[ONGEKI Vocal Party 06]",1003,"ONGEKI Vocal Party 06","プロモーション","Aqua","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",115126,115127,"[O.N.G.E.K.I.]VP06-0001" +5,101344,"【SR+】藍原 椿[ONGEKI Vocal Party 06]",1005,"ONGEKI Vocal Party 06","プロモーション","Leaf","高校1年生",12,"53,240,263,278,290,300,0,0,0,300",125086,125087,"[O.N.G.E.K.I.]VP06-0002" +5,101345,"【SR+】桜井 春菜[ONGEKI Vocal Party 06]",1007,"ONGEKI Vocal Party 06","プロモーション","Fire","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",125088,125089,"[O.N.G.E.K.I.]VP06-0003" +6,100467,"【SSR】チルノ[凍符「パーフェクトフリーズ」]",2007,"凍符「パーフェクトフリーズ」","東方Project","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.30-E-0038" +6,101322,"【SSR】皇城 セツナ[デイドリーム・フェアリーズ]",1016,"デイドリーム・フェアリーズ","奏坂学園","Fire","高校3年生",3,"65,270,291,304,314,322,0,0,0,327",130014,130015,"[O.N.G.E.K.I.]1.30-0003" +6,101323,"【SSR】藍原 椿[デイドリーム・エンジェルズ]",1005,"デイドリーム・エンジェルズ","奏坂学園","Leaf","高校1年生",3,"70,300,313,323,330,335,0,0,0,340",130016,130017,"[O.N.G.E.K.I.]1.30-0023" +6,101325,"【SSR】藤沢 柚子[デイドリーム・エンジェルズ]",1001,"デイドリーム・エンジェルズ","奏坂学園","Leaf","高校2年生",3,"70,300,313,323,330,335,0,0,0,340",130020,130021,"[O.N.G.E.K.I.]1.30-0014" +6,101326,"【SSR】高瀬 梨緒[デイドリーム・エンジェルズ]",1003,"デイドリーム・エンジェルズ","奏坂学園","Aqua","高校2年生",3,"70,300,313,323,330,335,0,0,0,340",130022,130023,"[O.N.G.E.K.I.]1.30-0021" +6,101328,"【SSR】星咲 あかり[デイドリーム・エンジェルズ]",1000,"デイドリーム・エンジェルズ","奏坂学園","Fire","高校2年生",3,"70,300,313,323,330,335,0,0,0,340",130026,130027,"[O.N.G.E.K.I.]1.30-0013" +6,101329,"【SSR】三角 葵[デイドリーム・エンジェルズ]",1002,"デイドリーム・エンジェルズ","奏坂学園","Aqua","高校2年生",3,"70,300,313,323,330,335,0,0,0,340",130028,130029,"[O.N.G.E.K.I.]1.30-0015" +6,101330,"【SSR】結城 莉玖[デイドリーム・エンジェルズ]",1004,"デイドリーム・エンジェルズ","奏坂学園","Fire","高校1年生",3,"70,300,313,323,330,335,0,0,0,340",130030,130031,"[O.N.G.E.K.I.]1.30-0022" +6,101563,"【N】皇城 セツナ",1016,"","奏坂学園","Fire","高校3年生",0,"50,197,212,227,242,257,287,317,347,347",130092,130093,"[O.N.G.E.K.I.]1.30-0008" +6,101564,"【SSR】皇城 セツナ[ONGEKI Sound Collection 06]",1016,"ONGEKI Sound Collection 06","プロモーション","Fire","高校3年生",3,"65,262,285,300,312,322,0,0,0,327",130096,130097,"[O.N.G.E.K.I.]SC-0006" +6,101566,"【SR】皇城 セツナ[てきぱきリサーチ]",1016,"てきぱきリサーチ","奏坂学園","Fire","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",130054,130055,"[O.N.G.E.K.I.]1.30-0005" +6,101567,"【SSR】皇城 セツナ[漆黒の執行者]",1016,"漆黒の執行者","奏坂学園","Fire","高校3年生",3,"65,270,291,304,314,322,0,0,0,327",130052,130053,"[O.N.G.E.K.I.]1.30-0002" +6,101573,"【SR+】三角 葵[ここだけSmiling]",1002,"ここだけSmiling","奏坂学園","Aqua","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",130090,130091,"[O.N.G.E.K.I.]1.30-0025" +6,101574,"【SR+】柏木 咲姫[お届けハウスキーパー]",1009,"お届けハウスキーパー","奏坂学園","Aqua","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",110066,110067,"[O.N.G.E.K.I.]1.30-0024" +6,101575,"【SR+】逢坂 茜[罰ゲームはメイド服]",1011,"罰ゲームはメイド服","奏坂学園","Fire","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",125084,125085,"[O.N.G.E.K.I.]1.30-0016" +6,101576,"【SR+】日向 千夏[カミナリにご用心]",1014,"カミナリにご用心","奏坂学園","Leaf","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",125084,125085,"[O.N.G.E.K.I.]1.30-0030" +6,101577,"【SR+】皇城 セツナ[薄暮バイオリニスト]",1016,"薄暮バイオリニスト","奏坂学園","Fire","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",130098,130099,"[O.N.G.E.K.I.]1.30-0004" +6,101589,"【R】皇城 セツナ[シュータードレス]",1016,"シュータードレス","奏坂学園","Fire","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",130056,130057,"[O.N.G.E.K.I.]1.30-0006" +6,101592,"【SSR】星咲 あかり[憧れの先を目指して]",1000,"憧れの先を目指して","奏坂学園","Fire","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",130050,130051,"[O.N.G.E.K.I.]1.30-0001" +6,101593,"【SSR】日向 千夏[一緒に入ろ?]",1014,"一緒に入ろ?","奏坂学園","Leaf","中学2年生",3,"65,270,291,304,314,322,0,0,0,327",130060,130061,"[O.N.G.E.K.I.]1.30-0012" +6,101594,"【SSR】東雲 つむぎ[イルミネーション・ファンタジー]",1015,"イルミネーション・ファンタジー","奏坂学園","Aqua","中学2年生",3,"65,270,291,304,314,322,0,0,0,327",130066,130067,"[O.N.G.E.K.I.]1.30-0020" +6,101595,"【SSR】早乙女 彩華[bitter kiss, bitter girl]",1006,"bitter kiss, bitter girl","奏坂学園","Aqua","高校3年生",3,"65,270,291,304,314,322,0,0,0,327",130070,130071,"[O.N.G.E.K.I.]1.30-0027" +6,101601,"【SR】九條 楓[ほっこり湯上がり]",1008,"ほっこり湯上がり","奏坂学園","Leaf","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",125064,125065,"[O.N.G.E.K.I.]1.30-0010" +6,101602,"【SR】藤沢 柚子[ビッグなプレゼント希望]",1001,"ビッグなプレゼント希望","奏坂学園","Leaf","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",130094,130095,"[O.N.G.E.K.I.]1.30-0017" +6,101603,"【SR】結城 莉玖[よりどりチョコバイキング]",1004,"よりどりチョコバイキング","奏坂学園","Fire","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",120088,120089,"[O.N.G.E.K.I.]1.30-0026" +6,101609,"【SSR】三角 葵[湯けむり温泉旅情]",1002,"湯けむり温泉旅情","奏坂学園","Aqua","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",125046,125047,"[O.N.G.E.K.I.]1.30-0009" +6,101610,"【SSR】逢坂 茜[湯けむり温泉旅情]",1011,"湯けむり温泉旅情","奏坂学園","Fire","高校3年生",3,"65,270,291,304,314,322,0,0,0,327",130058,130059,"[O.N.G.E.K.I.]1.30-0011" +6,101611,"【SSR】柏木 咲姫[Christmas with You]",1009,"Christmas with You","奏坂学園","Aqua","高校3年生",3,"65,270,291,304,314,322,0,0,0,327",130062,130063,"[O.N.G.E.K.I.]1.30-0019" +6,101612,"【SSR】桜井 春菜[Christmas with You]",1007,"Christmas with You","奏坂学園","Fire","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",130064,130065,"[O.N.G.E.K.I.]1.30-0018" +6,101613,"【SSR】珠洲島 有栖[満点ショコラティエ]",1012,"満点ショコラティエ","奏坂学園","Aqua","高校1年生",3,"65,270,291,304,314,322,0,0,0,327",130070,130071,"[O.N.G.E.K.I.]1.30-0028" +6,101614,"【SSR】皇城 セツナ[満点ショコラティエ]",1016,"満点ショコラティエ","奏坂学園","Fire","高校3年生",3,"65,270,291,304,314,322,0,0,0,327",130068,130069,"[O.N.G.E.K.I.]1.30-0029" +6,101619,"【R】皇城 セツナ[私服]",1016,"私服","奏坂学園","Fire","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",130056,130057,"[O.N.G.E.K.I.]1.30-0007" +6,101628,"【R】ティアラ[LiGHTs]",46049,"LiGHTs","ラピスリライツ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.30-E-0046" +6,101629,"【R】ロゼッタ[LiGHTs]",46050,"LiGHTs","ラピスリライツ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.30-E-0048" +6,101630,"【R】ラヴィ[LiGHTs]",46051,"LiGHTs","ラピスリライツ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.30-E-0050" +6,101631,"【R】アシュレイ[LiGHTs]",46052,"LiGHTs","ラピスリライツ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.30-E-0052" +6,101632,"【R】リネット[LiGHTs]",46053,"LiGHTs","ラピスリライツ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.30-E-0054" +6,101633,"【R】エミリア[IV KLORE]",46054,"IV KLORE","ラピスリライツ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.30-E-0056" +6,101634,"【R】あるふぁ[IV KLORE]",46055,"IV KLORE","ラピスリライツ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100003,100044,"[O.N.G.E.K.I.]1.30-E-0058" +6,101635,"【R】サルサ[IV KLORE]",46056,"IV KLORE","ラピスリライツ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.30-E-0060" +6,101636,"【R】ガーネット[IV KLORE]",46057,"IV KLORE","ラピスリライツ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.30-E-0062" +6,101637,"【R】ナデシコ[この花は乙女]",46058,"この花は乙女","ラピスリライツ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.30-E-0064" +6,101638,"【R】ツバキ[この花は乙女]",46059,"この花は乙女","ラピスリライツ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.30-E-0066" +6,101639,"【R】カエデ[この花は乙女]",46060,"この花は乙女","ラピスリライツ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.30-E-0068" +6,101640,"【R】ラトゥーラ[シュガーポケッツ]",46061,"シュガーポケッツ","ラピスリライツ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.30-E-0070" +6,101641,"【R】シャンペ[シュガーポケッツ]",46062,"シュガーポケッツ","ラピスリライツ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.30-E-0072" +6,101642,"【R】メアリーベリー[シュガーポケッツ]",46063,"シュガーポケッツ","ラピスリライツ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.30-E-0074" +6,101643,"【R】アンジェリカ[Sadistic★Candy]",46064,"Sadistic★Candy","ラピスリライツ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110000,110001,"[O.N.G.E.K.I.]1.30-E-0076" +6,101644,"【R】ルキフェル[Sadistic★Candy]",46065,"Sadistic★Candy","ラピスリライツ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",115148,115149,"[O.N.G.E.K.I.]1.30-E-0078" +6,101645,"【R】ユエ[supernova]",46066,"supernova","ラピスリライツ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.30-E-0080" +6,101646,"【R】ミルフィーユ[supernova]",46067,"supernova","ラピスリライツ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.30-E-0082" +6,101647,"【R】フィオナ[supernova]",46068,"supernova","ラピスリライツ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.30-E-0084" +6,101648,"【SR】ティアラ[私たちが、新しい「光」になる。]",46049,"私たちが、新しい「光」になる。","ラピスリライツ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.30-E-0045" +6,101649,"【SR】ロゼッタ[私たちが、新しい「光」になる。]",46050,"私たちが、新しい「光」になる。","ラピスリライツ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.30-E-0047" +6,101650,"【SR】ラヴィ[私たちが、新しい「光」になる。]",46051,"私たちが、新しい「光」になる。","ラピスリライツ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.30-E-0049" +6,101651,"【SR】アシュレイ[私たちが、新しい「光」になる。]",46052,"私たちが、新しい「光」になる。","ラピスリライツ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.30-E-0051" +6,101652,"【SR】リネット[私たちが、新しい「光」になる。]",46053,"私たちが、新しい「光」になる。","ラピスリライツ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100138,100139,"[O.N.G.E.K.I.]1.30-E-0053" +6,101653,"【SR】エミリア[奏でましょう、艶やかなる 都市伝説。]",46054,"奏でましょう、艶やかなる 都市伝説。","ラピスリライツ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.30-E-0055" +6,101654,"【SR】あるふぁ[奏でましょう、艶やかなる 都市伝説。]",46055,"奏でましょう、艶やかなる 都市伝説。","ラピスリライツ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",105008,105009,"[O.N.G.E.K.I.]1.30-E-0057" +6,101655,"【SR】サルサ[奏でましょう、艶やかなる 都市伝説。]",46056,"奏でましょう、艶やかなる 都市伝説。","ラピスリライツ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.30-E-0059" +6,101656,"【SR】ガーネット[奏でましょう、艶やかなる 都市伝説。]",46057,"奏でましょう、艶やかなる 都市伝説。","ラピスリライツ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110086,110087,"[O.N.G.E.K.I.]1.30-E-0061" +6,101657,"【SR】ナデシコ[さあ、「ろっく」に参りましょう。]",46058,"さあ、「ろっく」に参りましょう。","ラピスリライツ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110114,110115,"[O.N.G.E.K.I.]1.30-E-0063" +6,101658,"【SR】ツバキ[さあ、「ろっく」に参りましょう。]",46059,"さあ、「ろっく」に参りましょう。","ラピスリライツ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.30-E-0065" +6,101659,"【SR】カエデ[さあ、「ろっく」に参りましょう。]",46060,"さあ、「ろっく」に参りましょう。","ラピスリライツ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.30-E-0067" +6,101660,"【SR】ラトゥーラ[みんなのハートを盗んであげる!]",46061,"みんなのハートを盗んであげる!","ラピスリライツ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.30-E-0069" +6,101661,"【SR】シャンペ[みんなのハートを盗んであげる!]",46062,"みんなのハートを盗んであげる!","ラピスリライツ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.30-E-0071" +6,101662,"【SR】メアリーベリー[みんなのハートを盗んであげる!]",46063,"みんなのハートを盗んであげる!","ラピスリライツ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115028,115029,"[O.N.G.E.K.I.]1.30-E-0073" +6,101663,"【SR】アンジェリカ[きゃっるるーん♪ ハロハロー★]",46064,"きゃっるるーん♪ ハロハロー★","ラピスリライツ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100016,100057,"[O.N.G.E.K.I.]1.30-E-0075" +6,101664,"【SR】ルキフェル[きゃっるるーん♪ ハロハロー★]",46065,"きゃっるるーん♪ ハロハロー★","ラピスリライツ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",105024,105025,"[O.N.G.E.K.I.]1.30-E-0077" +6,101665,"【SR】ユエ[誰よりも、誰よりも「強く」輝きたい。]",46066,"誰よりも、誰よりも「強く」輝きたい。","ラピスリライツ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.30-E-0079" +6,101666,"【SR】ミルフィーユ[誰よりも、誰よりも「強く」輝きたい。]",46067,"誰よりも、誰よりも「強く」輝きたい。","ラピスリライツ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110098,110099,"[O.N.G.E.K.I.]1.30-E-0081" +6,101667,"【SR】フィオナ[誰よりも、誰よりも「強く」輝きたい。]",46068,"誰よりも、誰よりも「強く」輝きたい。","ラピスリライツ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",120074,120075,"[O.N.G.E.K.I.]1.30-E-0083" +6,101668,"【SSR】ティアラ[この世界のアイドルは魔法が使える]",46049,"この世界のアイドルは魔法が使える","ラピスリライツ","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",100138,100139,"[O.N.G.E.K.I.]1.30-E-0044" +6,101669,"【SR】赤座 あかり[わぁい、ちなつちゃんと一緒♪]",30003,"わぁい、ちなつちゃんと一緒♪","ゆるゆり♪♪","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.30-E-0002" +6,101670,"【SR】歳納 京子[てやんでぃ]",30001,"てやんでぃ","ゆるゆり♪♪","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",120018,120019,"[O.N.G.E.K.I.]1.30-E-0004" +6,101671,"【SR】船見 結衣[ラムレーズン食べていいよ]",30001,"ラムレーズン食べていいよ","ゆるゆり♪♪","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",125006,125007,"[O.N.G.E.K.I.]1.30-E-0005" +6,101672,"【SR】吉川 ちなつ[先輩大好きです~~っ]",30003,"先輩大好きです~~っ","ゆるゆり♪♪","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100031,100072,"[O.N.G.E.K.I.]1.30-E-0006" +6,101673,"【SR】杉浦 綾乃[余裕ありま温泉よ]",30002,"余裕ありま温泉よ","ゆるゆり♪♪","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",130082,130083,"[O.N.G.E.K.I.]1.30-E-0008" +6,101674,"【SR】池田 千歳[綾乃ちゃん告白せえへんの?]",30002,"綾乃ちゃん告白せえへんの?","ゆるゆり♪♪","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",130082,130083,"[O.N.G.E.K.I.]1.30-E-0009" +6,101675,"【SR】大室 櫻子[私だって負けないわよ!]",30002,"私だって負けないわよ!","ゆるゆり♪♪","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115028,115029,"[O.N.G.E.K.I.]1.30-E-0010" +6,101676,"【SR】古谷 向日葵[負けませんわよ!]",30002,"負けませんわよ!","ゆるゆり♪♪","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",105042,105043,"[O.N.G.E.K.I.]1.30-E-0011" +6,101677,"【SSR】歳納 京子[七森中 ごらく部!]",30001,"七森中 ごらく部!","ゆるゆり♪♪","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",105000,105001,"[O.N.G.E.K.I.]1.30-E-0003" +6,101678,"【SSR】杉浦 綾乃[七森中 生徒会!]",30002,"七森中 生徒会!","ゆるゆり♪♪","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",100031,100072,"[O.N.G.E.K.I.]1.30-E-0007" +6,101679,"【R】赤座 あかり[対戦相手専用]",30003,"対戦相手専用","ゆるゆり♪♪","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" +6,101680,"【SR】天海 春香[765プロダクション]",46001,"765プロダクション","「アイドルマスター」シリーズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.30-E-0013" +6,101681,"【SR】如月 千早[765プロダクション]",46002,"765プロダクション","「アイドルマスター」シリーズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",130000,130001,"[O.N.G.E.K.I.]1.30-E-0014" +6,101682,"【SR】星井 美希[765プロダクション]",46003,"765プロダクション","「アイドルマスター」シリーズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.30-E-0015" +6,101683,"【SR】島村 卯月[346プロダクション]",46004,"346プロダクション","「アイドルマスター」シリーズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.30-E-0017" +6,101684,"【SR】渋谷 凛[346プロダクション]",46005,"346プロダクション","「アイドルマスター」シリーズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.30-E-0018" +6,101685,"【SR】本田 未央[346プロダクション]",46006,"346プロダクション","「アイドルマスター」シリーズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",130000,130001,"[O.N.G.E.K.I.]1.30-E-0019" +6,101686,"【SR】春日 未来[765プロダクション]",46007,"765プロダクション","「アイドルマスター」シリーズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.30-E-0021" +6,101687,"【SR】最上 静香[765プロダクション]",46008,"765プロダクション","「アイドルマスター」シリーズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125016,125017,"[O.N.G.E.K.I.]1.30-E-0022" +6,101688,"【SR】伊吹 翼[765プロダクション]",46009,"765プロダクション","「アイドルマスター」シリーズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.30-E-0023" +6,101689,"【SR】天道 輝[315プロダクション]",46010,"315プロダクション","「アイドルマスター」シリーズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100016,100057,"[O.N.G.E.K.I.]1.30-E-0025" +6,101690,"【SR】桜庭 薫[315プロダクション]",46011,"315プロダクション","「アイドルマスター」シリーズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100018,100059,"[O.N.G.E.K.I.]1.30-E-0026" +6,101691,"【SR】柏木 翼[315プロダクション]",46012,"315プロダクション","「アイドルマスター」シリーズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100017,100058,"[O.N.G.E.K.I.]1.30-E-0027" +6,101692,"【SR】櫻木 真乃[283プロダクション]",46013,"283プロダクション","「アイドルマスター」シリーズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",125016,125017,"[O.N.G.E.K.I.]1.30-E-0029" +6,101693,"【SR】風野 灯織[283プロダクション]",46014,"283プロダクション","「アイドルマスター」シリーズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.30-E-0030" +6,101694,"【SR】八宮 めぐる[283プロダクション]",46015,"283プロダクション","「アイドルマスター」シリーズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.30-E-0031" +6,101695,"【SSR】天海 春香[なんどでも笑おう]",46001,"なんどでも笑おう","「アイドルマスター」シリーズ","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",130002,130003,"[O.N.G.E.K.I.]1.30-E-0012" +6,101696,"【SSR】島村 卯月[なんどでも笑おう]",46004,"なんどでも笑おう","「アイドルマスター」シリーズ","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",130002,130003,"[O.N.G.E.K.I.]1.30-E-0016" +6,101697,"【SSR】春日 未来[なんどでも笑おう]",46007,"なんどでも笑おう","「アイドルマスター」シリーズ","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",130002,130003,"[O.N.G.E.K.I.]1.30-E-0020" +6,101698,"【SSR】天道 輝[なんどでも笑おう]",46010,"なんどでも笑おう","「アイドルマスター」シリーズ","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",130002,130003,"[O.N.G.E.K.I.]1.30-E-0024" +6,101699,"【SSR】櫻木 真乃[なんどでも笑おう]",46013,"なんどでも笑おう","「アイドルマスター」シリーズ","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",130002,130003,"[O.N.G.E.K.I.]1.30-E-0028" +6,101700,"【SR】芹沢 あさひ[ストレイライト]",46016,"ストレイライト","「アイドルマスター」シリーズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",115036,115037,"[O.N.G.E.K.I.]1.30-E-0033" +6,101701,"【SR】黛 冬優子[ストレイライト]",46017,"ストレイライト","「アイドルマスター」シリーズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115036,115037,"[O.N.G.E.K.I.]1.30-E-0035" +6,101702,"【SR】和泉 愛依[ストレイライト]",46018,"ストレイライト","「アイドルマスター」シリーズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",115036,115037,"[O.N.G.E.K.I.]1.30-E-0037" +6,101703,"【SSR】芹沢 あさひ[隠匿シンギュラリティ]",46016,"隠匿シンギュラリティ","「アイドルマスター」シリーズ","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.30-E-0032" +6,101704,"【SSR】黛 冬優子[隠匿シンギュラリティ]",46017,"隠匿シンギュラリティ","「アイドルマスター」シリーズ","Leaf","-",3,"60,265,286,299,309,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]1.30-E-0034" +6,101705,"【SSR】和泉 愛依[隠匿シンギュラリティ]",46018,"隠匿シンギュラリティ","「アイドルマスター」シリーズ","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",130004,130005,"[O.N.G.E.K.I.]1.30-E-0036" +6,101706,"【R】天海 春香[モデル表示用]",46001,"モデル表示用","「アイドルマスター」シリーズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" +6,101707,"【R】如月 千早[モデル表示用]",46002,"モデル表示用","「アイドルマスター」シリーズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" +6,101708,"【R】星井 美希[モデル表示用]",46003,"モデル表示用","「アイドルマスター」シリーズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" +6,101709,"【R】島村 卯月[モデル表示用]",46004,"モデル表示用","「アイドルマスター」シリーズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" +6,101710,"【R】渋谷 凛[モデル表示用]",46005,"モデル表示用","「アイドルマスター」シリーズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" +6,101711,"【R】本田 未央[モデル表示用]",46006,"モデル表示用","「アイドルマスター」シリーズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" +6,101712,"【R】春日 未来[モデル表示用]",46007,"モデル表示用","「アイドルマスター」シリーズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" +6,101713,"【R】最上 静香[モデル表示用]",46008,"モデル表示用","「アイドルマスター」シリーズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" +6,101714,"【R】伊吹 翼[モデル表示用]",46009,"モデル表示用","「アイドルマスター」シリーズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" +6,101715,"【R】天道 輝[モデル表示用]",46010,"モデル表示用","「アイドルマスター」シリーズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" +6,101716,"【R】桜庭 薫[モデル表示用]",46011,"モデル表示用","「アイドルマスター」シリーズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" +6,101717,"【R】柏木 翼[モデル表示用]",46012,"モデル表示用","「アイドルマスター」シリーズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" +6,101718,"【R】櫻木 真乃[モデル表示用]",46013,"モデル表示用","「アイドルマスター」シリーズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" +6,101719,"【R】風野 灯織[モデル表示用]",46014,"モデル表示用","「アイドルマスター」シリーズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" +6,101720,"【R】八宮 めぐる[モデル表示用]",46015,"モデル表示用","「アイドルマスター」シリーズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" +6,101721,"【R】芹沢 あさひ[モデル表示用]",46016,"モデル表示用","「アイドルマスター」シリーズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" +6,101722,"【R】黛 冬優子[モデル表示用]",46017,"モデル表示用","「アイドルマスター」シリーズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" +6,101723,"【R】和泉 愛依[モデル表示用]",46018,"モデル表示用","「アイドルマスター」シリーズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" +6,101744,"【R】リンカ[ナビゲーター]",46040,"ナビゲーター","グルーヴコースター","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.30-E-0120" +6,101745,"【R】ユメ[ナビゲーター]",46041,"ナビゲーター","グルーヴコースター","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.30-E-0123" +6,101746,"【R】セイネ[ナビゲーター]",46042,"ナビゲーター","グルーヴコースター","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.30-E-0126" +6,101747,"【SR】リンカ[ヘヴンリーフェスティバル]",46040,"ヘヴンリーフェスティバル","グルーヴコースター","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100017,100058,"[O.N.G.E.K.I.]1.30-E-0118" +6,101748,"【SR】ユメ[サンタ]",46041,"サンタ","グルーヴコースター","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100016,100057,"[O.N.G.E.K.I.]1.30-E-0121" +6,101749,"【SR】セイネ[サマー]",46042,"サマー","グルーヴコースター","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100018,100059,"[O.N.G.E.K.I.]1.30-E-0124" +6,101750,"【SR】リンカ[インフィニティハイウェイ]",46040,"インフィニティハイウェイ","グルーヴコースター","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.30-E-0119" +6,101751,"【SR】ユメ[ドリームパーティー]",46041,"ドリームパーティー","グルーヴコースター","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",130012,130013,"[O.N.G.E.K.I.]1.30-E-0122" +6,101752,"【SR】セイネ[スターライトロード]",46042,"スターライトロード","グルーヴコースター","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.30-E-0125" +6,101753,"【SSR】リンカ[ダイヤモンドギャラクシー]",46040,"ダイヤモンドギャラクシー","グルーヴコースター","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.30-E-0117" +6,101754,"【SSR】式宮 碧音[オンゲキ?]",32001,"オンゲキ?","Re:ステージ!プリズムステップ","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.30-E-0146" +6,101755,"【SSR】岬 珊瑚[オンゲキ?]",32001,"オンゲキ?","Re:ステージ!プリズムステップ","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",100033,100074,"[O.N.G.E.K.I.]1.30-E-0147" +6,101756,"【SSR】式宮 舞菜[Happy Birthday!!]",32001,"Happy Birthday!!","Re:ステージ!プリズムステップ","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.30-E-0145" +6,101757,"【R】為栗 メロ[でんこ]",46043,"でんこ","駅メモ!&アワメモ!","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.30-E-0129" +6,101758,"【R】新阪 ルナ[でんこ]",46044,"でんこ","駅メモ!&アワメモ!","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.30-E-0132" +6,101759,"【R】恋浜 みろく[でんこ]",46045,"でんこ","駅メモ!&アワメモ!","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.30-E-0135" +6,101760,"【R】天下 さや[でんこ]",46046,"でんこ","駅メモ!&アワメモ!","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.30-E-0138" +6,101761,"【R】根雨 つむぎ[でんこ]",46047,"でんこ","駅メモ!&アワメモ!","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.30-E-0141" +6,101762,"【R】観音町 ひめ[でんこ]",46048,"でんこ","駅メモ!&アワメモ!","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.30-E-0144" +6,101763,"【SR】為栗 メロ[電子ウサギな]",46043,"電子ウサギな","駅メモ!&アワメモ!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.30-E-0127" +6,101764,"【SR】新阪 ルナ[ハロウィンパーティな]",46044,"ハロウィンパーティな","駅メモ!&アワメモ!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",130000,130001,"[O.N.G.E.K.I.]1.30-E-0130" +6,101765,"【SR】恋浜 みろく[帝都の]",46045,"帝都の","駅メモ!&アワメモ!","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.30-E-0133" +6,101766,"【SR】天下 さや[スクールライフな]",46046,"スクールライフな","駅メモ!&アワメモ!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.30-E-0136" +6,101767,"【SR】根雨 つむぎ[花の妖精な]",46047,"花の妖精な","駅メモ!&アワメモ!","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",125016,125017,"[O.N.G.E.K.I.]1.30-E-0139" +6,101768,"【SR】観音町 ひめ[マジカルな]",46048,"マジカルな","駅メモ!&アワメモ!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.30-E-0142" +6,101769,"【SR】為栗 メロ[ASTERISMな]",46043,"ASTERISMな","駅メモ!&アワメモ!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115088,115089,"[O.N.G.E.K.I.]1.30-E-0128" +6,101770,"【SR】新阪 ルナ[ASTERISMな]",46044,"ASTERISMな","駅メモ!&アワメモ!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100023,100064,"[O.N.G.E.K.I.]1.30-E-0131" +6,101771,"【SR】恋浜 みろく[ASTERISMな]",46045,"ASTERISMな","駅メモ!&アワメモ!","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",115088,115089,"[O.N.G.E.K.I.]1.30-E-0134" +6,101772,"【SR】天下 さや[⊿TRiEDGEな]",46046,"⊿TRiEDGEな","駅メモ!&アワメモ!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",130088,130089,"[O.N.G.E.K.I.]1.30-E-0137" +6,101773,"【SR】根雨 つむぎ[⊿TRiEDGEな]",46047,"⊿TRiEDGEな","駅メモ!&アワメモ!","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100024,100065,"[O.N.G.E.K.I.]1.30-E-0140" +6,101774,"【SR】観音町 ひめ[⊿TRiEDGEな]",46048,"⊿TRiEDGEな","駅メモ!&アワメモ!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",130088,130089,"[O.N.G.E.K.I.]1.30-E-0143" +6,101793,"【R】小豆沢 こはね[プロジェクトセカイ]",46028,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100001,100042,"[O.N.G.E.K.I.]1.30-E-0089" +6,101794,"【R】白石 杏[プロジェクトセカイ]",46029,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.30-E-0091" +6,101795,"【R】東雲 彰人[プロジェクトセカイ]",46030,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.30-E-0093" +6,101796,"【R】青柳 冬弥[プロジェクトセカイ]",46031,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.30-E-0095" +6,101797,"【R】天馬 司[プロジェクトセカイ]",46032,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.30-E-0099" +6,101798,"【R】鳳 えむ[プロジェクトセカイ]",46033,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.30-E-0101" +6,101799,"【R】草薙 寧々[プロジェクトセカイ]",46034,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.30-E-0103" +6,101800,"【R】神代 類[プロジェクトセカイ]",46035,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110142,110143,"[O.N.G.E.K.I.]1.30-E-0105" +6,101801,"【R】宵崎 奏[プロジェクトセカイ]",46036,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.30-E-0109" +6,101802,"【R】朝比奈 まふゆ[プロジェクトセカイ]",46037,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",105004,105005,"[O.N.G.E.K.I.]1.30-E-0111" +6,101803,"【R】東雲 絵名[プロジェクトセカイ]",46038,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.30-E-0113" +6,101804,"【R】暁山 瑞希[プロジェクトセカイ]",46039,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.30-E-0115" +6,101805,"【SR】小豆沢 こはね[Vivid BAD SQUAD]",46028,"Vivid BAD SQUAD","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.30-E-0088" +6,101806,"【SR】白石 杏[Vivid BAD SQUAD]",46029,"Vivid BAD SQUAD","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.30-E-0090" +6,101807,"【SR】東雲 彰人[Vivid BAD SQUAD]",46030,"Vivid BAD SQUAD","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",120074,120075,"[O.N.G.E.K.I.]1.30-E-0092" +6,101808,"【SR】青柳 冬弥[Vivid BAD SQUAD]",46031,"Vivid BAD SQUAD","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.30-E-0094" +6,101809,"【SR】天馬 司[ワンダーランズ×ショウタイム]",46032,"ワンダーランズ×ショウタイム","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115088,115089,"[O.N.G.E.K.I.]1.30-E-0098" +6,101810,"【SR】鳳 えむ[ワンダーランズ×ショウタイム]",46033,"ワンダーランズ×ショウタイム","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115088,115089,"[O.N.G.E.K.I.]1.30-E-0100" +6,101811,"【SR】草薙 寧々[ワンダーランズ×ショウタイム]",46034,"ワンダーランズ×ショウタイム","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.30-E-0102" +6,101812,"【SR】神代 類[ワンダーランズ×ショウタイム]",46035,"ワンダーランズ×ショウタイム","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110094,110095,"[O.N.G.E.K.I.]1.30-E-0104" +6,101813,"【SR】宵崎 奏[25時、ナイトコードで。]",46036,"25時、ナイトコードで。","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",130010,130011,"[O.N.G.E.K.I.]1.30-E-0108" +6,101814,"【SR】朝比奈 まふゆ[25時、ナイトコードで。]",46037,"25時、ナイトコードで。","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",130010,130011,"[O.N.G.E.K.I.]1.30-E-0110" +6,101815,"【SR】東雲 絵名[25時、ナイトコードで。]",46038,"25時、ナイトコードで。","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100031,100072,"[O.N.G.E.K.I.]1.30-E-0112" +6,101816,"【SR】暁山 瑞希[25時、ナイトコードで。]",46039,"25時、ナイトコードで。","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.30-E-0114" +6,101817,"【SR】初音ミク[Vivid BAD SQUAD]",5000,"Vivid BAD SQUAD","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110088,110089,"[O.N.G.E.K.I.]1.30-E-0096" +6,101818,"【SR】初音ミク[ワンダーランズ×ショウタイム]",5000,"ワンダーランズ×ショウタイム","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115028,115029,"[O.N.G.E.K.I.]1.30-E-0106" +6,101819,"【SR】初音ミク[25時、ナイトコードで。]",5000,"25時、ナイトコードで。","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",115038,115039,"[O.N.G.E.K.I.]1.30-E-0116" +6,101820,"【SSR】星乃 一歌[見上げる先に]",43001,"見上げる先に","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",110098,110099,"[O.N.G.E.K.I.]1.30-E-0085" +6,101821,"【SSR】花里 みのり[ライブ前夜の決意]",43006,"ライブ前夜の決意","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.30-E-0086" +6,101822,"【SSR】小豆沢 こはね[セカイで一息]",46028,"セカイで一息","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.30-E-0087" +6,101823,"【SSR】天馬 司[着ぐるみからこんにちは]",46032,"着ぐるみからこんにちは","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",110070,110071,"[O.N.G.E.K.I.]1.30-E-0097" +6,101824,"【SSR】宵崎 奏[画面の向こうに…]",46036,"画面の向こうに…","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",105056,105057,"[O.N.G.E.K.I.]1.30-E-0107" +6,101842,"【R】名取さな[ば~ちゃるな~す]",46019,"ば~ちゃるな~す","名取さな","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",130006,130007,"[O.N.G.E.K.I.]1.30-E-0043" +6,101843,"【SR】名取さな[おはようございナース!]",46019,"おはようございナース!","名取さな","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",120018,120019,"[O.N.G.E.K.I.]1.30-E-0041" +6,101844,"【SR】名取さな[オンゲキに進撃…ってね]",46019,"オンゲキに進撃…ってね","名取さな","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",115032,115033,"[O.N.G.E.K.I.]1.30-E-0042" +6,101845,"【SSR】名取さな[さなちゃんねる王のお言葉を聞きなさ~い!]",46019,"さなちゃんねる王のお言葉を聞きなさ~い!","名取さな","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",130086,130087,"[O.N.G.E.K.I.]1.30-E-0040" +6,101846,"【SSR】名取さな[さなのばくたん。]",46019,"さなのばくたん。","名取さな","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",130008,130009,"[O.N.G.E.K.I.]1.30-E-0039" +6,101847,"【R】初音ミク[対戦相手専用]",5000,"対戦相手専用","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" +6,101848,"【R】初音ミク[対戦相手専用]",5000,"対戦相手専用","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" +6,101849,"【R】初音ミク[対戦相手専用]",5000,"対戦相手専用","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" +6,101850,"【SSR】赤座 あかり[あかり、主人公だよね!?]",30003,"あかり、主人公だよね!?","ゆるゆり♪♪","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",130084,130085,"[O.N.G.E.K.I.]1.30-E-0001" +7,101339,"【SSR】皇城 セツナ[デイドリーム・エンジェルズ]",1016,"デイドリーム・エンジェルズ","奏坂学園","Fire","高校3年生",3,"70,300,313,323,330,335,0,0,0,340",130048,130049,"[O.N.G.E.K.I.]1.35-0032" +7,101569,"【SR+】星咲 あかり[Perfect Shining!!]",1000,"Perfect Shining!!","奏坂学園","Fire","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",135002,135003,"[O.N.G.E.K.I.]1.35-0038" +7,101571,"【SR+】桜井 春菜[My precious holiday]",1007,"My precious holiday","奏坂学園","Fire","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",135006,135007,"[O.N.G.E.K.I.]1.35-0035" +7,101572,"【SR+】井之原 小星[GAME IS LIFE]",1010,"GAME IS LIFE","奏坂学園","Leaf","高校1年生",12,"53,240,263,278,290,300,0,0,0,300",135008,135009,"[O.N.G.E.K.I.]1.35-0039" +7,101599,"【SSR】柏木 美亜[レイニー・カラーズ]",1013,"レイニー・カラーズ","奏坂学園","Fire","中学2年生",3,"65,270,291,304,314,322,0,0,0,327",120006,120007,"[O.N.G.E.K.I.]1.35-0033" +7,101600,"【SSR】結城 莉玖[宙舞うブランコマスター]",1004,"宙舞うブランコマスター","奏坂学園","Fire","高校1年生",3,"65,270,291,304,314,322,0,0,0,327",135000,135001,"[O.N.G.E.K.I.]1.35-0036" +7,101607,"【SR】東雲 つむぎ[雨降りエモーショナル]",1015,"雨降りエモーショナル","奏坂学園","Aqua","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",115000,115001,"[O.N.G.E.K.I.]1.35-0034" +7,101608,"【SR】珠洲島 有栖[噂の敏腕トレーナー]",1012,"噂の敏腕トレーナー","奏坂学園","Aqua","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",125062,125063,"[O.N.G.E.K.I.]1.35-0037" +7,101627,"【SSR】星咲 あかり[4th Anniversary]",1000,"4th Anniversary","奏坂学園","Fire","高校2年生",3,"52,237,252,267,282,297,0,0,0,302",115030,115031,"[O.N.G.E.K.I.]Special Card" +7,101851,"【SSR】星咲 あかり[Winter Memory]",1000,"Winter Memory","奏坂学園","Fire","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",135072,135073,"Winter Memory" +7,101852,"【SR】星咲 あかり[Transcend Lights]",1000,"Transcend Lights","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" +7,101853,"【SR】藤沢 柚子[Transcend Lights]",1001,"Transcend Lights","奏坂学園","Leaf","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" +7,101854,"【SR】三角 葵[Transcend Lights]",1002,"Transcend Lights","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" +7,101855,"【SR】高瀬 梨緒[Transcend Lights]",1003,"Transcend Lights","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" +7,101856,"【SR】結城 莉玖[Transcend Lights]",1004,"Transcend Lights","奏坂学園","Fire","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" +7,101857,"【SR】藍原 椿[Transcend Lights]",1005,"Transcend Lights","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" +7,101858,"【SR】早乙女 彩華[Transcend Lights]",1006,"Transcend Lights","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" +7,101859,"【SR】桜井 春菜[Transcend Lights]",1007,"Transcend Lights","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" +7,101860,"【SR】九條 楓[Transcend Lights]",1008,"Transcend Lights","奏坂学園","Leaf","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" +7,101861,"【SR】柏木 咲姫[Transcend Lights]",1009,"Transcend Lights","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" +7,101862,"【SR】井之原 小星[Transcend Lights]",1010,"Transcend Lights","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" +7,101863,"【SR】逢坂 茜[Transcend Lights]",1011,"Transcend Lights","奏坂学園","Fire","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" +7,101864,"【SR】珠洲島 有栖[Transcend Lights]",1012,"Transcend Lights","奏坂学園","Aqua","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" +7,101865,"【SR】柏木 美亜[Transcend Lights]",1013,"Transcend Lights","奏坂学園","Fire","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" +7,101866,"【SR】日向 千夏[Transcend Lights]",1014,"Transcend Lights","奏坂学園","Leaf","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" +7,101867,"【SR】東雲 つむぎ[Transcend Lights]",1015,"Transcend Lights","奏坂学園","Aqua","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" +7,101868,"【SR】皇城 セツナ[Transcend Lights]",1016,"Transcend Lights","奏坂学園","Fire","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" +7,101869,"【SSR】星咲 あかり[Make UP Future!]",1000,"Make UP Future!","奏坂学園","Fire","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",135014,135015,"Winter Memory" +7,101870,"【SSR】藤沢 柚子[Make UP Future!]",1001,"Make UP Future!","奏坂学園","Leaf","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",135016,135017,"Winter Memory" +7,101871,"【SSR】三角 葵[Make UP Future!]",1002,"Make UP Future!","奏坂学園","Aqua","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",135018,135019,"Winter Memory" +7,101872,"【SSR】高瀬 梨緒[Make UP Future!]",1003,"Make UP Future!","奏坂学園","Aqua","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",135026,135027,"Autumn Memory" +7,101873,"【SSR】結城 莉玖[Make UP Future!]",1004,"Make UP Future!","奏坂学園","Fire","高校1年生",3,"65,270,291,304,314,322,0,0,0,327",135028,135029,"Autumn Memory" +7,101874,"【SSR】藍原 椿[Make UP Future!]",1005,"Make UP Future!","奏坂学園","Leaf","高校1年生",3,"65,270,291,304,314,322,0,0,0,327",135030,135031,"Autumn Memory" +7,101885,"【SSR】皇城 セツナ[Make UP Future!]",1016,"Make UP Future!","奏坂学園","Fire","高校3年生",3,"65,270,291,304,314,322,0,0,0,327",135046,135047,"Autumn Memory" +7,101917,"【SSR】星咲 あかり[Autumn Memory]",1000,"Autumn Memory","奏坂学園","Fire","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",100034,100075,"Autumn Memory" +7,101956,"【SSR】星咲 あかり[O.N.G.E.K.I. R.E.D.]",1000,"O.N.G.E.K.I. R.E.D.","奏坂学園","Fire","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",115116,115117,"Autumn Memory" +7,101957,"【SSR】星咲 あかり[O.N.G.E.K.I. bright]",1000,"O.N.G.E.K.I. bright","奏坂学園","Fire","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",135052,135053,"Winter Memory" +7,101958,"【SR】こんじきニャン[対戦相手専用]",6,"対戦相手専用","-","Fire","-",2,"55,227,242,257,272,287,0,0,0,287",100000,100041,"[O.N.G.E.K.I.]*.**-****" +7,101959,"【SR】高瀬 梨緒[Memory対戦相手専用]",1003,"Memory対戦相手専用","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100000,100041,"[O.N.G.E.K.I.]*.**-****" +7,101960,"【SR】星咲 あかり[Memory対戦相手専用]",1000,"Memory対戦相手専用","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100000,100041,"[O.N.G.E.K.I.]*.**-****" +7,101965,"【SSR】星咲 あかり[Memories of O.N.G.E.K.I.]",1000,"Memories of O.N.G.E.K.I.","奏坂学園","Fire","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]Special Card" +7,101966,"【SSR】星咲 あかり[ONGEKI Sound Collection 07]",1000,"ONGEKI Sound Collection 07","プロモーション","Fire","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",115134,115135,"[O.N.G.E.K.I.]SC-007" diff --git a/titles/cm/cm_data/MU3/static_gacha_cards.csv b/titles/cm/cm_data/MU3/static_gacha_cards.csv new file mode 100644 index 0000000..303e71a --- /dev/null +++ b/titles/cm/cm_data/MU3/static_gacha_cards.csv @@ -0,0 +1,336 @@ +"gachaId","cardId","rarity","weight","isPickup","isSelect" +1070,100984,4,1,0,1 +1070,100997,3,2,0,1 +1070,100998,3,2,0,1 +1070,101020,2,3,0,1 +1070,101021,2,3,0,1 +1070,101022,2,3,0,1 +1067,100982,4,1,0,0 +1067,100983,4,1,0,0 +1067,100996,3,2,0,0 +1068,100075,2,3,0,0 +1068,100182,2,3,0,0 +1068,100348,2,3,0,0 +1068,100232,2,3,0,0 +1068,100417,2,3,0,0 +1068,100755,2,3,0,0 +1068,100077,3,2,0,0 +1068,100271,3,2,0,0 +1068,100425,3,2,0,0 +1068,100758,3,2,0,0 +1068,101000,3,2,0,0 +1068,100284,4,1,0,0 +1068,100767,4,1,0,0 +1068,101293,4,1,0,0 +1069,100069,2,3,0,0 +1069,100183,2,3,0,0 +1069,100349,2,3,0,0 +1069,100233,2,3,0,0 +1069,100416,2,3,0,0 +1069,100071,3,2,0,0 +1069,100272,3,2,0,0 +1069,100427,3,2,0,0 +1069,100805,3,2,0,0 +1069,101300,3,2,0,0 +1069,100285,4,1,0,0 +1069,100768,4,1,0,0 +1069,100988,4,1,0,0 +1071,100275,4,1,0,0 +1071,100437,4,1,0,0 +1071,100780,4,1,0,0 +1071,100006,3,2,0,0 +1071,100007,3,2,0,0 +1071,100249,3,2,0,0 +1071,100262,3,2,0,0 +1071,100418,3,2,0,0 +1071,100003,2,3,0,0 +1071,100004,2,3,0,0 +1071,100173,2,3,0,0 +1071,100223,2,3,0,0 +1071,100339,2,3,0,0 +1071,100692,2,3,0,0 +1072,100017,4,1,0,0 +1072,100276,4,1,0,0 +1072,100760,4,1,0,0 +1072,100015,3,2,0,0 +1072,100016,3,2,0,0 +1072,100250,3,2,0,0 +1072,100263,3,2,0,0 +1072,100423,3,2,0,0 +1072,100765,3,2,0,0 +1072,100012,2,3,0,0 +1072,100013,2,3,0,0 +1072,100174,2,3,0,0 +1072,100224,2,3,0,0 +1072,100340,2,3,0,0 +1072,100693,2,3,0,0 +1073,100026,4,1,0,0 +1073,100277,4,1,0,0 +1073,100761,4,1,0,0 +1073,100024,3,2,0,0 +1073,100025,3,2,0,0 +1073,100251,3,2,0,0 +1073,100264,3,2,0,0 +1073,100430,3,2,0,0 +1073,100021,2,3,0,0 +1073,100022,2,3,0,0 +1073,100175,2,3,0,0 +1073,100225,2,3,0,0 +1073,100341,2,3,0,0 +1073,100694,2,3,0,0 +1011,100454,4,1,0,0 +1011,100980,4,1,0,0 +1011,101553,4,1,0,0 +1011,100253,3,1,0,0 +1011,100241,3,1,0,0 +1011,100240,3,1,0,0 +1011,100239,3,1,0,0 +1011,100238,3,1,0,0 +1011,100237,3,1,0,0 +1011,100236,3,1,0,0 +1011,100261,3,1,0,0 +1011,100246,3,1,0,0 +1011,100245,3,1,0,0 +1011,100242,3,1,0,0 +1011,100243,3,1,0,0 +1011,100254,3,1,0,0 +1011,100338,3,1,0,0 +1011,100337,3,1,0,0 +1011,100336,3,1,0,0 +1011,100248,3,1,0,0 +1011,100247,3,1,0,0 +1011,100244,3,1,0,0 +1011,100259,3,1,0,0 +1011,100257,3,1,0,0 +1011,100258,3,1,0,0 +1011,100636,3,1,0,0 +1011,100634,3,1,0,0 +1011,100255,3,1,0,0 +1011,100256,3,1,0,0 +1011,100252,3,1,0,0 +1011,100638,3,1,0,0 +1011,100639,3,1,0,0 +1011,100637,3,1,0,0 +1011,100772,3,1,0,0 +1011,100667,3,1,0,0 +1011,100666,3,1,0,0 +1011,100665,3,1,0,0 +1011,100643,3,1,0,0 +1011,100640,3,1,0,0 +1011,100641,3,1,0,0 +1011,100642,3,1,0,0 +1011,100688,3,1,0,0 +1011,100645,3,1,0,0 +1011,100646,3,1,0,0 +1011,100644,3,1,0,0 +1012,100644,3,1,0,0 +1012,100646,3,1,0,0 +1012,100645,3,1,0,0 +1012,100688,3,1,0,0 +1012,100642,3,1,0,0 +1012,100641,3,1,0,0 +1012,100640,3,1,0,0 +1012,100643,3,1,0,0 +1012,100665,3,1,0,0 +1012,100666,3,1,0,0 +1012,100667,3,1,0,0 +1012,100634,3,1,0,0 +1012,100636,3,1,0,0 +1012,100772,3,1,0,0 +1012,100638,3,1,0,0 +1012,100637,3,1,0,0 +1012,100639,3,1,0,0 +1012,100252,3,1,0,0 +1012,100256,3,1,0,0 +1012,100255,3,1,0,0 +1012,100258,3,1,0,0 +1012,100257,3,1,0,0 +1012,100259,3,1,0,0 +1012,100244,3,1,0,0 +1012,100247,3,1,0,0 +1012,100248,3,1,0,0 +1012,100336,3,1,0,0 +1012,100337,3,1,0,0 +1012,100338,3,1,0,0 +1012,100254,3,1,0,0 +1012,100243,3,1,0,0 +1012,100242,3,1,0,0 +1012,100245,3,1,0,0 +1012,100246,3,1,0,0 +1012,100261,3,1,0,0 +1012,100236,3,1,0,0 +1012,100237,3,1,0,0 +1012,100238,3,1,0,0 +1012,100239,3,1,0,0 +1012,100240,3,1,0,0 +1012,100241,3,1,0,0 +1012,100253,3,1,0,0 +1012,100454,4,1,0,0 +1012,100980,4,1,0,0 +1012,101553,4,1,0,0 +1074,100985,4,1,0,0 +1074,100999,3,1,0,0 +1074,101000,3,1,0,0 +1074,101023,2,1,0,0 +1074,101024,2,1,0,0 +1075,100060,4,1,0,0 +1075,100434,4,1,0,0 +1075,100059,3,1,0,0 +1075,100268,3,1,0,0 +1075,100420,3,1,0,0 +1075,100763,3,1,0,0 +1075,101003,3,1,0,0 +1075,100057,2,1,0,0 +1075,100179,2,1,0,0 +1075,100229,2,1,0,0 +1075,100345,2,1,0,0 +1075,100415,2,1,0,0 +1076,100054,4,1,0,0 +1076,100282,4,1,0,0 +1076,100726,4,1,0,0 +1076,100053,3,1,0,0 +1076,100269,3,1,0,0 +1076,100422,3,1,0,0 +1076,100757,3,1,0,0 +1076,100051,2,1,0,0 +1076,100180,2,1,0,0 +1076,100230,2,1,0,0 +1076,100346,2,1,0,0 +1076,100414,2,1,0,0 +1077,100984,4,1,0,1 +1077,100997,3,1,0,1 +1077,100998,3,1,0,1 +1077,100986,4,1,0,1 +1077,101001,3,1,0,1 +1077,101002,3,1,0,1 +1077,101025,2,1,0,1 +1077,101026,2,1,0,1 +1077,101027,2,1,0,1 +1081,100987,4,1,0,0 +1081,100988,4,1,0,0 +1081,101003,3,1,0,0 +1085,100008,4,1,0,1 +1085,100017,4,1,0,1 +1085,100026,4,1,0,1 +1085,100034,4,1,0,1 +1085,100041,4,1,0,1 +1085,100048,4,1,0,1 +1085,100054,4,1,0,1 +1085,100060,4,1,0,1 +1085,100066,4,1,0,1 +1085,100078,4,1,0,1 +1085,100072,4,1,0,1 +1085,100084,4,1,0,1 +1085,100090,4,1,0,1 +1085,100282,4,1,0,1 +1085,100285,4,1,0,1 +1085,100284,4,1,0,1 +1085,100286,4,1,0,1 +1085,100280,4,1,0,1 +1085,100276,4,1,0,1 +1085,100277,4,1,0,1 +1085,100275,4,1,0,1 +1085,100278,4,1,0,1 +1085,100431,4,1,0,1 +1085,100407,4,1,0,1 +1085,100432,4,1,0,1 +1085,100433,4,1,0,1 +1085,100434,4,1,0,1 +1085,100435,4,1,0,1 +1085,100436,4,1,0,1 +1085,100437,4,1,0,1 +1085,100438,4,1,0,1 +1085,100439,4,1,0,1 +1085,100760,4,1,0,1 +1085,100761,4,1,0,1 +1085,100779,4,1,0,1 +1085,100767,4,1,0,1 +1085,100780,4,1,0,1 +1085,100784,4,1,0,1 +1085,100768,4,1,0,1 +1085,100725,4,1,0,1 +1085,100726,4,1,0,1 +1085,100984,4,1,0,1 +1085,100985,4,1,0,1 +1085,100987,4,1,0,1 +1085,100988,4,1,0,1 +1085,100986,4,1,0,1 +1085,100989,4,1,0,1 +1085,100982,4,1,0,1 +1085,100983,4,1,0,1 +1085,100787,4,1,0,1 +1085,101293,4,1,0,1 +1085,101294,4,1,0,1 +1085,101295,4,1,0,1 +1085,101296,4,1,0,1 +1085,101297,4,1,0,1 +1085,101320,4,1,0,1 +1085,101567,4,1,0,1 +1085,101592,4,1,0,1 +1085,101593,4,1,0,1 +1085,101594,4,1,0,1 +1085,101595,4,1,0,1 +1089,100989,4,1,0,0 +1089,101004,3,1,0,0 +1089,101005,3,1,0,0 +1104,101293,4,1,0,0 +1104,101294,4,1,0,0 +1104,101298,3,1,0,0 +1111,100008,4,1,0,1 +1111,100017,4,1,0,1 +1111,100026,4,1,0,1 +1111,100034,4,1,0,1 +1111,100041,4,1,0,1 +1111,100048,4,1,0,1 +1111,100054,4,1,0,1 +1111,100060,4,1,0,1 +1111,100066,4,1,0,1 +1111,100078,4,1,0,1 +1111,100072,4,1,0,1 +1111,100084,4,1,0,1 +1111,100090,4,1,0,1 +1111,100282,4,1,0,1 +1111,100285,4,1,0,1 +1111,100284,4,1,0,1 +1111,100286,4,1,0,1 +1111,100280,4,1,0,1 +1111,100276,4,1,0,1 +1111,100277,4,1,0,1 +1111,100275,4,1,0,1 +1111,100278,4,1,0,1 +1111,100431,4,1,0,1 +1111,100407,4,1,0,1 +1111,100432,4,1,0,1 +1111,100433,4,1,0,1 +1111,100434,4,1,1,1 +1111,100435,4,1,1,1 +1111,100436,4,1,0,1 +1111,100437,4,1,0,1 +1111,100438,4,1,0,1 +1111,100439,4,1,0,1 +1111,100760,4,1,1,1 +1111,100761,4,1,0,1 +1111,100779,4,1,0,1 +1111,100767,4,1,0,1 +1111,100780,4,1,1,1 +1111,100784,4,1,1,1 +1111,100768,4,1,0,1 +1111,100725,4,1,1,1 +1111,100726,4,1,1,1 +1111,100985,4,1,1,1 +1111,100988,4,1,1,1 +1111,100989,4,1,1,1 +1111,100982,4,1,1,1 +1111,100983,4,1,1,1 +1111,101293,4,1,1,1 +1111,101294,4,1,1,1 +1111,101295,4,1,1,1 +1111,101320,4,1,1,1 +1135,101567,4,1,0,0 +1135,101592,4,1,0,0 +1135,101594,4,1,0,0 +1135,101595,4,1,0,0 +1135,101566,3,1,0,0 +1135,101602,3,1,0,0 +1135,101603,3,1,0,0 +1135,101619,2,1,0,0 diff --git a/titles/cm/cm_data/MU3/static_gachas.csv b/titles/cm/cm_data/MU3/static_gachas.csv new file mode 100644 index 0000000..2061574 --- /dev/null +++ b/titles/cm/cm_data/MU3/static_gachas.csv @@ -0,0 +1,104 @@ +"version","gachaId","gachaName","type","kind","isCeiling","maxSelectPoint","ceilingCnt","changeRateCnt1","changeRateCnt2" +6,1011,"無料ガチャ",0,3,0,0,10,0,0 +6,1012,"無料ガチャ(SR確定)",0,3,0,0,10,0,0 +6,1043,"レギュラーガチャ",0,0,0,0,10,0,0 +6,1067,"例えるなら大人のパッションフルーツ +リゾートプールガチャ",0,1,0,0,10,0,0 +6,1068,"柏木 咲姫 +ピックアップガチャ",0,2,0,0,10,0,0 +6,1069,"井之原 小星 +ピックアップガチャ",0,2,0,0,10,0,0 +6,1070,"目指すは優勝! +炎の体育祭リミテッドガチャ",0,1,1,110,10,0,0 +6,1071,"星咲 あかり +ピックアップガチャ",0,2,0,0,10,0,0 +6,1072,"藤沢 柚子 +ピックアップガチャ",0,2,0,0,10,0,0 +6,1073,"三角 葵 +ピックアップガチャ",0,2,0,0,10,0,0 +6,1074,"おくれてきた +Halloweenガチャ",0,1,0,0,10,0,0 +6,1075,"早乙女 彩華 +ピックアップガチャ",0,2,0,0,10,0,0 +6,1076,"桜井 春菜 +ピックアップガチャ",0,2,0,0,10,0,0 +6,1077,"ふわふわすぺーす +お仕事体験リミテッドガチャ",0,1,1,110,10,0,0 +6,1078,"高瀬 梨緒 +ピックアップガチャ",0,2,0,0,10,0,0 +6,1079,"結城 莉玖 +ピックアップガチャ",0,2,0,0,10,0,0 +6,1080,"藍原 椿 +ピックアップガチャ",0,2,0,0,10,0,0 +6,1081,"今夜はおうちでパーティ☆ +メリクリガチャ",0,1,0,0,10,0,0 +6,1082,"日向 千夏 +ピックアップガチャ",0,2,0,0,10,0,0 +6,1083,"柏木 美亜 +ピックアップガチャ",0,2,0,0,10,0,0 +6,1084,"東雲 つむぎ +ピックアップガチャ",0,2,0,0,10,0,0 +6,1085,"謹賀新年 +福袋ガチャ",0,0,1,33,10,0,0 +6,1086,"逢坂 茜 +ピックアップガチャ",0,2,0,0,10,0,0 +6,1087,"珠洲島 有栖 +ピックアップガチャ",0,2,0,0,10,0,0 +6,1088,"九條 楓 +ピックアップガチャ",0,2,0,0,10,0,0 +6,1089,"冬の魔法 +スーパーウルトラウィンターガチャ",0,1,0,0,10,0,0 +6,1093,"高瀬 梨緒ピックアップガチャ",0,2,0,0,10,0,0 +6,1094,"結城 莉玖ピックアップガチャ",0,2,0,0,10,0,0 +6,1095,"藍原 椿ピックアップガチャ",0,2,0,0,10,0,0 +6,1096,"早乙女 彩華ピックアップガチャ",0,2,0,0,10,0,0 +6,1097,"桜井 春菜ピックアップガチャ",0,2,0,0,10,0,0 +6,1098,"逢坂 茜ピックアップガチャ",0,2,0,0,10,0,0 +6,1099,"九條 楓ピックアップガチャ",0,2,0,0,10,0,0 +6,1100,"珠洲島 有栖ピックアップガチャ",0,2,0,0,10,0,0 +6,1101,"LEAF属性オンリーガチャ",0,2,0,0,10,0,0 +6,1102,"AQUA属性オンリーガチャ",0,2,0,0,10,0,0 +6,1103,"FIRE属性オンリーガチャ",0,2,0,0,10,0,0 +6,1104,"夜明け前の双星ガチャ",0,1,0,0,10,0,0 +6,1105,"謎の洞窟 黄金は実在した!!ガチャ",0,1,0,0,10,0,0 +6,1106,"スウィートブライダルリミテッドガチャ",0,1,0,0,10,0,0 +6,1107,"忘れられない、愛(ピュア)とロックがここにある。ガチャ",0,1,0,0,10,0,0 +6,1108,"メルティ夜ふかしガチャ",0,1,0,0,10,0,0 +6,1109,"絵本の国のシューターズガチャ",0,1,0,0,10,0,0 +6,1110,"オンゲキ R.E.D. PLUS 大感謝祭ガチャ",0,1,0,0,10,0,0 +6,1111,"オンゲキ 3rd Anniversaryガチャ",0,1,1,33,10,0,0 +6,1113,"柏木 咲姫ピックアップガチャ",0,2,0,0,10,0,0 +6,1114,"井之原 小星ピックアップガチャ",0,2,0,0,10,0,0 +6,1115,"星咲 あかりピックアップガチャ",0,2,0,0,10,0,0 +6,1116,"藤沢 柚子ピックアップガチャ",0,2,0,0,10,0,0 +6,1117,"三角 葵ピックアップガチャ",0,2,0,0,10,0,0 +6,1118,"日向 千夏ピックアップガチャ",0,2,0,0,10,0,0 +6,1119,"柏木 美亜ピックアップガチャ",0,2,0,0,10,0,0 +6,1120,"東雲 つむぎピックアップガチャ",0,2,0,0,10,0,0 +6,1121,"LEAF属性オンリーガチャ",0,2,0,0,10,0,0 +6,1122,"FIRE属性オンリーガチャ",0,2,0,0,10,0,0 +6,1123,"AQUA属性オンリーガチャ",0,2,0,0,10,0,0 +6,1125,"Let`s SHOOT!ガチャ",0,1,0,0,10,0,0 +6,1126,"ぽかぽか""温""ゲキ!いい湯だな リミテッドガチャ",0,1,0,0,10,0,0 +6,1127,"聖夜に煌めく イルミネーションガチャ",0,1,0,0,10,0,0 +6,1128,"bitter chocolate kiss ガチャ",0,1,0,0,10,0,0 +6,1134,"謹賀新年福袋ガチャ",0,1,0,0,10,0,0 +6,1135,"オンゲキ bright 大感謝祭ガチャ",0,1,0,0,10,0,0 +7,1140,"カラフルアンブレラガチャ",0,0,0,0,10,0,0 +7,1141,"It's Showtime!ワンダフルサーカスガチャ",0,0,0,0,10,0,0 +7,1147,"R.B.P. ピックアップガチャ",0,0,0,0,10,0,0 +7,1148,"皇城 セツナ ピックアップガチャ",0,0,0,0,10,0,0 +7,1149,"ASTERISM ピックアップガチャ",0,0,0,0,10,0,0 +7,1153,"Memories of O.N.G.E.K.I.打ち上げガチャ",0,0,0,0,10,0,0 +7,1156,"bright memory振り返りガチャ",0,0,0,0,10,0,0 +7,1158,"レギュラーガチャ",0,0,0,100,0,0,0 +7,1159,"オンゲキ&オンゲキ PLUS ピックアップガチャ",0,2,0,100,0,0,0 +7,1160,"SUMMER & SUMMER PLUS ピックアップガチャ",0,2,0,100,0,0,0 +7,1161,"R.E.D. & R.E.D. PLUS ピックアップガチャ",0,2,0,100,0,0,0 +7,1162,"bright & bright MEMORY ピックアップガチャ",0,2,0,100,0,0,0 +7,1163,"4周年記念!! 4rd Anniversaryセレクトガチャ",0,1,0,100,0,0,0 +7,1164,"2023謹賀新年福袋ガチャ",0,1,0,100,0,0,0 +7,1165,"5周年記念!! 5rd Anniversaryセレクトガチャ",0,1,0,100,0,0,0 +7,1166,"2024謹賀新年福袋ガチャ",0,1,0,100,0,0,0 +7,1167,"6周年記念!! 6rd Anniversaryセレクトガチャ",0,1,0,100,0,0,0 +7,1168,"2025謹賀新年福袋ガチャ",0,1,0,100,0,0,0 diff --git a/titles/cm/config.py b/titles/cm/config.py new file mode 100644 index 0000000..df9f65a --- /dev/null +++ b/titles/cm/config.py @@ -0,0 +1,19 @@ +from core.config import CoreConfig + + +class CardMakerServerConfig(): + def __init__(self, parent_config: "CardMakerConfig") -> None: + self.__config = parent_config + + @property + def enable(self) -> bool: + return CoreConfig.get_config_field(self.__config, 'cardmaker', 'server', 'enable', default=True) + + @property + def loglevel(self) -> int: + return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'cardmaker', 'server', 'loglevel', default="info")) + + +class CardMakerConfig(dict): + def __init__(self) -> None: + self.server = CardMakerServerConfig(self) diff --git a/titles/cm/const.py b/titles/cm/const.py new file mode 100644 index 0000000..f59af5d --- /dev/null +++ b/titles/cm/const.py @@ -0,0 +1,10 @@ +class CardMakerConstants(): + GAME_CODE = "SDED" + + VER_CARD_MAKER = 0 + + VERSION_NAMES = ["Card Maker 1.34"] + + @classmethod + def game_ver_to_string(cls, ver: int): + return cls.VERSION_NAMES[ver] diff --git a/titles/cm/index.py b/titles/cm/index.py new file mode 100644 index 0000000..6ba7382 --- /dev/null +++ b/titles/cm/index.py @@ -0,0 +1,91 @@ +from twisted.web.http import Request +import json +import inflection +import yaml +import string +import logging +import coloredlogs +import zlib +from logging.handlers import TimedRotatingFileHandler + +from core.config import CoreConfig +from titles.cm.config import CardMakerConfig +from titles.cm.const import CardMakerConstants +from titles.cm.base import CardMakerBase + + +class CardMakerServlet(): + def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: + self.core_cfg = core_cfg + self.game_cfg = CardMakerConfig() + self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/cardmaker.yaml"))) + + self.versions = [ + CardMakerBase(core_cfg, self.game_cfg) + ] + + self.logger = logging.getLogger("cardmaker") + log_fmt_str = "[%(asctime)s] Card Maker | %(levelname)s | %(message)s" + log_fmt = logging.Formatter(log_fmt_str) + fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.core_cfg.server.log_dir, "cardmaker"), encoding='utf8', + when="d", backupCount=10) + + fileHandler.setFormatter(log_fmt) + + consoleHandler = logging.StreamHandler() + consoleHandler.setFormatter(log_fmt) + + self.logger.addHandler(fileHandler) + self.logger.addHandler(consoleHandler) + + self.logger.setLevel(self.game_cfg.server.loglevel) + coloredlogs.install(level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str) + + def render_POST(self, request: Request, version: int, url_path: str) -> bytes: + req_raw = request.content.getvalue() + url_split = url_path.split("/") + internal_ver = 0 + endpoint = url_split[len(url_split) - 1] + + print(f"version: {version}") + + if version >= 130 and version < 135: # Card Maker + internal_ver = CardMakerConstants.VER_CARD_MAKER + + if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32: + # If we get a 32 character long hex string, it's a hash and we're + # doing encrypted. The likelyhood of false positives is low but + # technically not 0 + self.logger.error("Encryption not supported at this time") + + try: + unzip = zlib.decompress(req_raw) + + except zlib.error as e: + self.logger.error(f"Failed to decompress v{version} {endpoint} request -> {e}") + return zlib.compress("{\"stat\": \"0\"}".encode("utf-8")) + + req_data = json.loads(unzip) + + self.logger.info(f"v{version} {endpoint} request - {req_data}") + + func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" + + try: + handler = getattr(self.versions[internal_ver], func_to_find) + resp = handler(req_data) + + except AttributeError as e: + self.logger.warning(f"Unhandled v{version} request {endpoint} - {e}") + return zlib.compress("{\"stat\": \"0\"}".encode("utf-8")) + + except Exception as e: + self.logger.error(f"Error handling v{version} method {endpoint} - {e}") + return zlib.compress("{\"stat\": \"0\"}".encode("utf-8")) + + if resp is None: + resp = {'returnCode': 1} + + self.logger.info(f"Response {resp}") + + return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) diff --git a/titles/cm/read.py b/titles/cm/read.py new file mode 100644 index 0000000..b9cb674 --- /dev/null +++ b/titles/cm/read.py @@ -0,0 +1,199 @@ +from decimal import Decimal +import logging +import os +import re +import csv +import xml.etree.ElementTree as ET +from typing import Any, Dict, List, Optional + +from read import BaseReader +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 + + +class CardMakerReader(BaseReader): + def __init__(self, config: CoreConfig, version: int, bin_dir: Optional[str], + opt_dir: Optional[str], extra: Optional[str]) -> None: + super().__init__(config, version, bin_dir, opt_dir, extra) + self.ongeki_data = OngekiData(config) + + try: + self.logger.info( + f"Start importer for {CardMakerConstants.game_ver_to_string(version)}") + except IndexError: + self.logger.error(f"Invalid ongeki version {version}") + exit(1) + + def read(self) -> None: + static_datas = { + "static_cards.csv": "read_ongeki_card_csv", + "static_gachas.csv": "read_ongeki_gacha_csv", + "static_gacha_cards.csv": "read_ongeki_gacha_card_csv" + } + + if self.bin_dir is not None: + for file, func in static_datas.items(): + if os.path.exists(f"{self.bin_dir}/MU3/{file}"): + read_csv = getattr(CardMakerReader, func) + read_csv(self, f"{self.bin_dir}/MU3/{file}") + else: + self.logger.warn(f"Couldn't find {file} file in {self.bin_dir}, skipping") + + if self.opt_dir is not None: + dir = self.get_data_directories(self.opt_dir) + + # ONGEKI (MU3) cnnot easily access the bin data(A000.pac) + # so only opt_dir will work for now + self.read_gacha(f"{dir}/MU3/gacha") + self.read_card(f"{dir}/MU3/card") + + def read_ongeki_card_csv(self, file_path: str) -> None: + self.logger.info(f"Reading cards from {file_path}...") + + with open(file_path, encoding="utf-8") as f: + reader = csv.DictReader(f) + for row in reader: + self.ongeki_data.static.put_card( + row["version"], + row["cardId"], + name=row["name"], + charaId=row["charaId"], + nickName=row["nickName"] if row["nickName"] != "" else None, + school=row["school"], + attribute=row["attribute"], + gakunen=row["gakunen"], + rarity=row["rarity"], + levelParam=row["levelParam"], + skillId=row["skillId"], + choKaikaSkillId=row["choKaikaSkillId"], + cardNumber=row["cardNumber"] if row["cardNumber"] != "" else None + ) + + self.logger.info(f"Added card {row['cardId']}") + + 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: + self.ongeki_data.static.put_gacha( + row["version"], + row["gachaId"], + row["gachaName"], + row["kind"], + type=row["type"], + isCeiling=True if row["isCeiling"] == "1" else False, + maxSelectPoint=row["maxSelectPoint"], + ceilingCnt=row["ceilingCnt"], + changeRateCnt1=row["changeRateCnt1"], + changeRateCnt2=row["changeRateCnt2"] + ) + + self.logger.info(f"Added gacha {row['gachaId']}") + + 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: + self.ongeki_data.static.put_gacha_card( + row["gachaId"], + row["cardId"], + rarity=row["rarity"], + weight=row["weight"], + isPickup=True if row["isPickup"] == "1" else False, + isSelect=True if row["isSelect"] == "1" else False + ) + + self.logger.info(f"Added card {row['cardId']} to gacha") + + def read_ongeki_card(self, base_dir: str) -> None: + self.logger.info(f"Reading cards from {base_dir}...") + + version_ids = { + '1000': OngekiConstants.VER_ONGEKI, + '1005': OngekiConstants.VER_ONGEKI_PLUS, + '1010': OngekiConstants.VER_ONGEKI_SUMMER, + '1015': OngekiConstants.VER_ONGEKI_SUMMER_PLUS, + '1020': OngekiConstants.VER_ONGEKI_RED, + '1025': OngekiConstants.VER_ONGEKI_RED_PLUS, + '1030': OngekiConstants.VER_ONGEKI_BRIGHT, + '1035': OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY + } + + for root, dirs, files in os.walk(base_dir): + for dir in dirs: + if os.path.exists(f"{root}/{dir}/Card.xml"): + with open(f"{root}/{dir}/Card.xml", "r", encoding="utf-8") as f: + troot = ET.fromstring(f.read()) + + card_id = int(troot.find('Name').find('id').text) + name = troot.find('Name').find('str').text + chara_id = int(troot.find('CharaID').find('id').text) + nick_name = troot.find('NickName').text + school = troot.find('School').find('str').text + attribute = troot.find('Attribute').text + gakunen = troot.find('Gakunen').find('str').text + rarity = OngekiConstants.RARITY_TYPES[ + troot.find('Rarity').text].value + + level_param = [] + for lvl in troot.find('LevelParam').findall('int'): + level_param.append(lvl.text) + + skill_id = int(troot.find('SkillID').find('id').text) + cho_kai_ka_skill_id = int(troot.find('ChoKaikaSkillID').find('id').text) + + version = version_ids[ + troot.find('VersionID').find('id').text] + card_number = troot.find('CardNumberString').text + + self.ongeki_data.static.put_card( + version, + card_id, + name=name, + charaId=chara_id, + nickName=nick_name, + school=school, + attribute=attribute, + gakunen=gakunen, + rarity=rarity, + levelParam=','.join(level_param), + skillId=skill_id, + choKaikaSkillId=cho_kai_ka_skill_id, + cardNumber=card_number + ) + self.logger.info(f"Added card {card_id}") + + def read_ongeki_gacha(self, base_dir: str) -> None: + self.logger.info(f"Reading gachas from {base_dir}...") + + # assuming some GachaKinds based on the GachaType + type_to_kind = { + "Normal": "Normal", + "Pickup": "Pickup", + "RecoverFiveShotFlag": "BonusRestored", + "Free": "Free", + "FreeSR": "Free" + } + + for root, dirs, files in os.walk(base_dir): + for dir in dirs: + if os.path.exists(f"{root}/{dir}/Gacha.xml"): + with open(f"{root}/{dir}/Gacha.xml", "r", encoding="utf-8") as f: + troot = ET.fromstring(f.read()) + + name = troot.find('Name').find('str').text + id = int(troot.find('Name').find('id').text) + + gacha_kind = OngekiConstants.CM_GACHA_KINDS[ + type_to_kind[troot.find('Type').text]].value + + self.ongeki_data.static.put_gacha( + self.version, id, name, gacha_kind) + self.logger.info(f"Added gacha {id}") diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 4b0b81c..3817890 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -16,6 +16,9 @@ class Mai2Base(): self.data = Mai2Data(cfg) self.logger = logging.getLogger("mai2") + def handle_ping_request(self, data: Dict) -> Dict: + return {"returnCode": 1} + def handle_get_game_setting_api_request(self, data: Dict): reboot_start = date.strftime(datetime.now() + timedelta(hours=3), Mai2Constants.DATE_TIME_FORMAT) reboot_end = date.strftime(datetime.now() + timedelta(hours=4), Mai2Constants.DATE_TIME_FORMAT) @@ -202,9 +205,9 @@ class Mai2Base(): for fav in upsert["userFavoriteList"]: self.data.item.put_favorite(user_id, fav["kind"], fav["itemIdList"]) - if "isNewFriendSeasonRankingList" in upsert and int(upsert["isNewFriendSeasonRankingList"]) > 0: - for fsr in upsert["userFriendSeasonRankingList"]: - pass + # if "isNewFriendSeasonRankingList" in upsert and int(upsert["isNewFriendSeasonRankingList"]) > 0: + # for fsr in upsert["userFriendSeasonRankingList"]: + # pass def handle_user_logout_api_request(self, data: Dict) -> Dict: pass diff --git a/titles/ongeki/bright.py b/titles/ongeki/bright.py index d92ed48..57660b5 100644 --- a/titles/ongeki/bright.py +++ b/titles/ongeki/bright.py @@ -1,5 +1,6 @@ from datetime import date, datetime, timedelta from typing import Any, Dict +from random import randint import pytz import json @@ -8,8 +9,8 @@ from titles.ongeki.base import OngekiBase from titles.ongeki.const import OngekiConstants from titles.ongeki.config import OngekiConfig -class OngekiBright(OngekiBase): +class OngekiBright(OngekiBase): def __init__(self, core_cfg: CoreConfig, game_cfg: OngekiConfig) -> None: super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_BRIGHT @@ -19,3 +20,609 @@ class OngekiBright(OngekiBase): ret["gameSetting"]["dataVersion"] = "1.30.00" ret["gameSetting"]["onlineDataVersion"] = "1.30.00" return ret + + def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: + # first check for a bright memory profile after that check for a + # bright profile + p = (self.data.profile.get_profile_data(data["userId"], self.version+1) + or self.data.profile.get_profile_data(data["userId"], self.version)) + if p is None: + return {} + + cards = self.data.card.get_user_cards(data["userId"]) + if cards is None or len(cards) == 0: + # This should never happen + self.logger.error( + f"handle_get_user_data_api_request: Internal error - No cards found for user id {data['userId']}") + return {} + + # get the dict representation of the row so we can modify values + user_data = p._asdict() + + # remove the values the game doesn't want + user_data.pop("id") + user_data.pop("user") + user_data.pop("version") + + # TODO: replace datetime objects with strings + + # add access code that we don't store + user_data["accessCode"] = cards[0]["access_code"] + + # hardcode Card Maker version for now + user_data["compatibleCmVersion"] = "1.30.01" + + return {"userId": data["userId"], "userData": user_data} + + def handle_printer_login_api_request(self, data: Dict): + return {"returnCode": 1} + + def handle_printer_logout_api_request(self, data: Dict): + return {"returnCode": 1} + + def handle_cm_get_user_card_api_request(self, data: Dict) -> Dict: + user_cards = self.data.item.get_cards(data["userId"]) + if user_cards is None: + return {} + + max_ct = data["maxCount"] + next_idx = data["nextIndex"] + start_idx = next_idx + end_idx = max_ct + start_idx + + if len(user_cards[start_idx:]) > max_ct: + next_idx += max_ct + else: + next_idx = -1 + + card_list = [] + for card in user_cards: + tmp = card._asdict() + tmp.pop("id") + tmp.pop("user") + card_list.append(tmp) + + return { + "userId": data["userId"], + "length": len(card_list[start_idx:end_idx]), + "nextIndex": next_idx, + "userCardList": card_list[start_idx:end_idx] + } + + def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: + user_characters = self.data.item.get_characters(data["userId"]) + if user_characters is None: + return {} + + max_ct = data["maxCount"] + next_idx = data["nextIndex"] + start_idx = next_idx + end_idx = max_ct + start_idx + + if len(user_characters[start_idx:]) > max_ct: + next_idx += max_ct + else: + next_idx = -1 + + character_list = [] + for character in user_characters: + tmp = character._asdict() + tmp.pop("id") + tmp.pop("user") + character_list.append(tmp) + + return { + "userId": data["userId"], + "length": len(character_list[start_idx:end_idx]), + "nextIndex": next_idx, + "userCharacterList": character_list[start_idx:end_idx] + } + + def handle_get_user_gacha_api_request(self, data: Dict) -> Dict: + user_gachas = self.data.item.get_user_gachas(data["userId"]) + if user_gachas is None: + return { + "userId": data["userId"], + "length": 0, + "userGachaList": [] + } + + user_gacha_list = [] + for gacha in user_gachas: + tmp = gacha._asdict() + tmp.pop("id") + tmp.pop("user") + tmp["dailyGachaDate"] = datetime.strftime( + tmp["dailyGachaDate"], "%Y-%m-%d") + user_gacha_list.append(tmp) + + return { + "userId": data["userId"], + "length": len(user_gacha_list), + "userGachaList": user_gacha_list + } + + def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: + return self.handle_get_user_item_api_request(data) + + def handle_cm_get_user_gacha_supply_api_request(self, data: Dict) -> Dict: + # not used for now? not sure what it even does + user_gacha_supplies = self.data.item.get_user_gacha_supplies( + data["userId"]) + if user_gacha_supplies is None: + return { + "supplyId": 1, + "length": 0, + "supplyCardList": [] + } + + supply_list = [gacha["cardId"] for gacha in user_gacha_supplies] + + return { + "supplyId": 1, + "length": len(supply_list), + "supplyCardList": supply_list + } + + def handle_get_game_gacha_api_request(self, data: Dict) -> Dict: + """ + returns all current active banners (gachas) + "Select Gacha" requires maxSelectPoint set and isCeiling set to 1 + """ + game_gachas = [] + # for every gacha_id in the OngekiConfig, grab the banner from the db + for gacha_id in self.game_cfg.gachas.enabled_gachas: + game_gacha = self.data.static.get_gacha(self.version, gacha_id) + if game_gacha: + game_gachas.append(game_gacha) + + # clean the database rows + game_gacha_list = [] + for gacha in game_gachas: + tmp = gacha._asdict() + tmp.pop("id") + tmp.pop("version") + tmp["startDate"] = datetime.strftime( + tmp["startDate"], "%Y-%m-%d %H:%M:%S") + tmp["endDate"] = datetime.strftime( + tmp["endDate"], "%Y-%m-%d %H:%M:%S") + tmp["noticeStartDate"] = datetime.strftime( + tmp["noticeStartDate"], "%Y-%m-%d %H:%M:%S") + tmp["noticeEndDate"] = datetime.strftime( + tmp["noticeEndDate"], "%Y-%m-%d %H:%M:%S") + tmp["convertEndDate"] = datetime.strftime( + tmp["convertEndDate"], "%Y-%m-%d %H:%M:%S") + + # make sure to only show gachas for the current version + # so only up to bright, 1140 is the first bright memory gacha + if tmp["gachaId"] < 1140: + game_gacha_list.append(tmp) + + return { + "length": len(game_gacha_list), + "gameGachaList": game_gacha_list, + # no clue + "registIdList": [] + } + + def handle_roll_gacha_api_request(self, data: Dict) -> Dict: + """ + Handle a gacha roll API request + """ + gacha_id = data["gachaId"] + num_rolls = data["times"] + # change_rate is the 5 gacha rool SR gurantee once a week + change_rate = data["changeRate"] + # SSR book which guarantees a SSR card, itemKind=15, itemId=1 + book_used = data["bookUseCount"] + if num_rolls not in {1, 5, 11}: + return {} + + # https://gamerch.com/ongeki/entry/462978 + + # 77% chance of gett ing a R card + # 20% chance of getting a SR card + # 3% chance of getting a SSR card + rarity = [1 for _ in range(77)] + rarity += [2 for _ in range(20)] + rarity += [3 for _ in range(3)] + + # gachaId 1011 is "無料ガチャ" (free gacha), which requires GatchaTickets + # itemKind=11, itemId=1 and appearenty sucks + # 94% chance of getting a R card + # 5% chance of getting a SR card + # 1% chance of getting a SSR card + if gacha_id == 1011: + rarity = [1 for _ in range(94)] + rarity += [2 for _ in range(5)] + rarity += [3 for _ in range(1)] + + # gachaId 1012 is "無料ガチャ(SR確定)" (SR confirmed! free gacha), which + # requires GatchaTickets itemKind=11, itemId=4 and always guarantees + # a SR card or higher + # 92% chance of getting a SR card + # 8% chance of getting a SSR card + elif gacha_id == 1012: + rarity = [2 for _ in range(92)] + rarity += [3 for _ in range(8)] + + assert len(rarity) == 100 + + # uniform distribution to get the rarity of the card + rolls = [rarity[randint(0, len(rarity)-1)] for _ in range(num_rolls)] + + # if SSR book used, make sure you always get one SSR + if book_used == 1: + if rolls.count(3) == 0: + # if there is no SSR, re-roll + return self.handle_roll_gacha_api_request(data) + # make sure that 11 rolls always have at least 1 SR or SSR + elif (num_rolls == 5 and change_rate is True) or num_rolls == 11: + if rolls.count(2) == 0 and rolls.count(3) == 0: + # if there is no SR or SSR, re-roll + return self.handle_roll_gacha_api_request(data) + + # get a list of cards for each rarity + cards_r = self.data.static.get_cards_by_rarity(self.version, 1) + cards_sr, cards_ssr = [], [] + + # free gachas are only allowed to get their specific cards! (R irrelevant) + if gacha_id in {1011, 1012}: + gacha_cards = self.data.static.get_gacha_cards(gacha_id) + for card in gacha_cards: + if card["rarity"] == 3: + cards_sr.append({ + "cardId": card["cardId"], + "rarity": 2 + }) + elif card["rarity"] == 4: + cards_ssr.append({ + "cardId": card["cardId"], + "rarity": 3 + }) + else: + cards_sr = self.data.static.get_cards_by_rarity(self.version, 2) + cards_ssr = self.data.static.get_cards_by_rarity(self.version, 3) + + # get the promoted cards for that gacha and add them multiple + # times to increase chances by factor chances + chances = 10 + + gacha_cards = self.data.static.get_gacha_cards(gacha_id) + for card in gacha_cards: + # make sure to add the cards to the corresponding rarity + if card["rarity"] == 2: + cards_r += [{ + "cardId": card["cardId"], + "rarity": 1 + }] * chances + if card["rarity"] == 3: + cards_sr += [{ + "cardId": card["cardId"], + "rarity": 2 + }] * chances + elif card["rarity"] == 4: + cards_ssr += [{ + "cardId": card["cardId"], + "rarity": 3 + }] * chances + + # get the card id for each roll + rolled_cards = [] + for i in range(len(rolls)): + if rolls[i] == 1: + rolled_cards.append(cards_r[randint(0, len(cards_r)-1)]) + elif rolls[i] == 2: + rolled_cards.append(cards_sr[randint(0, len(cards_sr)-1)]) + elif rolls[i] == 3: + rolled_cards.append(cards_ssr[randint(0, len(cards_ssr)-1)]) + + game_gacha_card_list = [] + for card in rolled_cards: + game_gacha_card_list.append({ + "gachaId": data["gachaId"], + "cardId": card["cardId"], + # +1 because Card Maker is weird + "rarity": card["rarity"] + 1, + "weight": 1, + "isPickup": False, + "isSelect": False + }) + + return { + "length": len(game_gacha_card_list), + "gameGachaCardList": game_gacha_card_list + } + + def handle_cm_upsert_user_gacha_api_request(self, data: Dict): + upsert = data["cmUpsertUserGacha"] + user_id = data["userId"] + + gacha_id = data["gachaId"] + gacha_count = data["gachaCnt"] + play_date = datetime.strptime(data["playDate"][:10], '%Y-%m-%d') + select_point = data["selectPoint"] + + total_gacha_count, ceiling_gacha_count = 0, 0 + daily_gacha_cnt, five_gacha_cnt, eleven_gacha_cnt = 0, 0, 0 + daily_gacha_date = datetime.strptime('2000-01-01', '%Y-%m-%d') + + # check if the user previously rolled the exact same gacha + user_gacha = self.data.item.get_user_gacha(user_id, gacha_id) + if user_gacha: + total_gacha_count = user_gacha["totalGachaCnt"] + ceiling_gacha_count = user_gacha["ceilingGachaCnt"] + daily_gacha_cnt = user_gacha["dailyGachaCnt"] + five_gacha_cnt = user_gacha["fiveGachaCnt"] + eleven_gacha_cnt = user_gacha["elevenGachaCnt"] + # parse just the year, month and date + daily_gacha_date = user_gacha["dailyGachaDate"] + + # if the saved dailyGachaDate is different from the roll, + # reset dailyGachaCnt and change the date + if daily_gacha_date != play_date: + daily_gacha_date = play_date + daily_gacha_cnt = 0 + + self.data.item.put_user_gacha( + user_id, + gacha_id, + totalGachaCnt=total_gacha_count + gacha_count, + ceilingGachaCnt=ceiling_gacha_count + gacha_count, + selectPoint=select_point, + useSelectPoint=0, + dailyGachaCnt=daily_gacha_cnt + gacha_count, + fiveGachaCnt=five_gacha_cnt+1 if gacha_count == 5 else five_gacha_cnt, + elevenGachaCnt=eleven_gacha_cnt+1 if gacha_count == 11 else eleven_gacha_cnt, + dailyGachaDate=daily_gacha_date + ) + + if "userData" in upsert and len(upsert["userData"]) > 0: + # check if the profile is a bright memory profile + p = self.data.profile.get_profile_data(data["userId"], self.version+1) + if p is not None: + # save the bright memory profile + self.data.profile.put_profile_data( + user_id, self.version+1, upsert["userData"][0]) + else: + # save the bright profile + self.data.profile.put_profile_data( + user_id, self.version, upsert["userData"][0]) + + if "userCharacterList" in upsert: + for x in upsert["userCharacterList"]: + self.data.item.put_character(user_id, x) + + if "userItemList" in upsert: + for x in upsert["userItemList"]: + self.data.item.put_item(user_id, x) + + if "userCardList" in upsert: + for x in upsert["userCardList"]: + self.data.item.put_card(user_id, x) + + # TODO? + # if "gameGachaCardList" in upsert: + # for x in upsert["gameGachaCardList"]: + + return {'returnCode': 1, 'apiName': 'CMUpsertUserGachaApi'} + + def handle_cm_upsert_user_select_gacha_api_request(self, data: Dict) -> Dict: + upsert = data["cmUpsertUserSelectGacha"] + user_id = data["userId"] + + if "userData" in upsert and len(upsert["userData"]) > 0: + # check if the profile is a bright memory profile + p = self.data.profile.get_profile_data(data["userId"], self.version+1) + if p is not None: + # save the bright memory profile + self.data.profile.put_profile_data( + user_id, self.version+1, upsert["userData"][0]) + else: + # save the bright profile + self.data.profile.put_profile_data( + user_id, self.version, upsert["userData"][0]) + + if "userCharacterList" in upsert: + for x in upsert["userCharacterList"]: + self.data.item.put_character(user_id, x) + + if "userCardList" in upsert: + for x in upsert["userCardList"]: + self.data.item.put_card(user_id, x) + + if "selectGachaLogList" in data: + for x in data["selectGachaLogList"]: + self.data.item.put_user_gacha( + user_id, + x["gachaId"], + selectPoint=0, + useSelectPoint=x["useSelectPoint"] + ) + + return {'returnCode': 1, 'apiName': 'cmUpsertUserSelectGacha'} + + def handle_get_game_gacha_card_by_id_api_request(self, data: Dict) -> Dict: + game_gacha_cards = self.data.static.get_gacha_cards(data["gachaId"]) + if game_gacha_cards == []: + # fallback to be at least able to select that gacha + return { + "gachaId": data["gachaId"], + "length": 6, + "isPickup": False, + "gameGachaCardList": [ + { + "gachaId": data["gachaId"], + "cardId": 100984, + "rarity": 4, + "weight": 1, + "isPickup": False, + "isSelect": True + }, + { + "gachaId": data["gachaId"], + "cardId": 100997, + "rarity": 3, + "weight": 2, + "isPickup": False, + "isSelect": True + }, + { + "gachaId": data["gachaId"], + "cardId": 100998, + "rarity": 3, + "weight": 2, + "isPickup": False, + "isSelect": True + }, + { + "gachaId": data["gachaId"], + "cardId": 101020, + "rarity": 2, + "weight": 3, + "isPickup": False, + "isSelect": True + }, + { + "gachaId": data["gachaId"], + "cardId": 101021, + "rarity": 2, + "weight": 3, + "isPickup": False, + "isSelect": True + }, + { + "gachaId": data["gachaId"], + "cardId": 101022, + "rarity": 2, + "weight": 3, + "isPickup": False, + "isSelect": True + } + ], + "emissionList": [], + "afterCalcList": [], + "ssrBookCalcList": [] + } + + game_gacha_card_list = [] + for gacha_card in game_gacha_cards: + tmp = gacha_card._asdict() + tmp.pop("id") + game_gacha_card_list.append(tmp) + + return { + "gachaId": data["gachaId"], + "length": len(game_gacha_card_list), + "isPickup": False, + "gameGachaCardList": game_gacha_card_list, + # again no clue + "emissionList": [], + "afterCalcList": [], + "ssrBookCalcList": [] + } + + def handle_get_game_theater_api_request(self, data: Dict) -> Dict: + """ + shows a banner after every print, not sure what its used for + """ + + """ + return { + "length": 1, + "gameTheaterList": [{ + "theaterId": 1, + "theaterName": "theaterName", + "startDate": "2018-01-01 00:00:00.0", + "endDate": "2038-01-01 00:00:00.0", + "gameSubTheaterList": [{ + "theaterId": 1, + "id": 2, + "no": 4 + }] + } + ], + "registIdList": [] + } + """ + + return { + "length": 0, + "gameTheaterList": [], + "registIdList": [] + } + + def handle_cm_upsert_user_print_playlog_api_request(self, data: Dict) -> Dict: + return { + "returnCode": 1, + "orderId": 0, + "serialId": "11111111111111111111", + "apiName": "CMUpsertUserPrintPlaylogApi" + } + + def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: + return { + "returnCode": 1, + "orderId": 0, + "serialId": "11111111111111111111", + "apiName": "CMUpsertUserPrintlogApi" + } + + def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: + user_print_detail = data["userPrintDetail"] + + # generate random serial id + serial_id = ''.join([str(randint(0, 9)) for _ in range(20)]) + + # not needed because are either zero or unset + user_print_detail.pop("orderId") + user_print_detail.pop("printNumber") + user_print_detail.pop("serialId") + user_print_detail["printDate"] = datetime.strptime( + user_print_detail["printDate"], '%Y-%m-%d' + ) + + # add the entry to the user print table with the random serialId + self.data.item.put_user_print_detail( + data["userId"], + serial_id, + user_print_detail + ) + + return { + "returnCode": 1, + "serialId": serial_id, + "apiName": "CMUpsertUserPrintApi" + } + + def handle_cm_upsert_user_all_api_request(self, data: Dict) -> Dict: + upsert = data["cmUpsertUserAll"] + user_id = data["userId"] + + if "userData" in upsert and len(upsert["userData"]) > 0: + # check if the profile is a bright memory profile + p = self.data.profile.get_profile_data(data["userId"], self.version+1) + if p is not None: + # save the bright memory profile + self.data.profile.put_profile_data( + user_id, self.version+1, upsert["userData"][0]) + else: + # save the bright profile + self.data.profile.put_profile_data( + user_id, self.version, upsert["userData"][0]) + + if "userActivityList" in upsert: + for act in upsert["userActivityList"]: + self.data.profile.put_profile_activity( + user_id, act["kind"], act["id"], act["sortNumber"], + act["param1"], act["param2"], act["param3"], act["param4"]) + + if "userItemList" in upsert: + for x in upsert["userItemList"]: + self.data.item.put_item(user_id, x) + + if "userCardList" in upsert: + for x in upsert["userCardList"]: + self.data.item.put_card(user_id, x) + + return {'returnCode': 1, 'apiName': 'cmUpsertUserAll'} diff --git a/titles/ongeki/config.py b/titles/ongeki/config.py index 722677c..3a89f29 100644 --- a/titles/ongeki/config.py +++ b/titles/ongeki/config.py @@ -1,17 +1,31 @@ +from typing import List + from core.config import CoreConfig + class OngekiServerConfig(): def __init__(self, parent_config: "OngekiConfig") -> None: self.__config = parent_config - + @property def enable(self) -> bool: return CoreConfig.get_config_field(self.__config, 'ongeki', 'server', 'enable', default=True) - + @property def loglevel(self) -> int: return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'ongeki', 'server', 'loglevel', default="info")) + +class OngekiGachaConfig(): + def __init__(self, parent_config: "OngekiConfig") -> None: + self.__config = parent_config + + @property + def enabled_gachas(self) -> List[int]: + return CoreConfig.get_config_field(self.__config, 'ongeki', 'gachas', 'enabled_gachas', default=[]) + + class OngekiConfig(dict): def __init__(self) -> None: self.server = OngekiServerConfig(self) + self.gachas = OngekiGachaConfig(self) diff --git a/titles/ongeki/const.py b/titles/ongeki/const.py index dd6c0f5..518459a 100644 --- a/titles/ongeki/const.py +++ b/titles/ongeki/const.py @@ -1,5 +1,7 @@ from typing import Final, Dict from enum import Enum + + class OngekiConstants(): GAME_CODE = "SDDT" @@ -35,6 +37,20 @@ class OngekiConstants(): 'SilverJewelEvent', ]) + class CM_GACHA_KINDS(Enum): + Normal = 0 + Pickup = 1 + BonusRestored = 2 + Free = 3 + PickupBonusRestored = 4 + + class RARITY_TYPES(Enum): + N = 0 + R = 1 + SR = 2 + SSR = 3 + SRPlus = 12 + # The game expects the server to give Lunatic an ID of 10, while the game uses 4 internally... except in Music.xml class DIFF_NAME(Enum): Basic = 0 @@ -48,4 +64,4 @@ class OngekiConstants(): @classmethod def game_ver_to_string(cls, ver: int): - return cls.VERSION_NAMES[ver] \ No newline at end of file + return cls.VERSION_NAMES[ver] diff --git a/titles/ongeki/schema/item.py b/titles/ongeki/schema/item.py index c3f8c7e..001388a 100644 --- a/titles/ongeki/schema/item.py +++ b/titles/ongeki/schema/item.py @@ -2,6 +2,7 @@ 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 @@ -242,6 +243,63 @@ tech_event = Table( mysql_charset='utf8mb4' ) +gacha = Table( + "ongeki_user_gacha", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("gachaId", Integer, nullable=False), + Column("totalGachaCnt", Integer, server_default="0"), + Column("ceilingGachaCnt", Integer, server_default="0"), + Column("selectPoint", Integer, server_default="0"), + Column("useSelectPoint", Integer, server_default="0"), + Column("dailyGachaCnt", Integer, server_default="0"), + Column("fiveGachaCnt", Integer, server_default="0"), + Column("elevenGachaCnt", Integer, server_default="0"), + Column("dailyGachaDate", TIMESTAMP, nullable=False, server_default=func.now()), + UniqueConstraint("user", "gachaId", name="ongeki_user_gacha_uk"), + mysql_charset='utf8mb4' +) + +gacha_supply = Table( + "ongeki_user_gacha_supply", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("cardId", Integer, nullable=False), + UniqueConstraint("user", "cardId", name="ongeki_user_gacha_supply_uk"), + mysql_charset='utf8mb4' +) + + +print_detail = Table( + "ongeki_user_print_detail", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("cardId", Integer, nullable=False), + Column("cardType", Integer, server_default="0"), + Column("printDate", TIMESTAMP, nullable=False), + Column("serialId", String(20), nullable=False), + Column("placeId", Integer, nullable=False), + Column("clientId", String(11), nullable=False), + Column("printerSerialId", String(20), nullable=False), + Column("isHolograph", Boolean, server_default="0"), + Column("isAutographed", Boolean, server_default="0"), + Column("printOption1", Boolean, server_default="1"), + Column("printOption2", Boolean, server_default="1"), + Column("printOption3", Boolean, server_default="1"), + Column("printOption4", Boolean, server_default="1"), + Column("printOption5", Boolean, server_default="1"), + Column("printOption6", Boolean, server_default="1"), + Column("printOption7", Boolean, server_default="1"), + Column("printOption8", Boolean, server_default="1"), + Column("printOption9", Boolean, server_default="1"), + Column("printOption10", Boolean, server_default="0"), + UniqueConstraint("serialId", name="ongeki_user_print_detail_uk"), + mysql_charset='utf8mb4' +) + class OngekiItemData(BaseData): def put_card(self, aime_id: int, card_data: Dict) -> Optional[int]: @@ -545,7 +603,7 @@ class OngekiItemData(BaseData): if result is None: return None return result.fetchall() - + def put_memorychapter(self, aime_id: int, memorychapter_data: Dict) -> Optional[int]: memorychapter_data["user"] = aime_id @@ -557,10 +615,73 @@ class OngekiItemData(BaseData): self.logger.warn(f"put_memorychapter: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - + def get_memorychapters(self, aime_id: int) -> Optional[List[Dict]]: sql = select(memorychapter).where(memorychapter.c.user == aime_id) result = self.execute(sql) - if result is None: return None - return result.fetchall() \ No newline at end of file + if result is None: + return None + return result.fetchall() + + def get_user_gacha(self, aime_id: int, gacha_id: int) -> Optional[Row]: + sql = gacha.select(and_( + gacha.c.user == aime_id, + gacha.c.gachaId == gacha_id + )) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_user_gachas(self, aime_id: int) -> Optional[List[Row]]: + sql = gacha.select(gacha.c.user == aime_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_user_gacha_supplies(self, aime_id: int) -> Optional[List[Row]]: + sql = gacha_supply.select(gacha_supply.c.user == aime_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def put_user_gacha(self, aime_id: int, gacha_id: int, **data) -> Optional[int]: + sql = insert(gacha).values( + user=aime_id, + gachaId=gacha_id, + **data) + + conflict = sql.on_duplicate_key_update( + user=aime_id, + gachaId=gacha_id, + **data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_user_gacha: Failed to insert! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_user_print_detail(self, aime_id: int, serial_id: str, + user_print_data: Dict) -> Optional[int]: + sql = insert(print_detail).values( + user=aime_id, + serialId=serial_id, + **user_print_data) + + conflict = sql.on_duplicate_key_update( + user=aime_id, + serialId=serial_id, + **user_print_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_user_print_detail: Failed to insert! aime_id: {aime_id}") + return None + return result.lastrowid diff --git a/titles/ongeki/schema/profile.py b/titles/ongeki/schema/profile.py index 26c79b5..d7514c6 100644 --- a/titles/ongeki/schema/profile.py +++ b/titles/ongeki/schema/profile.py @@ -78,7 +78,8 @@ profile = Table( Column("overDamageBattlePoint", Integer, server_default="0"), Column("bestBattlePoint", Integer, server_default="0"), Column("lastEmoneyBrand", Integer, server_default="0"), - Column("isDialogWatchedSuggestMemory", Boolean), + Column("lastEmoneyCredit", Integer, server_default="0"), + Column("isDialogWatchedSuggestMemory", Boolean, server_default="0"), UniqueConstraint("user", "version", name="ongeki_profile_profile_uk"), mysql_charset='utf8mb4' ) diff --git a/titles/ongeki/schema/static.py b/titles/ongeki/schema/static.py index e98ec58..2e1bc51 100644 --- a/titles/ongeki/schema/static.py +++ b/titles/ongeki/schema/static.py @@ -37,7 +37,210 @@ music = Table( mysql_charset='utf8mb4' ) +gachas = Table( + "ongeki_static_gachas", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column("gachaId", Integer, nullable=False), + Column("gachaName", String(255), nullable=False), + Column("type", Integer, nullable=False, server_default="0"), + Column("kind", Integer, nullable=False, server_default="0"), + Column("isCeiling", Boolean, server_default="0"), + Column("maxSelectPoint", Integer, server_default="0"), + Column("ceilingCnt", Integer, server_default="10"), + Column("changeRateCnt1", Integer, server_default="0"), + Column("changeRateCnt2", Integer, server_default="0"), + Column("startDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"), + Column("endDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"), + Column("noticeStartDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"), + Column("noticeEndDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"), + Column("convertEndDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"), + UniqueConstraint("version", "gachaId", "gachaName", name="ongeki_static_gachas_uk"), + mysql_charset='utf8mb4' +) + +gacha_cards = Table( + "ongeki_static_gacha_cards", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("gachaId", Integer, nullable=False), + Column("cardId", Integer, nullable=False), + Column("rarity", Integer, nullable=False), + Column("weight", Integer, server_default="1"), + Column("isPickup", Boolean, server_default="0"), + Column("isSelect", Boolean, server_default="0"), + UniqueConstraint("gachaId", "cardId", name="ongeki_static_gacha_cards_uk"), + mysql_charset='utf8mb4' +) + +cards = Table( + "ongeki_static_cards", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column("cardId", Integer, nullable=False), + Column("name", String(255), nullable=False), + Column("charaId", Integer, nullable=False), + Column("nickName", String(255)), + Column("school", String(255), nullable=False), + Column("attribute", String(5), nullable=False), + Column("gakunen", String(255), nullable=False), + Column("rarity", Integer, nullable=False), + Column("levelParam", String(255), nullable=False), + Column("skillId", Integer, nullable=False), + Column("choKaikaSkillId", Integer, nullable=False), + Column("cardNumber", String(255)), + UniqueConstraint("version", "cardId", name="ongeki_static_cards_uk"), + mysql_charset='utf8mb4' +) + + class OngekiStaticData(BaseData): + def put_card(self, version: int, card_id: int, **card_data) -> Optional[int]: + sql = insert(cards).values( + version=version, + cardId=card_id, + **card_data) + + conflict = sql.on_duplicate_key_update(**card_data) + + result = self.execute(conflict) + if result is None: + self.logger.warn(f"Failed to insert card! card_id {card_id}") + return None + return result.lastrowid + + def get_card(self, version: int, card_id: int) -> Optional[Dict]: + sql = cards.select(and_( + cards.c.version <= version, + cards.c.cardId == card_id + )) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_card_by_card_number(self, version: int, card_number: str) -> Optional[Dict]: + if not card_number.startswith("[O.N.G.E.K.I.]"): + card_number = f"[O.N.G.E.K.I.]{card_number}" + + sql = cards.select(and_( + cards.c.version <= version, + cards.c.cardNumber == card_number + )) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_card_by_name(self, version: int, name: str) -> Optional[Dict]: + sql = cards.select(and_( + cards.c.version <= version, + cards.c.name == name + )) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_cards(self, version: int) -> Optional[List[Dict]]: + sql = cards.select(cards.c.version <= version) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_cards_by_rarity(self, version: int, rarity: int) -> Optional[List[Dict]]: + sql = cards.select(and_( + cards.c.version <= version, + cards.c.rarity == rarity + )) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def put_gacha(self, version: int, gacha_id: int, gacha_name: int, + gacha_kind: int, **gacha_data) -> Optional[int]: + sql = insert(gachas).values( + version=version, + gachaId=gacha_id, + gachaName=gacha_name, + kind=gacha_kind, + **gacha_data + ) + + conflict = sql.on_duplicate_key_update( + version=version, + gachaId=gacha_id, + gachaName=gacha_name, + kind=gacha_kind, + **gacha_data + ) + + result = self.execute(conflict) + if result is None: + self.logger.warn(f"Failed to insert gacha! gacha_id {gacha_id}") + return None + return result.lastrowid + + def get_gacha(self, version: int, gacha_id: int) -> Optional[Dict]: + sql = gachas.select(and_( + gachas.c.version == version, + gachas.c.gachaId == gacha_id + )) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_gachas(self, version: int) -> Optional[List[Dict]]: + sql = gachas.select( + gachas.c.version == version).order_by( + gachas.c.gachaId.asc() + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def put_gacha_card(self, gacha_id: int, card_id: int, **gacha_card) -> Optional[int]: + sql = insert(gacha_cards).values( + gachaId=gacha_id, + cardId=card_id, + **gacha_card + ) + + conflict = sql.on_duplicate_key_update( + gachaId=gacha_id, + cardId=card_id, + **gacha_card + ) + + result = self.execute(conflict) + if result is None: + self.logger.warn(f"Failed to insert gacha card! gacha_id {gacha_id}") + return None + return result.lastrowid + + def get_gacha_cards(self, gacha_id: int) -> Optional[List[Dict]]: + sql = gacha_cards.select( + gacha_cards.c.gachaId == gacha_id + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + def put_event(self, version: int, event_id: int, event_type: int, event_name: str) -> Optional[int]: sql = insert(events).values( version = version, From 4f3d3d8395b5ca416792e2f12f273b171398d48e Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 3 Mar 2023 19:23:14 -0500 Subject: [PATCH 02/63] database: fix error when trying to upgrade the schema for a game that wasn't created yet --- core/data/schema/base.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/data/schema/base.py b/core/data/schema/base.py index 78f3ab4..fd49ac1 100644 --- a/core/data/schema/base.py +++ b/core/data/schema/base.py @@ -85,7 +85,12 @@ class BaseData(): result = self.execute(sql) if result is None: return None - return result.fetchone()["version"] + + row = result.fetchone() + if row is None: + return None + + return row["version"] def set_schema_ver(self, ver: int, game: str = "CORE") -> Optional[int]: sql = insert(schema_ver).values(game = game, version = ver) From dc5e5c14407a400f7d85bac7c2a6a9c0c4731890 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 3 Mar 2023 19:56:12 -0500 Subject: [PATCH 03/63] database: fix event logging table --- core/data/schema/base.py | 7 ++++--- core/data/schema/versions/CORE_2_rollback.sql | 1 + core/data/schema/versions/CORE_3_upgrade.sql | 1 + dbutils.py | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 core/data/schema/versions/CORE_2_rollback.sql create mode 100644 core/data/schema/versions/CORE_3_upgrade.sql diff --git a/core/data/schema/base.py b/core/data/schema/base.py index fd49ac1..955c772 100644 --- a/core/data/schema/base.py +++ b/core/data/schema/base.py @@ -29,6 +29,7 @@ event_log = Table( Column("system", String(255), nullable=False), Column("type", String(255), nullable=False), Column("severity", Integer, nullable=False), + Column("message", String(1000), nullable=False), Column("details", JSON, nullable=False), Column("when_logged", TIMESTAMP, nullable=False, server_default=func.now()), mysql_charset='utf8mb4' @@ -102,12 +103,12 @@ class BaseData(): return None return result.lastrowid - def log_event(self, system: str, type: str, severity: int, details: Dict) -> Optional[int]: - sql = event_log.insert().values(system = system, type = type, severity = severity, details = json.dumps(details)) + def log_event(self, system: str, type: str, severity: int, message: str, details: Dict = {}) -> Optional[int]: + sql = event_log.insert().values(system = system, type = type, severity = severity, message = message, details = json.dumps(details)) result = self.execute(sql) if result is None: - self.logger.error(f"{__name__}: Failed to insert event into event log! system = {system}, type = {type}, severity = {severity}, details = {details}") + self.logger.error(f"{__name__}: Failed to insert event into event log! system = {system}, type = {type}, severity = {severity}, message = {message}") return None return result.lastrowid diff --git a/core/data/schema/versions/CORE_2_rollback.sql b/core/data/schema/versions/CORE_2_rollback.sql new file mode 100644 index 0000000..8944df0 --- /dev/null +++ b/core/data/schema/versions/CORE_2_rollback.sql @@ -0,0 +1 @@ +ALTER TABLE `event_log` DROP COLUMN `message`; \ No newline at end of file diff --git a/core/data/schema/versions/CORE_3_upgrade.sql b/core/data/schema/versions/CORE_3_upgrade.sql new file mode 100644 index 0000000..cc0e8c6 --- /dev/null +++ b/core/data/schema/versions/CORE_3_upgrade.sql @@ -0,0 +1 @@ +ALTER TABLE `event_log` ADD COLUMN `message` VARCHAR(1000) NOT NULL AFTER `severity`; \ No newline at end of file diff --git a/dbutils.py b/dbutils.py index 4500a13..6285e30 100644 --- a/dbutils.py +++ b/dbutils.py @@ -28,7 +28,7 @@ if __name__=='__main__': if args.game is None: data.logger.info("No game set, upgrading core schema") - data.migrate_database("CORE", int(args.version)) + data.migrate_database("CORE", int(args.version), args.action) else: data.migrate_database(args.game, int(args.version), args.action) From 279f48dc0cc78619f38a3dbe684ef472c0f00753 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 3 Mar 2023 21:31:23 -0500 Subject: [PATCH 04/63] frontend: fix login, remove frontend_session in favor of twisted sessions --- core/data/database.py | 2 +- core/data/schema/user.py | 62 ++++--------------- core/data/schema/versions/CORE_3_rollback.sql | 12 ++++ core/data/schema/versions/CORE_4_upgrade.sql | 1 + core/frontend.py | 62 +++++++++++++------ core/frontend/gate/gate.jinja | 15 ++++- core/frontend/widgets/topbar.jinja | 5 ++ 7 files changed, 88 insertions(+), 71 deletions(-) create mode 100644 core/data/schema/versions/CORE_3_rollback.sql create mode 100644 core/data/schema/versions/CORE_4_upgrade.sql diff --git a/core/data/database.py b/core/data/database.py index 65b01aa..4ba3b3a 100644 --- a/core/data/database.py +++ b/core/data/database.py @@ -31,7 +31,7 @@ class Data: self.arcade = ArcadeData(self.config, self.session) self.card = CardData(self.config, self.session) self.base = BaseData(self.config, self.session) - self.schema_ver_latest = 2 + self.schema_ver_latest = 4 log_fmt_str = "[%(asctime)s] %(levelname)s | Database | %(message)s" log_fmt = logging.Formatter(log_fmt_str) diff --git a/core/data/schema/user.py b/core/data/schema/user.py index 9e79891..b7f72e2 100644 --- a/core/data/schema/user.py +++ b/core/data/schema/user.py @@ -9,6 +9,7 @@ from sqlalchemy.sql import func, select, Delete from uuid import uuid4 from datetime import datetime, timedelta from sqlalchemy.engine import Row +import bcrypt from core.data.schema.base import BaseData, metadata @@ -26,17 +27,6 @@ aime_user = Table( mysql_charset='utf8mb4' ) -frontend_session = Table( - "frontend_session", - metadata, - Column("id", Integer, primary_key=True, unique=True), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column("ip", String(15)), - Column('session_cookie', String(32), nullable=False, unique=True), - Column("expires", TIMESTAMP, nullable=False), - mysql_charset='utf8mb4' -) - class PermissionBits(Enum): PermUser = 1 PermMod = 2 @@ -74,50 +64,20 @@ class UserData(BaseData): if result is None: return None return result.lastrowid - def login(self, user_id: int, passwd: bytes = None, ip: str = "0.0.0.0") -> Optional[str]: - sql = select(aime_user).where(and_(aime_user.c.id == user_id, aime_user.c.password == passwd)) - - result = self.execute(sql) - if result is None: return None - - usr = result.fetchone() - if usr is None: return None - - return self.create_session(user_id, ip) - - def check_session(self, cookie: str, ip: str = "0.0.0.0") -> Optional[Row]: - sql = select(frontend_session).where( - and_( - frontend_session.c.session_cookie == cookie, - frontend_session.c.ip == ip - ) - ) - - result = self.execute(sql) - if result is None: return None - return result.fetchone() - - def delete_session(self, session_id: int) -> bool: - sql = Delete(frontend_session).where(frontend_session.c.id == session_id) - + def get_user(self, user_id: int) -> Optional[Row]: + sql = select(aime_user).where(aime_user.c.id == user_id) result = self.execute(sql) if result is None: return False - return True + return result.fetchone() + + def check_password(self, user_id: int, passwd: bytes = None) -> bool: + usr = self.get_user(user_id) + if usr is None: return False - def create_session(self, user_id: int, ip: str = "0.0.0.0", expires: datetime = datetime.now() + timedelta(days=1)) -> Optional[str]: - cookie = uuid4().hex + if usr['password'] is None: + return False - sql = insert(frontend_session).values( - user = user_id, - ip = ip, - session_cookie = cookie, - expires = expires - ) - - result = self.execute(sql) - if result is None: - return None - return cookie + return bcrypt.checkpw(passwd, usr['password'].encode()) def reset_autoincrement(self, ai_value: int) -> None: # ALTER TABLE isn't in sqlalchemy so we do this the ugly way diff --git a/core/data/schema/versions/CORE_3_rollback.sql b/core/data/schema/versions/CORE_3_rollback.sql new file mode 100644 index 0000000..9132cc3 --- /dev/null +++ b/core/data/schema/versions/CORE_3_rollback.sql @@ -0,0 +1,12 @@ +CREATE TABLE `frontend_session` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `user` int(11) NOT NULL, + `ip` varchar(15) DEFAULT NULL, + `session_cookie` varchar(32) NOT NULL, + `expires` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), + PRIMARY KEY (`id`), + UNIQUE KEY `id` (`id`), + UNIQUE KEY `session_cookie` (`session_cookie`), + KEY `user` (`user`), + CONSTRAINT `frontend_session_ibfk_1` FOREIGN KEY (`user`) REFERENCES `aime_user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4; \ No newline at end of file diff --git a/core/data/schema/versions/CORE_4_upgrade.sql b/core/data/schema/versions/CORE_4_upgrade.sql new file mode 100644 index 0000000..6a04c74 --- /dev/null +++ b/core/data/schema/versions/CORE_4_upgrade.sql @@ -0,0 +1 @@ +DROP TABLE `frontend_session`; \ No newline at end of file diff --git a/core/frontend.py b/core/frontend.py index 780698e..6f95073 100644 --- a/core/frontend.py +++ b/core/frontend.py @@ -4,6 +4,9 @@ from twisted.web import resource from twisted.web.util import redirectTo from twisted.web.http import Request from logging.handlers import TimedRotatingFileHandler +from twisted.web.server import Session +from zope.interface import Interface, Attribute, implementer +from twisted.python.components import registerAdapter import jinja2 import bcrypt @@ -11,6 +14,18 @@ from core.config import CoreConfig from core.data import Data from core.utils import Utils +class IUserSession(Interface): + userId = Attribute("User's ID") + current_ip = Attribute("User's current ip address") + permissions = Attribute("User's permission level") + +@implementer(IUserSession) +class UserSession(object): + def __init__(self, session): + self.userId = 0 + self.current_ip = "0.0.0.0" + self.permissions = 0 + class FrontendServlet(resource.Resource): def getChild(self, name: bytes, request: Request): self.logger.debug(f"{request.getClientIP()} -> {name.decode()}") @@ -38,6 +53,7 @@ class FrontendServlet(resource.Resource): 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() @@ -59,8 +75,8 @@ class FrontendServlet(resource.Resource): def render_GET(self, request): self.logger.debug(f"{request.getClientIP()} -> {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).encode("utf-16") + template = self.environment.get_template("core/frontend/index.jinja") + return template.render(server_name=self.config.server.name, title=self.config.server.name, game_list=self.game_list, sesh=vars(IUserSession(request.getSession()))).encode("utf-16") class FE_Base(resource.Resource): """ @@ -80,6 +96,12 @@ class FE_Gate(FE_Base): def render_GET(self, request: Request): self.logger.debug(f"{request.getClientIP()} -> {request.uri.decode()}") uri: str = request.uri.decode() + + sesh = request.getSession() + usr_sesh = IUserSession(sesh) + if usr_sesh.userId > 0: + return redirectTo(b"/user", request) + if uri.startswith("/gate/create"): return self.create_user(request) @@ -92,7 +114,7 @@ class FE_Gate(FE_Base): else: err = 0 template = self.environment.get_template("core/frontend/gate/gate.jinja") - return template.render(title=f"{self.core_config.server.name} | Login Gate", error=err).encode("utf-16") + return template.render(title=f"{self.core_config.server.name} | Login Gate", error=err, sesh=vars(usr_sesh)).encode("utf-16") def render_POST(self, request: Request): uri = request.uri.decode() @@ -100,7 +122,7 @@ class FE_Gate(FE_Base): if uri == "/gate/gate.login": access_code: str = request.args[b"access_code"][0].decode() - passwd: str = request.args[b"passwd"][0] + passwd: bytes = request.args[b"passwd"][0] if passwd == b"": passwd = None @@ -109,20 +131,22 @@ class FE_Gate(FE_Base): return redirectTo(b"/gate?e=1", request) if passwd is None: - sesh = self.data.user.login(uid, ip=ip) + sesh = self.data.user.check_password(uid) if sesh is not None: return redirectTo(f"/gate/create?ac={access_code}".encode(), request) return redirectTo(b"/gate?e=1", request) - salt = bcrypt.gensalt() - hashed = bcrypt.hashpw(passwd, salt) - sesh = self.data.user.login(uid, hashed, ip) - - if sesh is None: + if not self.data.user.check_password(uid, passwd): return redirectTo(b"/gate?e=1", request) - - request.addCookie('session', sesh) + + self.logger.info(f"Successful login of user {uid} at {ip}") + + sesh = request.getSession() + usr_sesh = IUserSession(sesh) + usr_sesh.userId = uid + usr_sesh.current_ip = ip + return redirectTo(b"/user", request) elif uri == "/gate/gate.create": @@ -142,10 +166,8 @@ class FE_Gate(FE_Base): if result is None: return redirectTo(b"/gate?e=3", request) - sesh = self.data.user.login(uid, hashed, ip) - if sesh is None: + if not self.data.user.check_password(uid, passwd.encode()): return redirectTo(b"/gate", request) - request.addCookie('session', sesh) return redirectTo(b"/user", request) @@ -159,14 +181,18 @@ class FE_Gate(FE_Base): ac = request.args[b'ac'][0].decode() template = self.environment.get_template("core/frontend/gate/create.jinja") - return template.render(title=f"{self.core_config.server.name} | Create User", code=ac).encode("utf-16") + return template.render(title=f"{self.core_config.server.name} | Create User", code=ac, sesh={"userId": 0}).encode("utf-16") class FE_User(FE_Base): def render_GET(self, request: Request): template = self.environment.get_template("core/frontend/user/index.jinja") - return template.render().encode("utf-16") - if b'session' not in request.cookies: + + 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} | Account", sesh=vars(usr_sesh)).encode("utf-16") class FE_Game(FE_Base): isLeaf = False diff --git a/core/frontend/gate/gate.jinja b/core/frontend/gate/gate.jinja index 760fbab..4f4e2fb 100644 --- a/core/frontend/gate/gate.jinja +++ b/core/frontend/gate/gate.jinja @@ -2,10 +2,23 @@ {% block content %}

Gate

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

- +

diff --git a/core/frontend/widgets/topbar.jinja b/core/frontend/widgets/topbar.jinja index 6bef3e3..d196361 100644 --- a/core/frontend/widgets/topbar.jinja +++ b/core/frontend/widgets/topbar.jinja @@ -9,5 +9,10 @@
+ {% if sesh is defined and sesh["userId"] > 0 %} + + {% else %} + {% endif %} +
\ No newline at end of file From 3181e1f4f857fa2003f81355470278817dd71560 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 3 Mar 2023 21:38:26 -0500 Subject: [PATCH 05/63] frontend: add registration instructions --- core/frontend/gate/gate.jinja | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/frontend/gate/gate.jinja b/core/frontend/gate/gate.jinja index 4f4e2fb..90abb98 100644 --- a/core/frontend/gate/gate.jinja +++ b/core/frontend/gate/gate.jinja @@ -27,4 +27,6 @@

+
*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 From f5d4f519d389cb8be97a6df71223658f3222f06c Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sat, 4 Mar 2023 00:04:47 -0500 Subject: [PATCH 06/63] database: add create-owner, migrate-card, and cleanup commands --- core/data/database.py | 66 +++++++++++++++++++++++++++++++++++++--- core/data/schema/card.py | 36 ++++++++++++++++++---- core/data/schema/user.py | 32 +++++++++++++------ dbutils.py | 13 ++++++++ 4 files changed, 127 insertions(+), 20 deletions(-) diff --git a/core/data/database.py b/core/data/database.py index 4ba3b3a..ab4c587 100644 --- a/core/data/database.py +++ b/core/data/database.py @@ -1,12 +1,12 @@ import logging, coloredlogs -from typing import Any, Dict, List +from typing import Optional from sqlalchemy.orm import scoped_session, sessionmaker from sqlalchemy.exc import SQLAlchemyError from sqlalchemy import create_engine from logging.handlers import TimedRotatingFileHandler -from datetime import datetime -import importlib, os, json - +import importlib, os +import secrets, string +import bcrypt from hashlib import sha256 from core.config import CoreConfig @@ -138,3 +138,61 @@ class Data: return None self.logger.info(f"Successfully migrated {game} to schema version {version}") + + def create_owner(self, email: Optional[str] = None) -> None: + pw = ''.join(secrets.choice(string.ascii_letters + string.digits) for i in range(20)) + hash = bcrypt.hashpw(pw.encode(), bcrypt.gensalt()) + + user_id = self.user.create_user(email=email, permission=255, password=hash) + if user_id is None: + self.logger.error(f"Failed to create owner with email {email}") + return + + card_id = self.card.create_card(user_id, "00000000000000000000") + if card_id is None: + self.logger.error(f"Failed to create card for owner with id {user_id}") + return + + self.logger.warn(f"Successfully created owner with email {email}, access code 00000000000000000000, and password {pw} Make sure to change this password and assign a real card ASAP!") + + def migrate_card(self, old_ac: str, new_ac: str, should_force: bool) -> None: + if old_ac == new_ac: + self.logger.error("Both access codes are the same!") + return + + new_card = self.card.get_card_by_access_code(new_ac) + if new_card is None: + self.card.update_access_code(old_ac, new_ac) + return + + if not should_force: + self.logger.warn(f"Card already exists for access code {new_ac} (id {new_card['id']}). If you wish to continue, rerun with the '--force' flag."\ + f" All exiting data on the target card {new_ac} will be perminently erased and replaced with data from card {old_ac}.") + return + + self.logger.info(f"All exiting data on the target card {new_ac} will be perminently erased and replaced with data from card {old_ac}.") + self.card.delete_card(new_card["id"]) + self.card.update_access_code(old_ac, new_ac) + + hanging_user = self.user.get_user(new_card["user"]) + if hanging_user["password"] is None: + self.logger.info(f"Delete hanging user {hanging_user['id']}") + self.user.delete_user(hanging_user['id']) + + def delete_hanging_users(self) -> None: + """ + Finds and deletes users that have not registered for the webui that have no cards assocated with them. + """ + unreg_users = self.user.get_unregistered_users() + if unreg_users is None: + self.logger.error("Error occoured finding unregistered users") + + for user in unreg_users: + cards = self.card.get_user_cards(user['id']) + if cards is None: + self.logger.error(f"Error getting cards for user {user['id']}") + continue + + if not cards: + self.logger.info(f"Delete hanging user {user['id']}") + self.user.delete_user(user['id']) diff --git a/core/data/schema/card.py b/core/data/schema/card.py index 7c0c945..dc74832 100644 --- a/core/data/schema/card.py +++ b/core/data/schema/card.py @@ -3,6 +3,7 @@ from sqlalchemy import Table, Column, UniqueConstraint from sqlalchemy.types import Integer, String, Boolean, TIMESTAMP from sqlalchemy.sql.schema import ForeignKey from sqlalchemy.sql import func +from sqlalchemy.engine import Row from core.data.schema.base import BaseData, metadata @@ -21,21 +22,44 @@ aime_card = Table( ) class CardData(BaseData): - 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 - """ + def get_card_by_access_code(self, access_code: str) -> Optional[Row]: sql = aime_card.select(aime_card.c.access_code == access_code) result = self.execute(sql) if result is None: return None + return result.fetchone() - card = result.fetchone() + def get_card_by_id(self, card_id: int) -> Optional[Row]: + sql = aime_card.select(aime_card.c.id == card_id) + + result = self.execute(sql) + if result is None: return None + return result.fetchone() + + def update_access_code(self, old_ac: str, new_ac: str) -> None: + sql = aime_card.update(aime_card.c.access_code == old_ac).values(access_code = new_ac) + + result = self.execute(sql) + if result is None: + self.logger.error(f"Failed to change card access code from {old_ac} to {new_ac}") + + def get_user_id_from_card(self, access_code: str) -> Optional[int]: + """ + Given a 20 digit access code as a string, get the user id associated with that card + """ + card = self.get_card_by_access_code(access_code) if card is None: return None return int(card["user"]) + + def delete_card(self, card_id: int) -> None: + sql = aime_card.delete(aime_card.c.id == card_id) - def get_user_cards(self, aime_id: int) -> Optional[List[Dict]]: + result = self.execute(sql) + if result is None: + self.logger.error(f"Failed to delete card with id {card_id}") + + def get_user_cards(self, aime_id: int) -> Optional[List[Row]]: """ Returns all cards owned by a user """ diff --git a/core/data/schema/user.py b/core/data/schema/user.py index b7f72e2..aee07e9 100644 --- a/core/data/schema/user.py +++ b/core/data/schema/user.py @@ -1,13 +1,10 @@ from enum import Enum -from typing import Dict, Optional -from sqlalchemy import Table, Column, and_ +from typing import Optional, List +from sqlalchemy import Table, Column from sqlalchemy.types import Integer, String, TIMESTAMP -from sqlalchemy.sql.schema import ForeignKey from sqlalchemy.sql import func from sqlalchemy.dialects.mysql import insert -from sqlalchemy.sql import func, select, Delete -from uuid import uuid4 -from datetime import datetime, timedelta +from sqlalchemy.sql import func, select from sqlalchemy.engine import Row import bcrypt @@ -34,9 +31,6 @@ class PermissionBits(Enum): class UserData(BaseData): def create_user(self, id: int = None, username: str = None, email: str = None, password: str = None, permission: int = 1) -> Optional[int]: - if email is None: - permission = 1 - if id is None: sql = insert(aime_user).values( username=username, @@ -82,4 +76,22 @@ class UserData(BaseData): 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) \ No newline at end of file + self.execute(sql) + + def delete_user(self, user_id: int) -> None: + sql = aime_user.delete(aime_user.c.id == user_id) + + result = self.execute(sql) + if result is None: + self.logger.error(f"Failed to delete user with id {user_id}") + + def get_unregistered_users(self) -> List[Row]: + """ + Returns a list of users who have not registered with the webui. They may or may not have cards. + """ + sql = select(aime_user).where(aime_user.c.password == None) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() \ No newline at end of file diff --git a/dbutils.py b/dbutils.py index 6285e30..b80a070 100644 --- a/dbutils.py +++ b/dbutils.py @@ -8,6 +8,10 @@ if __name__=='__main__': parser.add_argument("--config", "-c", type=str, help="Config folder to use", default="config") parser.add_argument("--version", "-v", type=str, help="Version of the database to upgrade/rollback to") parser.add_argument("--game", "-g", type=str, help="Game code of the game who's schema will be updated/rolled back. Ex. SDFE") + parser.add_argument("--email", "-e", type=str, help="Email for the new user") + parser.add_argument("--old_ac", "-o", type=str, help="Access code to transfer from") + parser.add_argument("--new_ac", "-n", type=str, help="Access code to transfer to") + parser.add_argument("--force", "-f", type=bool, help="Force the action to happen") parser.add_argument("action", type=str, help="DB Action, create, recreate, upgrade, or rollback") args = parser.parse_args() @@ -33,4 +37,13 @@ if __name__=='__main__': else: data.migrate_database(args.game, int(args.version), args.action) + elif args.action == "create-owner": + data.create_owner(args.email) + + elif args.action == "migrate-card": + data.migrate_card(args.old_ac, args.new_ac, args.force) + + elif args.action == "cleanup": + data.delete_hanging_users() + data.logger.info("Done") From fe8f40c62718cd0d49aef362b96a2d469cd4a7de Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Sat, 4 Mar 2023 11:09:03 +0100 Subject: [PATCH 07/63] Fixed upgrade script endDate --- core/data/schema/versions/SDED_1_upgrade.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/data/schema/versions/SDED_1_upgrade.sql b/core/data/schema/versions/SDED_1_upgrade.sql index c601745..a4d666e 100644 --- a/core/data/schema/versions/SDED_1_upgrade.sql +++ b/core/data/schema/versions/SDED_1_upgrade.sql @@ -35,7 +35,7 @@ CREATE TABLE ongeki_static_gachas ( changeRateCnt1 INT DEFAULT 0, changeRateCnt2 INT DEFAULT 0, startDate TIMESTAMP DEFAULT '2018-01-01 00:00:00.0', - endDate TIMESTAMP DEFAULT '2018-01-01 00:00:00.0', + endDate TIMESTAMP DEFAULT '2038-01-01 00:00:00.0', noticeStartDate TIMESTAMP DEFAULT '2018-01-01 00:00:00.0', noticeEndDate TIMESTAMP DEFAULT '2038-01-01 00:00:00.0', convertEndDate TIMESTAMP DEFAULT '2038-01-01 00:00:00.0', From a340bcf1dd4ebde9b7a2c204fab6c4b4707900cd Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sat, 4 Mar 2023 21:27:52 -0500 Subject: [PATCH 08/63] change how allnet uri/host is generated --- core/allnet.py | 79 +++++++++++++++++++++----------------- core/title.py | 20 +++++++--- example_config/pokken.yaml | 4 ++ titles/pokken/__init__.py | 11 ------ titles/pokken/base.py | 14 +++---- titles/pokken/config.py | 18 ++++++++- titles/pokken/index.py | 14 +++++++ 7 files changed, 101 insertions(+), 59 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index 71eeb33..792cdff 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -48,49 +48,58 @@ class AllnetServlet: self.logger.error("No games detected!") for _, mod in plugins.items(): - for code in mod.game_codes: - if hasattr(mod, "use_default_title") and mod.use_default_title: - if hasattr(mod, "include_protocol") and mod.include_protocol: - if hasattr(mod, "title_secure") and mod.title_secure: - uri = "https://" + if hasattr(mod.index, "get_allnet_info"): + for code in mod.game_codes: + enabled, uri, host = mod.index.get_allnet_info(code, self.config, self.config_folder) + + if enabled: + self.uri_registry[code] = (uri, host) + + else: + for code in mod.game_codes: + if hasattr(mod, "use_default_title") and mod.use_default_title: + if hasattr(mod, "include_protocol") and mod.include_protocol: + if hasattr(mod, "title_secure") and mod.title_secure: + uri = "https://" + + else: + uri = "http://" + + else: + uri = "" + + if core_cfg.server.is_develop: + uri += f"{core_cfg.title.hostname}:{core_cfg.title.port}" else: - uri = "http://" + uri += f"{core_cfg.title.hostname}" + + uri += f"/{code}/$v" - else: - uri = "" - - if core_cfg.server.is_develop: - uri += f"{core_cfg.title.hostname}:{core_cfg.title.port}" + if hasattr(mod, "trailing_slash") and mod.trailing_slash: + uri += "/" else: - uri += f"{core_cfg.title.hostname}" - - uri += f"/{code}/$v" + if hasattr(mod, "uri"): + uri = mod.uri + else: + uri = "" - if hasattr(mod, "trailing_slash") and mod.trailing_slash: - uri += "/" - - else: - if hasattr(mod, "uri"): - uri = mod.uri - else: - uri = "" - - if hasattr(mod, "host"): - host = mod.host - - elif hasattr(mod, "use_default_host") and mod.use_default_host: - if core_cfg.server.is_develop: - host = f"{core_cfg.title.hostname}:{core_cfg.title.port}" + if hasattr(mod, "host"): + host = mod.host + + elif hasattr(mod, "use_default_host") and mod.use_default_host: + if core_cfg.server.is_develop: + host = f"{core_cfg.title.hostname}:{core_cfg.title.port}" + + else: + host = f"{core_cfg.title.hostname}" else: - host = f"{core_cfg.title.hostname}" - - else: - host = "" - - self.uri_registry[code] = (uri, host) + host = "" + + self.uri_registry[code] = (uri, host) + self.logger.info(f"Allnet serving {len(self.uri_registry)} games on port {core_cfg.allnet.port}") def handle_poweron(self, request: Request, _: Dict): diff --git a/core/title.py b/core/title.py index 252a130..c704e9a 100644 --- a/core/title.py +++ b/core/title.py @@ -37,16 +37,26 @@ class TitleServlet(): for folder, mod in plugins.items(): if hasattr(mod, "game_codes") and hasattr(mod, "index"): - handler_cls = mod.index(self.config, self.config_folder) - if hasattr(handler_cls, "setup"): - handler_cls.setup() + should_call_setup = True for code in mod.game_codes: - self.title_registry[code] = handler_cls + if hasattr(mod.index, "get_allnet_info"): + enabled, _, _ = mod.index.get_allnet_info(code, self.config, self.config_folder) + + else: + enabled = True + + if enabled: + handler_cls = mod.index(self.config, self.config_folder) + if hasattr(handler_cls, "setup") and should_call_setup: + handler_cls.setup() + should_call_setup = False + + self.title_registry[code] = handler_cls else: self.logger.error(f"{folder} missing game_code or index in __init__.py") - + self.logger.info(f"Serving {len(self.title_registry)} game codes on port {core_cfg.title.port}") def render_GET(self, request: Request, endpoints: dict) -> bytes: diff --git a/example_config/pokken.yaml b/example_config/pokken.yaml index 5523996..e465ceb 100644 --- a/example_config/pokken.yaml +++ b/example_config/pokken.yaml @@ -1,7 +1,11 @@ server: + hostname: "localhost" enable: True loglevel: "info" port: 9000 port_matching: 9001 + port_stun: 9002 + port_turn: 9003 + port_admission: 9004 ssl_cert: cert/pokken.crt ssl_key: cert/pokken.key \ No newline at end of file diff --git a/titles/pokken/__init__.py b/titles/pokken/__init__.py index 3b574fd..6340de8 100644 --- a/titles/pokken/__init__.py +++ b/titles/pokken/__init__.py @@ -4,16 +4,5 @@ from titles.pokken.database import PokkenData index = PokkenServlet database = PokkenData - -use_default_title = True -include_protocol = True -title_secure = True game_codes = [PokkenConstants.GAME_CODE] -trailing_slash = True -use_default_host = False - -include_port = True -uri="https://$h:$p/" -host="$h:$p/" - current_schema_version = 1 \ No newline at end of file diff --git a/titles/pokken/base.py b/titles/pokken/base.py index bd95295..33232e4 100644 --- a/titles/pokken/base.py +++ b/titles/pokken/base.py @@ -35,19 +35,19 @@ class PokkenBase(): regist_pcb.server_time = int(datetime.now().timestamp() / 1000) biwa_setting = { "MatchingServer": { - "host": f"https://{self.core_cfg.title.hostname}", - "port": 9000, + "host": f"https://{self.game_cfg.server.hostname}", + "port": self.game_cfg.server.port_matching, "url": "/matching" }, "StunServer": { - "addr": self.core_cfg.title.hostname, - "port": 3333 + "addr": self.game_cfg.server.hostname, + "port": self.game_cfg.server.port_stun }, "TurnServer": { - "addr": self.core_cfg.title.hostname, - "port": 4444 + "addr": self.game_cfg.server.hostname, + "port": self.game_cfg.server.port_turn }, - "AdmissionUrl": f"ws://{self.core_cfg.title.hostname}:1111", + "AdmissionUrl": f"ws://{self.game_cfg.server.hostname}:{self.game_cfg.server.port_admission}", "locationId": 123, "logfilename": "JackalMatchingLibrary.log", "biwalogfilename": "./biwa.log" diff --git a/titles/pokken/config.py b/titles/pokken/config.py index b6596f2..9b2ae0f 100644 --- a/titles/pokken/config.py +++ b/titles/pokken/config.py @@ -4,6 +4,10 @@ class PokkenServerConfig(): def __init__(self, parent_config: "PokkenConfig"): self.__config = parent_config + @property + def hostname(self) -> str: + return CoreConfig.get_config_field(self.__config, 'pokken', 'server', 'hostname', default="localhost") + @property def enable(self) -> bool: return CoreConfig.get_config_field(self.__config, 'pokken', 'server', 'enable', default=True) @@ -18,7 +22,19 @@ class PokkenServerConfig(): @property def port_matching(self) -> int: - return CoreConfig.get_config_field(self.__config, 'pokken', 'server', 'port', default=9001) + return CoreConfig.get_config_field(self.__config, 'pokken', 'server', 'port_matching', default=9001) + + @property + def port_stun(self) -> int: + return CoreConfig.get_config_field(self.__config, 'pokken', 'server', 'port_stun', default=9002) + + @property + def port_turn(self) -> int: + return CoreConfig.get_config_field(self.__config, 'pokken', 'server', 'port_turn', default=9003) + + @property + def port_admission(self) -> int: + return CoreConfig.get_config_field(self.__config, 'pokken', 'server', 'port_admission', default=9004) @property def ssl_cert(self) -> str: diff --git a/titles/pokken/index.py b/titles/pokken/index.py index a47cac7..13fdf6c 100644 --- a/titles/pokken/index.py +++ b/titles/pokken/index.py @@ -1,3 +1,4 @@ +from typing import Tuple from twisted.web.http import Request from twisted.web import resource, server from twisted.internet import reactor, endpoints @@ -40,6 +41,19 @@ class PokkenServlet(resource.Resource): self.logger.inited = True self.base = PokkenBase(core_cfg, self.game_cfg) + + @classmethod + def get_allnet_info(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str, str]: + game_cfg = PokkenConfig() + game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/pokken.yaml"))) + + if not game_cfg.server.enable: + return (False, "", "") + + if core_cfg.server.is_develop: + return (True, f"https://{game_cfg.server.hostname}:{game_cfg.server.port}/{game_code}/$v/", f"{game_cfg.server.hostname}:{game_cfg.server.port}/") + + return (True, f"https://{game_cfg.server.hostname}/{game_code}/$v/", f"{game_cfg.server.hostname}/") def setup(self): """ From b8fd0baee53f50af22de71400b4e493627cd5f5c Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sat, 4 Mar 2023 21:34:35 -0500 Subject: [PATCH 09/63] wacca: move to get_allnet_info, add safety for loading config --- titles/wacca/index.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/titles/wacca/index.py b/titles/wacca/index.py index 37d3f9e..8b30dc6 100644 --- a/titles/wacca/index.py +++ b/titles/wacca/index.py @@ -3,10 +3,10 @@ import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler import logging import json -from datetime import datetime from hashlib import md5 from twisted.web.http import Request -from typing import Dict +from typing import Dict, Tuple +from os import path from core.config import CoreConfig from titles.wacca.config import WaccaConfig @@ -24,7 +24,8 @@ class WaccaServlet(): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: self.core_cfg = core_cfg self.game_cfg = WaccaConfig() - self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/wacca.yaml"))) + if path.exists(f"{cfg_dir}/wacca.yaml"): + self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/wacca.yaml"))) self.versions = [ WaccaBase(core_cfg, self.game_cfg), @@ -51,6 +52,19 @@ class WaccaServlet(): self.logger.setLevel(self.game_cfg.server.loglevel) coloredlogs.install(level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str) + @classmethod + def get_allnet_info(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str, str]: + game_cfg = WaccaConfig() + game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/wacca.yaml"))) + + if not game_cfg.server.enable: + return (False, "", "") + + if core_cfg.server.is_develop: + return (True, f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/", "") + + return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", "") + def render_POST(self, request: Request, version: int, url_path: str) -> bytes: def end(resp: Dict) -> bytes: hash = md5(json.dumps(resp, ensure_ascii=False).encode()).digest() From e0fdd937e67eb00a5b4b80a0af32186795201823 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sat, 4 Mar 2023 21:36:15 -0500 Subject: [PATCH 10/63] pokken: add safety for loading config that doesn't exist --- titles/pokken/index.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/titles/pokken/index.py b/titles/pokken/index.py index 13fdf6c..908d247 100644 --- a/titles/pokken/index.py +++ b/titles/pokken/index.py @@ -19,7 +19,8 @@ class PokkenServlet(resource.Resource): self.core_cfg = core_cfg self.config_dir = cfg_dir self.game_cfg = PokkenConfig() - self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/pokken.yaml"))) + if path.exists(f"{cfg_dir}/pokken.yaml"): + self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/pokken.yaml"))) self.logger = logging.getLogger("pokken") if not hasattr(self.logger, "inited"): From b2b28850ddf8c593f58a42110e9ff36ceb1b3511 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sat, 4 Mar 2023 21:39:38 -0500 Subject: [PATCH 11/63] ongeki: add get_allnet_info, --- titles/ongeki/__init__.py | 8 -------- titles/ongeki/index.py | 18 +++++++++++++++++- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/titles/ongeki/__init__.py b/titles/ongeki/__init__.py index 26e107c..7f03a8f 100644 --- a/titles/ongeki/__init__.py +++ b/titles/ongeki/__init__.py @@ -6,13 +6,5 @@ from titles.ongeki.read import OngekiReader index = OngekiServlet database = OngekiData reader = OngekiReader - -use_default_title = True -include_protocol = True -title_secure = False game_codes = [OngekiConstants.GAME_CODE] -trailing_slash = True -use_default_host = False -host = "" - current_schema_version = 2 \ No newline at end of file diff --git a/titles/ongeki/index.py b/titles/ongeki/index.py index c636659..9e512b2 100644 --- a/titles/ongeki/index.py +++ b/titles/ongeki/index.py @@ -6,6 +6,8 @@ import string import logging, coloredlogs import zlib from logging.handlers import TimedRotatingFileHandler +from os import path +from typing import Tuple from core.config import CoreConfig from titles.ongeki.config import OngekiConfig @@ -23,7 +25,8 @@ class OngekiServlet(): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: self.core_cfg = core_cfg self.game_cfg = OngekiConfig() - self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/ongeki.yaml"))) + if path.exists(f"{cfg_dir}/ongeki.yaml"): + self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/ongeki.yaml"))) self.versions = [ OngekiBase(core_cfg, self.game_cfg), @@ -52,6 +55,19 @@ class OngekiServlet(): self.logger.setLevel(self.game_cfg.server.loglevel) coloredlogs.install(level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str) + + @classmethod + def get_allnet_info(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str, str]: + game_cfg = OngekiConfig() + game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/ongeki.yaml"))) + + if not game_cfg.server.enable: + return (False, "", "") + + if core_cfg.server.is_develop: + return (True, f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/", f"{core_cfg.title.hostname}:{core_cfg.title.port}/") + + return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", f"{core_cfg.title.hostname}/") def render_POST(self, request: Request, version: int, url_path: str) -> bytes: req_raw = request.content.getvalue() From bfe5294d51593a6a00b7c7186a9e2af226b6c205 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sat, 4 Mar 2023 21:58:51 -0500 Subject: [PATCH 12/63] add get_allnet_info and config loading safety to all games --- core/allnet.py | 44 +--------------------------------------- core/title.py | 23 +++++++++++---------- dbutils.py | 4 +++- index.py | 3 ++- read.py | 5 +++-- titles/chuni/__init__.py | 8 -------- titles/chuni/const.py | 2 ++ titles/chuni/index.py | 19 ++++++++++++++++- titles/cxb/__init__.py | 11 ---------- titles/cxb/index.py | 20 ++++++++++++++++-- titles/diva/__init__.py | 8 -------- titles/diva/const.py | 2 ++ titles/diva/index.py | 20 +++++++++++++++++- titles/mai2/__init__.py | 8 -------- titles/mai2/index.py | 20 +++++++++++++++++- titles/ongeki/const.py | 2 ++ titles/ongeki/index.py | 8 +++++--- titles/pokken/index.py | 5 ++++- titles/wacca/__init__.py | 8 -------- titles/wacca/index.py | 11 +++++----- 20 files changed, 116 insertions(+), 115 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index 792cdff..ec39158 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -56,49 +56,7 @@ class AllnetServlet: self.uri_registry[code] = (uri, host) else: - for code in mod.game_codes: - if hasattr(mod, "use_default_title") and mod.use_default_title: - if hasattr(mod, "include_protocol") and mod.include_protocol: - if hasattr(mod, "title_secure") and mod.title_secure: - uri = "https://" - - else: - uri = "http://" - - else: - uri = "" - - if core_cfg.server.is_develop: - uri += f"{core_cfg.title.hostname}:{core_cfg.title.port}" - - else: - uri += f"{core_cfg.title.hostname}" - - uri += f"/{code}/$v" - - if hasattr(mod, "trailing_slash") and mod.trailing_slash: - uri += "/" - - else: - if hasattr(mod, "uri"): - uri = mod.uri - else: - uri = "" - - if hasattr(mod, "host"): - host = mod.host - - elif hasattr(mod, "use_default_host") and mod.use_default_host: - if core_cfg.server.is_develop: - host = f"{core_cfg.title.hostname}:{core_cfg.title.port}" - - else: - host = f"{core_cfg.title.hostname}" - - else: - host = "" - - self.uri_registry[code] = (uri, host) + self.logger.warn("Game {_} has no get_allnet_info method.") self.logger.info(f"Allnet serving {len(self.uri_registry)} games on port {core_cfg.allnet.port}") diff --git a/core/title.py b/core/title.py index c704e9a..f201d8b 100644 --- a/core/title.py +++ b/core/title.py @@ -39,20 +39,21 @@ class TitleServlet(): if hasattr(mod, "game_codes") and hasattr(mod, "index"): should_call_setup = True - for code in mod.game_codes: - if hasattr(mod.index, "get_allnet_info"): + if hasattr(mod.index, "get_allnet_info"): + for code in mod.game_codes: enabled, _, _ = mod.index.get_allnet_info(code, self.config, self.config_folder) - - else: - enabled = True - if enabled: - handler_cls = mod.index(self.config, self.config_folder) - if hasattr(handler_cls, "setup") and should_call_setup: - handler_cls.setup() - should_call_setup = False + if enabled: + handler_cls = mod.index(self.config, self.config_folder) + + if hasattr(handler_cls, "setup") and should_call_setup: + handler_cls.setup() + should_call_setup = False - self.title_registry[code] = handler_cls + self.title_registry[code] = handler_cls + + else: + self.logger.warn(f"Game {folder} has no get_allnet_info") else: self.logger.error(f"{folder} missing game_code or index in __init__.py") diff --git a/dbutils.py b/dbutils.py index b80a070..a2aa36b 100644 --- a/dbutils.py +++ b/dbutils.py @@ -2,6 +2,7 @@ import yaml import argparse from core.config import CoreConfig from core.data import Data +from os import path if __name__=='__main__': parser = argparse.ArgumentParser(description="Database utilities") @@ -16,7 +17,8 @@ if __name__=='__main__': args = parser.parse_args() cfg = CoreConfig() - cfg.update(yaml.safe_load(open(f"{args.config}/core.yaml"))) + if path.exists(f"{args.config}/core.yaml"): + cfg.update(yaml.safe_load(open(f"{args.config}/core.yaml"))) data = Data(cfg) if args.action == "create": diff --git a/index.py b/index.py index 51842bd..4c0ece9 100644 --- a/index.py +++ b/index.py @@ -90,7 +90,8 @@ if __name__ == "__main__": exit(1) cfg: CoreConfig = CoreConfig() - cfg.update(yaml.safe_load(open(f"{args.config}/core.yaml"))) + if path.exists(f"{args.config}/core.yaml"): + cfg.update(yaml.safe_load(open(f"{args.config}/core.yaml"))) logger = logging.getLogger("core") log_fmt_str = "[%(asctime)s] Core | %(levelname)s | %(message)s" diff --git a/read.py b/read.py index 869ad97..1f89cb6 100644 --- a/read.py +++ b/read.py @@ -3,7 +3,7 @@ import argparse import re import os import yaml -import importlib +from os import path import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler @@ -79,7 +79,8 @@ if __name__ == "__main__": args = parser.parse_args() config = CoreConfig() - config.update(yaml.safe_load(open(f"{args.config}/core.yaml"))) + if path.exists(f"{args.config}/core.yaml"): + config.update(yaml.safe_load(open(f"{args.config}/core.yaml"))) log_fmt_str = "[%(asctime)s] Reader | %(levelname)s | %(message)s" log_fmt = logging.Formatter(log_fmt_str) diff --git a/titles/chuni/__init__.py b/titles/chuni/__init__.py index 3883aeb..7256b10 100644 --- a/titles/chuni/__init__.py +++ b/titles/chuni/__init__.py @@ -6,13 +6,5 @@ from titles.chuni.read import ChuniReader index = ChuniServlet database = ChuniData reader = ChuniReader - -use_default_title = True -include_protocol = True -title_secure = False game_codes = [ChuniConstants.GAME_CODE, ChuniConstants.GAME_CODE_NEW] -trailing_slash = True -use_default_host = False -host = "" - current_schema_version = 1 diff --git a/titles/chuni/const.py b/titles/chuni/const.py index ebc8cf2..3a111f8 100644 --- a/titles/chuni/const.py +++ b/titles/chuni/const.py @@ -2,6 +2,8 @@ class ChuniConstants(): GAME_CODE = "SDBT" GAME_CODE_NEW = "SDHD" + CONFIG_NAME = "chuni.yaml" + VER_CHUNITHM = 0 VER_CHUNITHM_PLUS = 1 VER_CHUNITHM_AIR = 2 diff --git a/titles/chuni/index.py b/titles/chuni/index.py index a0f8b55..866e9d9 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -8,6 +8,8 @@ import inflection import string from Crypto.Cipher import AES from Crypto.Util.Padding import pad +from os import path +from typing import Tuple from core import CoreConfig from titles.chuni.config import ChuniConfig @@ -30,7 +32,8 @@ class ChuniServlet(): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: self.core_cfg = core_cfg self.game_cfg = ChuniConfig() - self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/chuni.yaml"))) + if path.exists(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"): + self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"))) self.versions = [ ChuniBase(core_cfg, self.game_cfg), @@ -68,6 +71,20 @@ class ChuniServlet(): coloredlogs.install(level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str) self.logger.inited = True + @classmethod + def get_allnet_info(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str, str]: + game_cfg = ChuniConfig() + if path.exists(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"): + game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"))) + + if not game_cfg.server.enable: + return (False, "", "") + + if core_cfg.server.is_develop: + return (True, f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/", "") + + return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", "") + def render_POST(self, request: Request, version: int, url_path: str) -> bytes: req_raw = request.content.getvalue() url_split = url_path.split("/") diff --git a/titles/cxb/__init__.py b/titles/cxb/__init__.py index d57dde0..0a9db97 100644 --- a/titles/cxb/__init__.py +++ b/titles/cxb/__init__.py @@ -6,16 +6,5 @@ from titles.cxb.read import CxbReader index = CxbServlet database = CxbData reader = CxbReader - -use_default_title = False -include_protocol = True -title_secure = True game_codes = [CxbConstants.GAME_CODE] -trailing_slash = True -use_default_host = False - -include_port = True -uri = "http://$h:$p/" # If you care about the allnet response you're probably running with no SSL -host = "" - current_schema_version = 1 \ No newline at end of file diff --git a/titles/cxb/index.py b/titles/cxb/index.py index f01cf3b..ad852a8 100644 --- a/titles/cxb/index.py +++ b/titles/cxb/index.py @@ -7,7 +7,8 @@ import re import inflection import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler -from typing import Dict +from typing import Dict, Tuple +from os import path from core.config import CoreConfig from titles.cxb.config import CxbConfig @@ -22,7 +23,8 @@ class CxbServlet(resource.Resource): self.cfg_dir = cfg_dir self.core_cfg = core_cfg self.game_cfg = CxbConfig() - self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/cxb.yaml"))) + if path.exists(f"{cfg_dir}/{CxbConstants.CONFIG_NAME}"): + self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{CxbConstants.CONFIG_NAME}"))) self.logger = logging.getLogger("cxb") if not hasattr(self.logger, "inited"): @@ -49,6 +51,20 @@ class CxbServlet(resource.Resource): CxbRevSunriseS2(core_cfg, self.game_cfg), ] + @classmethod + def get_allnet_info(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str, str]: + game_cfg = CxbConfig() + if path.exists(f"{cfg_dir}/{CxbConstants.CONFIG_NAME}"): + game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{CxbConstants.CONFIG_NAME}"))) + + if not game_cfg.server.enable: + return (False, "", "") + + if core_cfg.server.is_develop: + return (True, f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/", "") + + return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", "") + def setup(self): if self.game_cfg.server.enable: endpoints.serverFromString(reactor, f"tcp:{self.game_cfg.server.port}:interface={self.core_cfg.server.listen_address}")\ diff --git a/titles/diva/__init__.py b/titles/diva/__init__.py index acc7ce4..e14aee2 100644 --- a/titles/diva/__init__.py +++ b/titles/diva/__init__.py @@ -6,13 +6,5 @@ from titles.diva.read import DivaReader index = DivaServlet database = DivaData reader = DivaReader - -use_default_title = True -include_protocol = True -title_secure = False game_codes = [DivaConstants.GAME_CODE] -trailing_slash = True -use_default_host = False -host = "" - current_schema_version = 1 \ No newline at end of file diff --git a/titles/diva/const.py b/titles/diva/const.py index 44bbe36..2ea7024 100644 --- a/titles/diva/const.py +++ b/titles/diva/const.py @@ -1,6 +1,8 @@ class DivaConstants(): GAME_CODE = "SBZV" + CONFIG_NAME = "diva.yaml" + VER_PROJECT_DIVA_ARCADE = 0 VER_PROJECT_DIVA_ARCADE_FUTURE_TONE = 1 diff --git a/titles/diva/index.py b/titles/diva/index.py index d48a125..b049fef 100644 --- a/titles/diva/index.py +++ b/titles/diva/index.py @@ -6,16 +6,20 @@ import zlib import json import urllib.parse import base64 +from os import path +from typing import Tuple from core.config import CoreConfig from titles.diva.config import DivaConfig +from titles.diva.const import DivaConstants from titles.diva.base import DivaBase class DivaServlet(): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: self.core_cfg = core_cfg self.game_cfg = DivaConfig() - self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/diva.yaml"))) + if path.exists(f"{cfg_dir}/{DivaConstants.CONFIG_NAME}"): + self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{DivaConstants.CONFIG_NAME}"))) self.base = DivaBase(core_cfg, self.game_cfg) @@ -36,6 +40,20 @@ class DivaServlet(): self.logger.setLevel(self.game_cfg.server.loglevel) coloredlogs.install(level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str) + @classmethod + def get_allnet_info(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str, str]: + game_cfg = DivaConfig() + if path.exists(f"{cfg_dir}/{DivaConstants.CONFIG_NAME}"): + game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{DivaConstants.CONFIG_NAME}"))) + + if not game_cfg.server.enable: + return (False, "", "") + + if core_cfg.server.is_develop: + return (True, f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/", "") + + return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", "") + def render_POST(self, req: Request, version: int, url_path: str) -> bytes: req_raw = req.content.getvalue() url_header = req.getAllHeaders() diff --git a/titles/mai2/__init__.py b/titles/mai2/__init__.py index 3cc5f97..71dbd5e 100644 --- a/titles/mai2/__init__.py +++ b/titles/mai2/__init__.py @@ -6,13 +6,5 @@ from titles.mai2.read import Mai2Reader index = Mai2Servlet database = Mai2Data reader = Mai2Reader - -use_default_title = True -include_protocol = True -title_secure = False game_codes = [Mai2Constants.GAME_CODE] -trailing_slash = True -use_default_host = False -host = "" - current_schema_version = 2 \ No newline at end of file diff --git a/titles/mai2/index.py b/titles/mai2/index.py index a4a8f3e..64a38a6 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -6,6 +6,8 @@ import string import logging, coloredlogs import zlib from logging.handlers import TimedRotatingFileHandler +from os import path +from typing import Tuple from core.config import CoreConfig from titles.mai2.config import Mai2Config @@ -22,7 +24,8 @@ class Mai2Servlet(): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: self.core_cfg = core_cfg self.game_cfg = Mai2Config() - self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"))) + if path.exists(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"): + self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"))) self.versions = [ Mai2Base(core_cfg, self.game_cfg), @@ -50,6 +53,21 @@ class Mai2Servlet(): self.logger.setLevel(self.game_cfg.server.loglevel) coloredlogs.install(level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str) + @classmethod + def get_allnet_info(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str, str]: + game_cfg = Mai2Config() + + if path.exists(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"): + game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"))) + + if not game_cfg.server.enable: + return (False, "", "") + + if core_cfg.server.is_develop: + return (True, f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/", f"{core_cfg.title.hostname}:{core_cfg.title.port}/") + + return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", f"{core_cfg.title.hostname}/") + def render_POST(self, request: Request, version: int, url_path: str) -> bytes: req_raw = request.content.getvalue() url = request.uri.decode() diff --git a/titles/ongeki/const.py b/titles/ongeki/const.py index dd6c0f5..a68ca02 100644 --- a/titles/ongeki/const.py +++ b/titles/ongeki/const.py @@ -3,6 +3,8 @@ from enum import Enum class OngekiConstants(): GAME_CODE = "SDDT" + CONFIG_NAME = "ongeki.yaml" + VER_ONGEKI = 0 VER_ONGEKI_PLUS = 1 VER_ONGEKI_SUMMER = 2 diff --git a/titles/ongeki/index.py b/titles/ongeki/index.py index 9e512b2..c5e78f6 100644 --- a/titles/ongeki/index.py +++ b/titles/ongeki/index.py @@ -25,8 +25,8 @@ class OngekiServlet(): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: self.core_cfg = core_cfg self.game_cfg = OngekiConfig() - if path.exists(f"{cfg_dir}/ongeki.yaml"): - self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/ongeki.yaml"))) + if path.exists(f"{cfg_dir}/{OngekiConstants.CONFIG_NAME}"): + self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{OngekiConstants.CONFIG_NAME}"))) self.versions = [ OngekiBase(core_cfg, self.game_cfg), @@ -59,7 +59,9 @@ class OngekiServlet(): @classmethod def get_allnet_info(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str, str]: game_cfg = OngekiConfig() - game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/ongeki.yaml"))) + + if path.exists(f"{cfg_dir}/{OngekiConstants.CONFIG_NAME}"): + game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{OngekiConstants.CONFIG_NAME}"))) if not game_cfg.server.enable: return (False, "", "") diff --git a/titles/pokken/index.py b/titles/pokken/index.py index 908d247..a8b13f2 100644 --- a/titles/pokken/index.py +++ b/titles/pokken/index.py @@ -12,6 +12,7 @@ from google.protobuf.message import DecodeError from core.config import CoreConfig from titles.pokken.config import PokkenConfig from titles.pokken.base import PokkenBase +from titles.pokken.const import PokkenConstants class PokkenServlet(resource.Resource): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: @@ -46,7 +47,9 @@ class PokkenServlet(resource.Resource): @classmethod def get_allnet_info(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str, str]: game_cfg = PokkenConfig() - game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/pokken.yaml"))) + + if path.exists(f"{cfg_dir}/{PokkenConstants.CONFIG_NAME}"): + game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{PokkenConstants.CONFIG_NAME}"))) if not game_cfg.server.enable: return (False, "", "") diff --git a/titles/wacca/__init__.py b/titles/wacca/__init__.py index 41d8dc2..55205ed 100644 --- a/titles/wacca/__init__.py +++ b/titles/wacca/__init__.py @@ -8,13 +8,5 @@ index = WaccaServlet database = WaccaData reader = WaccaReader frontend = WaccaFrontend - -use_default_title = True -include_protocol = True -title_secure = False game_codes = [WaccaConstants.GAME_CODE] -trailing_slash = False -use_default_host = False -host = "" - current_schema_version = 3 \ No newline at end of file diff --git a/titles/wacca/index.py b/titles/wacca/index.py index 8b30dc6..e2f9758 100644 --- a/titles/wacca/index.py +++ b/titles/wacca/index.py @@ -24,8 +24,8 @@ class WaccaServlet(): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: self.core_cfg = core_cfg self.game_cfg = WaccaConfig() - if path.exists(f"{cfg_dir}/wacca.yaml"): - self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/wacca.yaml"))) + if path.exists(f"{cfg_dir}/{WaccaConstants.CONFIG_NAME}"): + self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{WaccaConstants.CONFIG_NAME}"))) self.versions = [ WaccaBase(core_cfg, self.game_cfg), @@ -55,15 +55,16 @@ class WaccaServlet(): @classmethod def get_allnet_info(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str, str]: game_cfg = WaccaConfig() - game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/wacca.yaml"))) + if path.exists(f"{cfg_dir}/{WaccaConstants.CONFIG_NAME}"): + game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{WaccaConstants.CONFIG_NAME}"))) if not game_cfg.server.enable: return (False, "", "") if core_cfg.server.is_develop: - return (True, f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/", "") + return (True, f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v", "") - return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", "") + return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v", "") def render_POST(self, request: Request, version: int, url_path: str) -> bytes: def end(resp: Dict) -> bytes: From b12f61198f75361913e2f88da82816c864b5ac22 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sat, 4 Mar 2023 22:46:26 -0500 Subject: [PATCH 13/63] mucha: fixups --- core/allnet.py | 3 --- core/mucha.py | 35 ++++++++++++++++++++++++++++++----- index.py | 2 +- titles/pokken/index.py | 12 ++++++++++++ 4 files changed, 43 insertions(+), 9 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index ec39158..a02c902 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -54,9 +54,6 @@ class AllnetServlet: if enabled: self.uri_registry[code] = (uri, host) - - else: - self.logger.warn("Game {_} has no get_allnet_info method.") self.logger.info(f"Allnet serving {len(self.uri_registry)} games on port {core_cfg.allnet.port}") diff --git a/core/mucha.py b/core/mucha.py index 0848c70..6cabd59 100644 --- a/core/mucha.py +++ b/core/mucha.py @@ -1,4 +1,4 @@ -from typing import Dict, Any, Optional +from typing import Dict, Any, Optional, List import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler from twisted.web import resource @@ -7,10 +7,13 @@ from datetime import datetime import pytz from core.config import CoreConfig +from core.utils import Utils class MuchaServlet: - def __init__(self, cfg: CoreConfig) -> None: + def __init__(self, cfg: CoreConfig, cfg_dir: str) -> None: self.config = cfg + self.config_dir = cfg_dir + self.mucha_registry: List[str] = [] self.logger = logging.getLogger('mucha') log_fmt_str = "[%(asctime)s] Mucha | %(levelname)s | %(message)s" @@ -28,6 +31,16 @@ class MuchaServlet: self.logger.setLevel(logging.INFO) coloredlogs.install(level=logging.INFO, logger=self.logger, fmt=log_fmt_str) + all_titles = Utils.get_all_titles() + + for _, mod in all_titles.items(): + if hasattr(mod, "index") and hasattr(mod.index, "get_mucha_info"): + enabled, game_cd = mod.index.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 on port {self.config.mucha.port}") + def handle_boardauth(self, request: Request, _: Dict) -> bytes: req_dict = self.mucha_preprocess(request.content.getvalue()) if req_dict is None: @@ -36,6 +49,13 @@ class MuchaServlet: req = MuchaAuthRequest(req_dict) self.logger.debug(f"Mucha request {vars(req)}") + self.logger.info(f"Boardauth request from {request.getClientAddress().host} for {req.gameVer}") + + if req.gameCd not in self.mucha_registry: + self.logger.warn(f"Unknown gameCd {req.gameCd}") + return b"" + + # TODO: Figure out why the S/N is the way it is. if self.config.server.is_develop: resp = MuchaAuthResponse(mucha_url=f"{self.config.mucha.hostname}:{self.config.mucha.port}") @@ -54,11 +74,16 @@ class MuchaServlet: req = MuchaUpdateRequest(req_dict) self.logger.debug(f"Mucha request {vars(req)}") + self.logger.info(f"Updatecheck request from {request.getClientAddress().host} for {req.gameVer}") + + if req.gameCd not in self.mucha_registry: + self.logger.warn(f"Unknown gameCd {req.gameCd}") + return b"" if self.config.server.is_develop: - resp = MuchaUpdateResponse(mucha_url=f"{self.config.mucha.hostname}:{self.config.mucha.port}") + resp = MuchaUpdateResponse(req.gameVer, f"{self.config.mucha.hostname}:{self.config.mucha.port}") else: - resp = MuchaUpdateResponse(mucha_url=f"{self.config.mucha.hostname}") + resp = MuchaUpdateResponse(req.gameVer, f"{self.config.mucha.hostname}") self.logger.debug(f"Mucha response {vars(resp)}") @@ -94,7 +119,7 @@ class MuchaServlet: class MuchaAuthRequest(): def __init__(self, request: Dict) -> None: self.gameVer = "" if "gameVer" not in request else request["gameVer"] - self.sendDate = "" if "sendDate" not in request else request["sendDate"] + self.sendDate = "" if "sendDate" not in request else datetime.strptime(request["sendDate"], "%Y%m%d") self.serialNum = "" if "serialNum" not in request else request["serialNum"] self.gameCd = "" if "gameCd" not in request else request["gameCd"] self.boardType = "" if "boardType" not in request else request["boardType"] diff --git a/index.py b/index.py index 4c0ece9..f545edb 100644 --- a/index.py +++ b/index.py @@ -23,7 +23,7 @@ class HttpDispatcher(resource.Resource): self.allnet = AllnetServlet(cfg, config_dir) self.title = TitleServlet(cfg, config_dir) - self.mucha = MuchaServlet(cfg) + self.mucha = MuchaServlet(cfg, config_dir) self.map_post.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'])) diff --git a/titles/pokken/index.py b/titles/pokken/index.py index a8b13f2..ccf21de 100644 --- a/titles/pokken/index.py +++ b/titles/pokken/index.py @@ -58,6 +58,18 @@ class PokkenServlet(resource.Resource): return (True, f"https://{game_cfg.server.hostname}:{game_cfg.server.port}/{game_code}/$v/", f"{game_cfg.server.hostname}:{game_cfg.server.port}/") return (True, f"https://{game_cfg.server.hostname}/{game_code}/$v/", f"{game_cfg.server.hostname}/") + + @classmethod + def get_mucha_info(cls, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str, str]: + game_cfg = PokkenConfig() + + if path.exists(f"{cfg_dir}/{PokkenConstants.CONFIG_NAME}"): + game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{PokkenConstants.CONFIG_NAME}"))) + + if not game_cfg.server.enable: + return (False, "", "") + + return (True, "PKFN") def setup(self): """ From a7a830c6b70973a1a43267bcf6f02136ef360d27 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sat, 4 Mar 2023 22:48:12 -0500 Subject: [PATCH 14/63] mucha: remove default values from response classes --- core/mucha.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/mucha.py b/core/mucha.py index 6cabd59..bbfe045 100644 --- a/core/mucha.py +++ b/core/mucha.py @@ -131,7 +131,7 @@ class MuchaAuthRequest(): self.allToken = "" if "allToken" not in request else request["allToken"] class MuchaAuthResponse(): - def __init__(self, mucha_url: str = "localhost") -> None: + def __init__(self, mucha_url: str) -> None: self.RESULTS = "001" self.AUTH_INTERVAL = "86400" self.SERVER_TIME = datetime.strftime(datetime.now(), "%Y%m%d%H%M") @@ -184,7 +184,7 @@ class MuchaUpdateRequest(): self.storeRouterIp = "" if "storeRouterIp" not in request else request["storeRouterIp"] class MuchaUpdateResponse(): - def __init__(self, game_ver: str = "PKFN0JPN01.01", mucha_url: str = "localhost") -> None: + def __init__(self, game_ver: str, mucha_url: str) -> None: self.RESULTS = "001" self.UPDATE_VER_1 = game_ver self.UPDATE_URL_1 = f"https://{mucha_url}/updUrl1/" From ff6ef16b89a96609136bff5bc4c28b638004fd6f Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sat, 4 Mar 2023 22:52:17 -0500 Subject: [PATCH 15/63] mucha: small cleanup with a oneliner --- core/mucha.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/core/mucha.py b/core/mucha.py index bbfe045..b85b535 100644 --- a/core/mucha.py +++ b/core/mucha.py @@ -56,11 +56,8 @@ class MuchaServlet: return b"" # TODO: Figure out why the S/N is the way it is. - - if self.config.server.is_develop: - resp = MuchaAuthResponse(mucha_url=f"{self.config.mucha.hostname}:{self.config.mucha.port}") - else: - resp = MuchaAuthResponse(mucha_url=f"{self.config.mucha.hostname}") + + resp = MuchaAuthResponse(f"{self.config.mucha.hostname}{':' + self.config.mucha.port if self.config.server.is_develop else ''}") self.logger.debug(f"Mucha response {vars(resp)}") @@ -80,10 +77,7 @@ class MuchaServlet: self.logger.warn(f"Unknown gameCd {req.gameCd}") return b"" - if self.config.server.is_develop: - resp = MuchaUpdateResponse(req.gameVer, f"{self.config.mucha.hostname}:{self.config.mucha.port}") - else: - resp = MuchaUpdateResponse(req.gameVer, f"{self.config.mucha.hostname}") + resp = MuchaUpdateResponse(req.gameVer, f"{self.config.mucha.hostname}{':' + self.config.mucha.port if self.config.server.is_develop else ''}") self.logger.debug(f"Mucha response {vars(resp)}") From f25152a6bf947e59603c1c52d57745cda0cc9d97 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sat, 4 Mar 2023 23:00:21 -0500 Subject: [PATCH 16/63] add RIP contributing guide --- contributing.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 contributing.md diff --git a/contributing.md b/contributing.md new file mode 100644 index 0000000..5397f70 --- /dev/null +++ b/contributing.md @@ -0,0 +1,8 @@ +# Contributing to ARTEMiS +If you would like to contribute to artemis, either by adding features, games, or fixing bugs, you can do so by forking the repo and submitting a pull request [here](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls). Please make sure, if you're submitting a PR for a game or game version, that you're following the n-0/y-1 guidelines, or it will be rejected. + +## Adding games +Guide WIP + +## Adding game versions +Guide WIP \ No newline at end of file From 59b2401a672c33db61baf59e0952d161c6b9a98d Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sun, 5 Mar 2023 09:43:05 -0500 Subject: [PATCH 17/63] mucha: add updatecheck response stub --- core/mucha.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/mucha.py b/core/mucha.py index b85b535..cdfadd1 100644 --- a/core/mucha.py +++ b/core/mucha.py @@ -77,7 +77,8 @@ class MuchaServlet: self.logger.warn(f"Unknown gameCd {req.gameCd}") return b"" - resp = MuchaUpdateResponse(req.gameVer, f"{self.config.mucha.hostname}{':' + self.config.mucha.port if self.config.server.is_develop else ''}") + #resp = MuchaUpdateResponse(req.gameVer, f"{self.config.mucha.hostname}{':' + self.config.mucha.port if self.config.server.is_develop else ''}") + resp = MuchaUpdateResponseStub(req.gameVer) self.logger.debug(f"Mucha response {vars(resp)}") @@ -190,3 +191,8 @@ class MuchaUpdateResponse(): self.COM_SIZE_1 = "0" self.COM_TIME_1 = "0" self.LAN_INFO_SIZE_1 = "0" + +class MuchaUpdateResponseStub(): + def __init__(self, game_ver: str) -> None: + self.RESULTS = "001" + self.UPDATE_VER_1 = game_ver From 82b159e5b17a52c9a491d0c6a07e50b8de3f122a Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sun, 5 Mar 2023 09:45:34 -0500 Subject: [PATCH 18/63] MuchaUpdateResponse: add missing values --- core/mucha.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/mucha.py b/core/mucha.py index cdfadd1..370042f 100644 --- a/core/mucha.py +++ b/core/mucha.py @@ -191,6 +191,8 @@ class MuchaUpdateResponse(): self.COM_SIZE_1 = "0" self.COM_TIME_1 = "0" self.LAN_INFO_SIZE_1 = "0" + self.USER_ID = "" + self.PASSWORD = "" class MuchaUpdateResponseStub(): def __init__(self, game_ver: str) -> None: From 36054ebb66978ee06a329357ec006ae512de0d64 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sun, 5 Mar 2023 11:42:03 -0500 Subject: [PATCH 19/63] mucha: add missing boardath values --- core/mucha.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/mucha.py b/core/mucha.py index 370042f..1f0312a 100644 --- a/core/mucha.py +++ b/core/mucha.py @@ -55,7 +55,7 @@ class MuchaServlet: self.logger.warn(f"Unknown gameCd {req.gameCd}") return b"" - # TODO: Figure out why the S/N is the way it is. + # TODO: Decrypt S/N resp = MuchaAuthResponse(f"{self.config.mucha.hostname}{':' + self.config.mucha.port if self.config.server.is_develop else ''}") @@ -77,7 +77,6 @@ class MuchaServlet: self.logger.warn(f"Unknown gameCd {req.gameCd}") return b"" - #resp = MuchaUpdateResponse(req.gameVer, f"{self.config.mucha.hostname}{':' + self.config.mucha.port if self.config.server.is_develop else ''}") resp = MuchaUpdateResponseStub(req.gameVer) self.logger.debug(f"Mucha response {vars(resp)}") @@ -113,12 +112,13 @@ class MuchaServlet: class MuchaAuthRequest(): def __init__(self, request: Dict) -> None: - self.gameVer = "" if "gameVer" not in request else request["gameVer"] - self.sendDate = "" if "sendDate" not in request else datetime.strptime(request["sendDate"], "%Y%m%d") + self.gameVer = "" if "gameVer" not in request else request["gameVer"] # gameCd + boardType + countryCd + version + self.sendDate = "" if "sendDate" not in request else request["sendDate"] # %Y%m%d self.serialNum = "" if "serialNum" not in request else request["serialNum"] self.gameCd = "" if "gameCd" not in request else request["gameCd"] self.boardType = "" if "boardType" not in request else request["boardType"] self.boardId = "" if "boardId" not in request else request["boardId"] + self.mac = "" if "mac" not in request else request["mac"] self.placeId = "" if "placeId" not in request else request["placeId"] self.storeRouterIp = "" if "storeRouterIp" not in request else request["storeRouterIp"] self.countryCd = "" if "countryCd" not in request else request["countryCd"] From 74f3ab7c3f4e9ea63fa4ca0cb097c2e3a8e49b33 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Sun, 5 Mar 2023 23:54:13 +0100 Subject: [PATCH 20/63] cm: added support for 1.36, fixed importer - Added support for Card Maker 1.36.xx - Added cards importer to ONGEKI importer - Added 4 new 1.36 gachas (requires importing them from opt files) - Fixed version for Card Maker opt importer --- example_config/ongeki.yaml | 7 +- readme.md | 4 + titles/cm/__init__.py | 5 +- titles/cm/cm136.py | 50 + titles/cm/cm_data/MU3/static_cards.csv | 1616 ------------------ titles/cm/cm_data/MU3/static_gacha_cards.csv | 165 ++ titles/cm/cm_data/MU3/static_gachas.csv | 127 +- titles/cm/const.py | 3 +- titles/cm/index.py | 26 +- titles/cm/read.py | 127 +- titles/ongeki/__init__.py | 2 +- titles/ongeki/bright.py | 24 +- titles/ongeki/brightmemory.py | 26 +- titles/ongeki/read.py | 104 +- titles/ongeki/schema/profile.py | 9 +- titles/ongeki/schema/static.py | 2 +- 16 files changed, 453 insertions(+), 1844 deletions(-) create mode 100644 titles/cm/cm136.py delete mode 100644 titles/cm/cm_data/MU3/static_cards.csv diff --git a/example_config/ongeki.yaml b/example_config/ongeki.yaml index 6ebd18a..3db7098 100644 --- a/example_config/ongeki.yaml +++ b/example_config/ongeki.yaml @@ -23,4 +23,9 @@ gachas: - 1089 - 1104 - 1111 - - 1135 \ No newline at end of file + - 1135 + # can be used for Card Maker 1.35 and up, else will be ignored + - 1149 + - 1156 + - 1163 + - 1164 diff --git a/readme.md b/readme.md index 24440fc..d64cede 100644 --- a/readme.md +++ b/readme.md @@ -15,6 +15,10 @@ Games listed below have been tested and confirmed working. Only game versions ol + Hatsune Miku Arcade + All versions ++ Card Maker + + 1.34.xx + + 1.36.xx + + Ongeki + All versions up to Bright Memory diff --git a/titles/cm/__init__.py b/titles/cm/__init__.py index 040bec7..7c03605 100644 --- a/titles/cm/__init__.py +++ b/titles/cm/__init__.py @@ -10,6 +10,7 @@ include_protocol = True title_secure = False game_codes = [CardMakerConstants.GAME_CODE] trailing_slash = True -use_default_host = True +use_default_host = False +host = "" -current_schema_version = 1 +current_schema_version = 1 \ No newline at end of file diff --git a/titles/cm/cm136.py b/titles/cm/cm136.py new file mode 100644 index 0000000..2859c24 --- /dev/null +++ b/titles/cm/cm136.py @@ -0,0 +1,50 @@ +from datetime import date, datetime, timedelta +from typing import Any, Dict, List +import json +import logging +from enum import Enum + +from core.config import CoreConfig +from core.data.cache import cached +from titles.cm.base import CardMakerBase +from titles.cm.const import CardMakerConstants +from titles.cm.config import CardMakerConfig + + +class CardMaker136(CardMakerBase): + def __init__(self, core_cfg: CoreConfig, game_cfg: CardMakerConfig) -> None: + super().__init__(core_cfg, game_cfg) + self.version = CardMakerConstants.VER_CARD_MAKER_136 + + def handle_get_game_connect_api_request(self, data: Dict) -> Dict: + uri = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}" + + # CHUNITHM = 0, maimai = 1, ONGEKI = 2 + return { + "length": 3, + "gameConnectList": [ + { + "modelKind": 0, + "type": 1, + "titleUri": f"{uri}/SDHD/205/" + }, + { + "modelKind": 1, + "type": 1, + "titleUri": f"{uri}/SDEZ/125/" + }, + { + "modelKind": 2, + "type": 1, + "titleUri": f"{uri}/SDDT/135/" + } + ] + } + + 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"]["ongekiCmVersion"] = "1.35.04" + ret["gameSetting"]["chuniCmVersion"] = "2.05.00" + ret["gameSetting"]["maimaiCmVersion"] = "1.25.00" + return ret diff --git a/titles/cm/cm_data/MU3/static_cards.csv b/titles/cm/cm_data/MU3/static_cards.csv deleted file mode 100644 index 53e6e56..0000000 --- a/titles/cm/cm_data/MU3/static_cards.csv +++ /dev/null @@ -1,1616 +0,0 @@ -"version","cardId","name","charaId","nickName","school","attribute","gakunen","rarity","levelParam","skillId","choKaikaSkillId","cardNumber" -0,1,"【SR】あかニャン[ボス専用]",1,"ボス専用","-","Fire","-",2,"55,227,242,257,272,287,0,0,0,287",0,0,"[O.N.G.E.K.I.]*.**-****" -0,2,"【SR】あおニャン[ボス専用]",2,"ボス専用","-","Aqua","-",2,"55,227,242,257,272,287,0,0,0,287",0,0,"[O.N.G.E.K.I.]*.**-****" -0,3,"【SR】みどニャン[ボス専用]",3,"ボス専用","-","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",0,0,"[O.N.G.E.K.I.]*.**-****" -0,100001,"【N】星咲 あかり",1000,"","奏坂学園","Fire","高校2年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"[O.N.G.E.K.I.]1.00-0001" -0,100002,"【R】星咲 あかり[シューター・ドレス]",1000,"シューター・ドレス","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100001,100042,"[O.N.G.E.K.I.]1.00-0005" -0,100003,"【R】星咲 あかり[体操着]",1000,"体操着","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.00-0004" -0,100004,"【R】星咲 あかり[私服]",1000,"私服","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100012,100053,"[O.N.G.E.K.I.]1.00-0003" -0,100005,"【SR】星咲 あかり[目指せ! いっぱいのエール!]",1000,"目指せ! いっぱいのエール!","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100008,100049,"[O.N.G.E.K.I.]1.00-0008" -0,100006,"【SR】星咲 あかり[ようこそ!奏坂へ!]",1000,"ようこそ!奏坂へ!","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100026,100067,"[O.N.G.E.K.I.]1.00-0007" -0,100007,"【SR】星咲 あかり[ドジっ子トラブル]",1000,"ドジっ子トラブル","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100032,100073,"[O.N.G.E.K.I.]1.00-0104" -0,100008,"【SSR】星咲 あかり[揺るぎない想い]",1000,"揺るぎない想い","奏坂学園","Fire","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",100034,100075,"[O.N.G.E.K.I.]1.00-0009" -0,100009,"【SSR】星咲 あかり[デイドリーム・フェアリーズ]",1000,"デイドリーム・フェアリーズ","奏坂学園","Fire","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",100038,100079,"[O.N.G.E.K.I.]1.00-0010" -0,100010,"【N】藤沢 柚子",1001,"","奏坂学園","Leaf","高校2年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"[O.N.G.E.K.I.]1.00-0011" -0,100011,"【R】藤沢 柚子[シューター・ドレス]",1001,"シューター・ドレス","奏坂学園","Leaf","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100002,100043,"[O.N.G.E.K.I.]1.00-0015" -0,100012,"【R】藤沢 柚子[体操着]",1001,"体操着","奏坂学園","Leaf","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.00-0014" -0,100013,"【R】藤沢 柚子[私服]",1001,"私服","奏坂学園","Leaf","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100012,100053,"[O.N.G.E.K.I.]1.00-0013" -0,100014,"【SR】藤沢 柚子[目指せ! いっぱいのエール!]",1001,"目指せ! いっぱいのエール!","奏坂学園","Leaf","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100008,100049,"[O.N.G.E.K.I.]1.00-0018" -0,100015,"【SR】藤沢 柚子[笑顔のお裾分け]",1001,"笑顔のお裾分け","奏坂学園","Leaf","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100027,100068,"[O.N.G.E.K.I.]1.00-0017" -0,100016,"【SR】藤沢 柚子[お着替えターイム!]",1001,"お着替えターイム!","奏坂学園","Leaf","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100032,100073,"[O.N.G.E.K.I.]1.00-0105" -0,100017,"【SSR】藤沢 柚子[ハッピー☆マジック]",1001,"ハッピー☆マジック","奏坂学園","Leaf","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",100034,100075,"[O.N.G.E.K.I.]1.00-0019" -0,100018,"【SSR】藤沢 柚子[デイドリーム・フェアリーズ]",1001,"デイドリーム・フェアリーズ","奏坂学園","Leaf","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",100039,100080,"[O.N.G.E.K.I.]1.00-0020" -0,100019,"【N】三角 葵",1002,"","奏坂学園","Aqua","高校2年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"[O.N.G.E.K.I.]1.00-0021" -0,100020,"【R】三角 葵[シューター・ドレス]",1002,"シューター・ドレス","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100003,100044,"[O.N.G.E.K.I.]1.00-0025" -0,100021,"【R】三角 葵[体操着]",1002,"体操着","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.00-0024" -0,100022,"【R】三角 葵[私服]",1002,"私服","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100012,100053,"[O.N.G.E.K.I.]1.00-0023" -0,100023,"【SR】三角 葵[目指せ! いっぱいのエール!]",1002,"目指せ! いっぱいのエール!","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100008,100049,"[O.N.G.E.K.I.]1.00-0028" -0,100024,"【SR】三角 葵[ね、簡単でしょ?]",1002,"ね、簡単でしょ?","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100028,100069,"[O.N.G.E.K.I.]1.00-0027" -0,100025,"【SR】三角 葵[かちこちストレッチ]",1002,"かちこちストレッチ","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100032,100073,"[O.N.G.E.K.I.]1.00-0106" -0,100026,"【SSR】三角 葵[ワールズエンド・ブルー]",1002,"ワールズエンド・ブルー","奏坂学園","Aqua","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",100034,100075,"[O.N.G.E.K.I.]1.00-0029" -0,100027,"【SSR】三角 葵[デイドリーム・フェアリーズ]",1002,"デイドリーム・フェアリーズ","奏坂学園","Aqua","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",100040,100081,"[O.N.G.E.K.I.]1.00-0030" -0,100028,"【N】高瀬 梨緒",1003,"","奏坂学園","Aqua","高校2年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"[O.N.G.E.K.I.]1.00-0031" -0,100029,"【R】高瀬 梨緒[シューター・ドレス]",1003,"シューター・ドレス","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.00-0034" -0,100030,"【R】高瀬 梨緒[体操着]",1003,"体操着","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-0033" -0,100031,"【SR】高瀬 梨緒[自称・超絶最強、登場!]",1003,"自称・超絶最強、登場!","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100015,100056,"[O.N.G.E.K.I.]1.00-0037" -0,100032,"【SR】高瀬 梨緒[カフェ de スクランブル]",1003,"カフェ de スクランブル","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100021,100062,"[O.N.G.E.K.I.]1.00-0038" -0,100033,"【SR】高瀬 梨緒[素直になれないお年頃]",1003,"素直になれないお年頃","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100029,100070,"[O.N.G.E.K.I.]1.00-0036" -0,100034,"【SSR】高瀬 梨緒[ジェットストライクリッパー]",1003,"ジェットストライクリッパー","奏坂学園","Aqua","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",100108,100109,"[O.N.G.E.K.I.]1.00-0039" -0,100035,"【N】結城 莉玖",1004,"","奏坂学園","Fire","高校1年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"[O.N.G.E.K.I.]1.00-0040" -0,100036,"【R】結城 莉玖[シューター・ドレス]",1004,"シューター・ドレス","奏坂学園","Fire","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.00-0043" -0,100037,"【R】結城 莉玖[体操着]",1004,"体操着","奏坂学園","Fire","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-0042" -0,100038,"【SR】結城 莉玖[嘘とがおー!とかたき討ち]",1004,"嘘とがおー!とかたき討ち","奏坂学園","Fire","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",100015,100056,"[O.N.G.E.K.I.]1.00-0046" -0,100039,"【SR】結城 莉玖[カフェ de スクランブル]",1004,"カフェ de スクランブル","奏坂学園","Fire","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",100019,100060,"[O.N.G.E.K.I.]1.00-0047" -0,100040,"【SR】結城 莉玖[未来のロックスター]",1004,"未来のロックスター","奏坂学園","Fire","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",100029,100070,"[O.N.G.E.K.I.]1.00-0045" -0,100041,"【SSR】結城 莉玖[リトルギターモンスター]",1004,"リトルギターモンスター","奏坂学園","Fire","高校1年生",3,"60,257,280,295,307,317,0,0,0,322",100104,100105,"[O.N.G.E.K.I.]1.00-0048" -0,100042,"【N】藍原 椿",1005,"","奏坂学園","Leaf","高校1年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"[O.N.G.E.K.I.]1.00-0049" -0,100043,"【R】藍原 椿[シューター・ドレス]",1005,"シューター・ドレス","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.00-0052" -0,100044,"【R】藍原 椿[体操着]",1005,"体操着","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-0051" -0,100045,"【SR】藍原 椿[嘘とがおー!とかたき討ち]",1005,"嘘とがおー!とかたき討ち","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",100015,100056,"[O.N.G.E.K.I.]1.00-0055" -0,100046,"【SR】藍原 椿[カフェ de スクランブル]",1005,"カフェ de スクランブル","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",100020,100061,"[O.N.G.E.K.I.]1.00-0056" -0,100047,"【SR】藍原 椿[夕暮れノスタルジー]",1005,"夕暮れノスタルジー","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",100029,100070,"[O.N.G.E.K.I.]1.00-0054" -0,100048,"【SSR】藍原 椿[サディスティック・スマイル]",1005,"サディスティック・スマイル","奏坂学園","Leaf","高校1年生",3,"60,257,280,295,307,317,0,0,0,322",100106,100107,"[O.N.G.E.K.I.]1.00-0057" -0,100049,"【N】桜井 春菜",1007,"","奏坂学園","Fire","高校2年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"[O.N.G.E.K.I.]1.00-0058" -0,100050,"【R】桜井 春菜[シューター・ドレス]",1007,"シューター・ドレス","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-0072" -0,100051,"【R】桜井 春菜[体操着]",1007,"体操着","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100001,100042,"[O.N.G.E.K.I.]1.00-0060" -0,100052,"【SR】桜井 春菜[ハルちゃん印のお弁当]",1007,"ハルちゃん印のお弁当","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100016,100057,"[O.N.G.E.K.I.]1.00-0073" -0,100053,"【SR】桜井 春菜[これは味見だから…!]",1007,"これは味見だから…!","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100025,100066,"[O.N.G.E.K.I.]1.00-0062" -0,100054,"【SSR】桜井 春菜[フラワリーブライド]",1007,"フラワリーブライド","奏坂学園","Fire","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]1.00-0074" -0,100055,"【N】早乙女 彩華",1006,"","奏坂学園","Aqua","高校3年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"[O.N.G.E.K.I.]1.00-0063" -0,100056,"【R】早乙女 彩華[シューター・ドレス]",1006,"シューター・ドレス","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-0075" -0,100057,"【R】早乙女 彩華[体操着]",1006,"体操着","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100003,100044,"[O.N.G.E.K.I.]1.00-0065" -0,100058,"【SR】早乙女 彩華[ハルちゃん印のお弁当]",1006,"ハルちゃん印のお弁当","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",100018,100059,"[O.N.G.E.K.I.]1.00-0076" -0,100059,"【SR】早乙女 彩華[しーっ!内緒だよ?]",1006,"しーっ!内緒だよ?","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",100025,100066,"[O.N.G.E.K.I.]1.00-0067" -0,100060,"【SSR】早乙女 彩華[爆アゲ☆バニーガール]",1006,"爆アゲ☆バニーガール","奏坂学園","Aqua","高校3年生",3,"60,257,280,295,307,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]1.00-0077" -0,100061,"【N】九條 楓",1008,"","奏坂学園","Leaf","高校3年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"[O.N.G.E.K.I.]1.00-0068" -0,100062,"【R】九條 楓[シューター・ドレス]",1008,"シューター・ドレス","奏坂学園","Leaf","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-0078" -0,100063,"【R】九條 楓[体操着]",1008,"体操着","奏坂学園","Leaf","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100002,100043,"[O.N.G.E.K.I.]1.00-0070" -0,100064,"【SR】九條 楓[波乱!副会長の通告!]",1008,"波乱!副会長の通告!","奏坂学園","Leaf","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",100017,100058,"[O.N.G.E.K.I.]1.00-0080" -0,100065,"【SR】九條 楓[雨傘を差して]",1008,"雨傘を差して","奏坂学園","Leaf","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",100025,100066,"[O.N.G.E.K.I.]1.00-0079" -0,100066,"【SSR】九條 楓[紫電一閃]",1008,"紫電一閃","奏坂学園","Leaf","高校3年生",3,"60,257,280,295,307,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]1.00-0095" -0,100067,"【N】井之原 小星",1010,"","奏坂学園","Leaf","高校1年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"[O.N.G.E.K.I.]1.00-0081" -0,100068,"【R】井之原 小星[シューター・ドレス]",1010,"シューター・ドレス","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-0084" -0,100069,"【R】井之原 小星[体操着]",1010,"体操着","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100007,100048,"[O.N.G.E.K.I.]1.00-0083" -0,100070,"【SR】井之原 小星[小星と咲姫が あらわれた!]",1010,"小星と咲姫が あらわれた!","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",100023,100064,"[O.N.G.E.K.I.]1.00-0087" -0,100071,"【SR】井之原 小星[ぐーたらゲーマー]",1010,"ぐーたらゲーマー","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",100031,100072,"[O.N.G.E.K.I.]1.00-0086" -0,100072,"【SSR】井之原 小星[お任せオートプレイ]",1010,"お任せオートプレイ","奏坂学園","Leaf","高校1年生",3,"60,257,280,295,307,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]1.00-0107" -0,100073,"【N】柏木 咲姫",1009,"","奏坂学園","Aqua","高校3年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"[O.N.G.E.K.I.]1.00-0088" -0,100074,"【R】柏木 咲姫[シューター・ドレス]",1009,"シューター・ドレス","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100007,100048,"[O.N.G.E.K.I.]1.00-0091" -0,100075,"【R】柏木 咲姫[体操着]",1009,"体操着","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-0090" -0,100076,"【SR】柏木 咲姫[小星と咲姫が あらわれた!]",1009,"小星と咲姫が あらわれた!","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",100024,100065,"[O.N.G.E.K.I.]1.00-0094" -0,100077,"【SR】柏木 咲姫[お姫様は隙だらけ]",1009,"お姫様は隙だらけ","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",100018,100059,"[O.N.G.E.K.I.]1.00-0093" -0,100078,"【SSR】柏木 咲姫[清く、凛々しく、美しく]",1009,"清く、凛々しく、美しく","奏坂学園","Aqua","高校3年生",3,"60,257,280,295,307,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]1.00-0096" -0,100079,"【N】逢坂 茜",1011,"","奏坂学園","Fire","高校3年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"[O.N.G.E.K.I.]1.00-0097" -0,100080,"【R】逢坂 茜[シューター・ドレス]",1011,"シューター・ドレス","奏坂学園","Fire","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100006,100047,"[O.N.G.E.K.I.]1.00-0100" -0,100081,"【R】逢坂 茜[体操着]",1011,"体操着","奏坂学園","Fire","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100136,100137,"[O.N.G.E.K.I.]1.00-0099" -0,100082,"【SR】逢坂 茜[世界征服のお誘い]",1011,"世界征服のお誘い","奏坂学園","Fire","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",100022,100063,"[O.N.G.E.K.I.]1.00-0103" -0,100083,"【SR】逢坂 茜[はちゃめちゃ生徒会長]",1011,"はちゃめちゃ生徒会長","奏坂学園","Fire","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",100030,100071,"[O.N.G.E.K.I.]1.00-0102" -0,100084,"【SSR】逢坂 茜[ショット&デストロイ]",1011,"ショット&デストロイ","奏坂学園","Fire","高校3年生",3,"60,257,280,295,307,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]1.00-0108" -0,100085,"【N】珠洲島 有栖",1012,"","奏坂学園","Aqua","高校1年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"[O.N.G.E.K.I.]1.00-0109" -0,100086,"【R】珠洲島 有栖[シューター・ドレス]",1012,"シューター・ドレス","奏坂学園","Aqua","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100008,100049,"[O.N.G.E.K.I.]1.00-0112" -0,100087,"【R】珠洲島 有栖[体操着]",1012,"体操着","奏坂学園","Aqua","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100014,100055,"[O.N.G.E.K.I.]1.00-0111" -0,100088,"【SR】珠洲島 有栖[黒幕はぬいぐるみ?]",1012,"黒幕はぬいぐるみ?","奏坂学園","Aqua","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",100025,100066,"[O.N.G.E.K.I.]1.00-0115" -0,100089,"【SR】珠洲島 有栖[優雅なティータイム]",1012,"優雅なティータイム","奏坂学園","Aqua","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",100033,100074,"[O.N.G.E.K.I.]1.00-0114" -1,100090,"【SSR】珠洲島 有栖[もふもふシューター]",1012,"もふもふシューター","奏坂学園","Aqua","高校1年生",3,"60,257,280,295,307,317,0,0,0,322",100037,100078,"[O.N.G.E.K.I.]1.05-0018" -0,100091,"【R】霧雨 魔理沙[東方Project]",2001,"東方Project","東方Project","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-E-0019" -0,100092,"【SR】霧雨 魔理沙[普通の魔法使い]",2001,"普通の魔法使い","東方Project","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.00-E-0020" -0,100093,"【SSR】霧雨 魔理沙[恋符「マスタースパーク」]",2001,"恋符「マスタースパーク」","東方Project","Leaf","-",3,"55,252,275,290,302,312,0,0,0,317",100144,100145,"[O.N.G.E.K.I.]1.00-E-0046" -0,100094,"【R】博麗 霊夢[東方Project]",2000,"東方Project","東方Project","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100140,100141,"[O.N.G.E.K.I.]1.00-E-0017" -0,100095,"【SR】博麗 霊夢[楽園の素敵な巫女]",2000,"楽園の素敵な巫女","東方Project","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.00-E-0018" -0,100096,"【SSR】博麗 霊夢[霊符「夢想封印」]",2000,"霊符「夢想封印」","東方Project","Fire","-",3,"55,252,275,290,302,312,0,0,0,317",100138,100139,"[O.N.G.E.K.I.]1.00-E-0045" -0,100097,"【R】十六夜 咲夜[東方紅魔郷]",2002,"東方紅魔郷","東方紅魔郷","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-E-0026" -0,100098,"【SR】十六夜 咲夜[完全で瀟洒な従者]",2002,"完全で瀟洒な従者","東方紅魔郷","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100028,100069,"[O.N.G.E.K.I.]1.00-E-0027" -0,100100,"【R】レミリア・スカーレット[東方紅魔郷]",2003,"東方紅魔郷","東方紅魔郷","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-E-0028" -0,100101,"【SR】レミリア・スカーレット[永遠に紅い幼き月]",2003,"永遠に紅い幼き月","東方紅魔郷","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.00-E-0029" -0,100102,"【SSR】レミリア・スカーレット[神槍「スピア・ザ・グングニル」]",2003,"神槍「スピア・ザ・グングニル」","東方紅魔郷","Fire","-",3,"55,252,275,290,302,312,0,0,0,317",100033,100074,"[O.N.G.E.K.I.]1.00-E-0032" -0,100103,"【R】フランドール・スカーレット[東方紅魔郷]",2004,"東方紅魔郷","東方紅魔郷","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-E-0030" -0,100104,"【SR】フランドール・スカーレット[悪魔の妹]",2004,"悪魔の妹","東方紅魔郷","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.00-E-0031" -0,100105,"【SSR】フランドール・スカーレット[禁忌「レーヴァテイン」]",2004,"禁忌「レーヴァテイン」","東方紅魔郷","Fire","-",3,"55,252,275,290,302,312,0,0,0,317",100036,100077,"[O.N.G.E.K.I.]1.00-E-0033" -0,100106,"【R】紅 美鈴[東方紅魔郷]",2005,"東方紅魔郷","東方紅魔郷","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-E-0022" -0,100107,"【SR】紅 美鈴[華人小娘]",2005,"華人小娘","東方紅魔郷","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100016,100057,"[O.N.G.E.K.I.]1.00-E-0023" -0,100109,"【R】パチュリー・ノーレッジ[東方紅魔郷]",2006,"東方紅魔郷","東方紅魔郷","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-E-0024" -0,100110,"【SR】パチュリー・ノーレッジ[動かない大図書館]",2006,"動かない大図書館","東方紅魔郷","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100027,100068,"[O.N.G.E.K.I.]1.00-E-0025" -1,100112,"【R】チルノ[東方Project]",2007,"東方Project","東方Project","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.05-E-0062" -1,100113,"【SR】チルノ[湖上の氷精]",2007,"湖上の氷精","東方Project","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",105026,105027,"[O.N.G.E.K.I.]1.05-E-0063" -0,100117,"【R】めぐみん[アークウィザード]",11001,"アークウィザード","この素晴らしい世界に祝福を!2","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-E-0034" -0,100118,"【SR】めぐみん[水辺にて]",11001,"水辺にて","この素晴らしい世界に祝福を!2","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100082,100083,"[O.N.G.E.K.I.]1.00-E-0035" -0,100119,"【SR】めぐみん[我が名はめぐみん]",11001,"我が名はめぐみん","この素晴らしい世界に祝福を!2","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.00-E-0040" -0,100120,"【SSR】めぐみん[紅魔族の休息]",11001,"紅魔族の休息","この素晴らしい世界に祝福を!2","Fire","-",3,"55,252,275,290,302,312,0,0,0,317",100084,100085,"[O.N.G.E.K.I.]1.00-E-0044" -0,100121,"【R】アクア[アークプリースト]",11002,"アークプリースト","この素晴らしい世界に祝福を!2","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.00-E-0036" -0,100122,"【SR】アクア[セイクリッド飲みくらべ]",11002,"セイクリッド飲みくらべ","この素晴らしい世界に祝福を!2","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100142,100143,"[O.N.G.E.K.I.]1.00-E-0037" -0,100123,"【SR】アクア[めぐみんのいたずら]",11002,"めぐみんのいたずら","この素晴らしい世界に祝福を!2","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100028,100069,"[O.N.G.E.K.I.]1.00-E-0041" -0,100124,"【R】ダクネス[クルセイダー]",11003,"クルセイダー","この素晴らしい世界に祝福を!2","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100002,100043,"[O.N.G.E.K.I.]1.00-E-0038" -0,100125,"【SR】ダクネス[爆風に吹かれて]",11003,"爆風に吹かれて","この素晴らしい世界に祝福を!2","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100017,100058,"[O.N.G.E.K.I.]1.00-E-0039" -0,100126,"【SR】ダクネス[露天風呂]",11003,"露天風呂","この素晴らしい世界に祝福を!2","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100027,100068,"[O.N.G.E.K.I.]1.00-E-0042" -0,100127,"【R】戸山香澄[ステージ]",18001,"ステージ","バンドリ! ガールズバンドパーティ!","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-E-0001" -0,100128,"【SR】戸山香澄[約束のキャンディ]",18001,"約束のキャンディ","バンドリ! ガールズバンドパーティ!","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.00-E-0003" -0,100129,"【SR】戸山香澄[最高のステージに!]",18001,"最高のステージに!","バンドリ! ガールズバンドパーティ!","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100015,100056,"[O.N.G.E.K.I.]1.00-E-0047" -0,100130,"【SR】戸山香澄[気合の円陣]",18001,"気合の円陣","バンドリ! ガールズバンドパーティ!","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100026,100067,"[O.N.G.E.K.I.]1.00-E-0048" -0,100131,"【SSR】戸山香澄[ガールズバンドパーティ!]",18001,"ガールズバンドパーティ!","バンドリ! ガールズバンドパーティ!","Fire","-",3,"55,252,275,290,302,312,0,0,0,317",100033,100074,"[O.N.G.E.K.I.]1.00-E-0016" -0,100132,"【R】美竹蘭[ステージ]",18002,"ステージ","バンドリ! ガールズバンドパーティ!","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-E-0004" -0,100133,"【SR】美竹蘭[断ち切った迷い]",18002,"断ち切った迷い","バンドリ! ガールズバンドパーティ!","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100016,100057,"[O.N.G.E.K.I.]1.00-E-0006" -0,100134,"【SR】美竹蘭[等身大の夜空]",18002,"等身大の夜空","バンドリ! ガールズバンドパーティ!","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100019,100060,"[O.N.G.E.K.I.]1.00-E-0049" -0,100135,"【SR】美竹蘭[夕焼けの先]",18002,"夕焼けの先","バンドリ! ガールズバンドパーティ!","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.00-E-0050" -0,100136,"【R】丸山彩[ステージ]",18003,"ステージ","バンドリ! ガールズバンドパーティ!","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-E-0007" -0,100137,"【SR】丸山彩[必殺アイドルポーズ☆]",18003,"必殺アイドルポーズ☆","バンドリ! ガールズバンドパーティ!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.00-E-0009" -0,100138,"【SR】丸山彩[私達のポスター]",18003,"私達のポスター","バンドリ! ガールズバンドパーティ!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100027,100068,"[O.N.G.E.K.I.]1.00-E-0051" -0,100139,"【SR】丸山彩[イベント、来てください!]",18003,"イベント、来てください!","バンドリ! ガールズバンドパーティ!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.00-E-0052" -0,100140,"【R】湊友希那[ステージ]",18004,"ステージ","バンドリ! ガールズバンドパーティ!","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-E-0010" -0,100141,"【SR】湊友希那[歌姫の覚悟]",18004,"歌姫の覚悟","バンドリ! ガールズバンドパーティ!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.00-E-0012" -0,100142,"【SR】湊友希那[秋晴れ、その先に]",18004,"秋晴れ、その先に","バンドリ! ガールズバンドパーティ!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100018,100059,"[O.N.G.E.K.I.]1.00-E-0053" -0,100143,"【SR】湊友希那[見守る目線]",18004,"見守る目線","バンドリ! ガールズバンドパーティ!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100028,100069,"[O.N.G.E.K.I.]1.00-E-0054" -0,100144,"【R】弦巻こころ[ステージ]",18005,"ステージ","バンドリ! ガールズバンドパーティ!","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-E-0013" -0,100145,"【SR】弦巻こころ[無敵のヒーロー]",18005,"無敵のヒーロー","バンドリ! ガールズバンドパーティ!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100027,100068,"[O.N.G.E.K.I.]1.00-E-0015" -0,100146,"【SR】弦巻こころ[見習い魔女]",18005,"見習い魔女","バンドリ! ガールズバンドパーティ!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100017,100058,"[O.N.G.E.K.I.]1.00-E-0055" -0,100147,"【SR】弦巻こころ[みんなが花マル1等賞!]",18005,"みんなが花マル1等賞!","バンドリ! ガールズバンドパーティ!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.00-E-0056" -1,100158,"【R】初音ミク[バーチャル・シンガー]",5000,"バーチャル・シンガー","バーチャル・シンガー","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.05-E-0001" -1,100159,"【SR】初音ミク[電子の歌姫]",5000,"電子の歌姫","バーチャル・シンガー","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100021,100062,"[O.N.G.E.K.I.]1.05-E-0002" -1,100160,"【SR】初音ミク[V4X]",5000,"V4X","バーチャル・シンガー","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.05-E-0023" -1,100161,"【R】巡音ルカ[バーチャル・シンガー]",5001,"バーチャル・シンガー","バーチャル・シンガー","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.05-E-0004" -1,100162,"【SR】巡音ルカ[クール&ハスキー]",5001,"クール&ハスキー","バーチャル・シンガー","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100019,100060,"[O.N.G.E.K.I.]1.05-E-0022" -1,100163,"【SR】巡音ルカ[V4X]",5001,"V4X","バーチャル・シンガー","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.05-E-0051" -1,100164,"【R】鏡音リン[バーチャル・シンガー]",5002,"バーチャル・シンガー","バーチャル・シンガー","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.05-E-0003" -1,100165,"【SR】鏡音リン[パワフル&チャーミング]",5002,"パワフル&チャーミング","バーチャル・シンガー","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100020,100061,"[O.N.G.E.K.I.]1.05-E-0021" -1,100166,"【SR】鏡音リン[V4X]",5002,"V4X","バーチャル・シンガー","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.05-E-0038" -0,100167,"【SSR】めぐみん[爆裂魔法]",11001,"爆裂魔法","この素晴らしい世界に祝福を!2","Fire","-",3,"55,252,275,290,302,312,0,0,0,317",100033,100074,"[O.N.G.E.K.I.]1.00-E-0043" -0,100168,"【R】戸山香澄[カラフルポッピン!]",18001,"カラフルポッピン!","バンドリ! ガールズバンドパーティ!","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100012,100053,"[O.N.G.E.K.I.]1.00-E-0002" -0,100169,"【R】美竹蘭[グロウアップロック]",18002,"グロウアップロック","バンドリ! ガールズバンドパーティ!","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100140,100141,"[O.N.G.E.K.I.]1.00-E-0005" -0,100170,"【R】丸山彩[煌めくステージへ]",18003,"煌めくステージへ","バンドリ! ガールズバンドパーティ!","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100012,100053,"[O.N.G.E.K.I.]1.00-E-0008" -0,100171,"【R】湊友希那[重なり合う青薔薇]",18004,"重なり合う青薔薇","バンドリ! ガールズバンドパーティ!","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100012,100053,"[O.N.G.E.K.I.]1.00-E-0011" -0,100172,"【R】弦巻こころ[笑顔のマジック]",18005,"笑顔のマジック","バンドリ! ガールズバンドパーティ!","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100002,100043,"[O.N.G.E.K.I.]1.00-E-0014" -0,100173,"【R】星咲 あかり[表情:笑顔]",1000,"表情:笑顔","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-0002" -0,100174,"【R】藤沢 柚子[表情:むすぅ]",1001,"表情:むすぅ","奏坂学園","Leaf","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-0012" -0,100175,"【R】三角 葵[表情:恥じらい]",1002,"表情:恥じらい","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-0022" -0,100176,"【R】高瀬 梨緒[表情:むかっ]",1003,"表情:むかっ","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100003,100044,"[O.N.G.E.K.I.]1.00-0032" -0,100177,"【R】結城 莉玖[表情:ドヤ]",1004,"表情:ドヤ","奏坂学園","Fire","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100001,100042,"[O.N.G.E.K.I.]1.00-0041" -0,100178,"【R】藍原 椿[表情:びっくり]",1005,"表情:びっくり","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100002,100043,"[O.N.G.E.K.I.]1.00-0050" -0,100179,"【R】早乙女 彩華[表情:ウィンク]",1006,"表情:ウィンク","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.00-0064" -0,100180,"【R】桜井 春菜[表情:笑顔]",1007,"表情:笑顔","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.00-0059" -0,100181,"【R】九條 楓[表情:剣幕]",1008,"表情:剣幕","奏坂学園","Leaf","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.00-0069" -0,100182,"【R】柏木 咲姫[表情:恥じらい]",1009,"表情:恥じらい","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100009,100050,"[O.N.G.E.K.I.]1.00-0089" -0,100183,"【R】井之原 小星[表情:キラキラ]",1010,"表情:キラキラ","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100010,100051,"[O.N.G.E.K.I.]1.00-0082" -0,100184,"【R】逢坂 茜[表情:にやり]",1011,"表情:にやり","奏坂学園","Fire","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100009,100050,"[O.N.G.E.K.I.]1.00-0098" -0,100185,"【R】珠洲島 有栖[表情:照れ]",1012,"表情:照れ","奏坂学園","Aqua","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100011,100052,"[O.N.G.E.K.I.]1.00-0110" -0,100186,"【SSR】戸山香澄[クインティプル☆すまいる]",18001,"クインティプル☆すまいる","バンドリ! ガールズバンドパーティ!","Fire","-",3,"55,252,275,290,302,312,0,0,0,317",100138,100139,"[O.N.G.E.K.I.]1.00-E-0021" -1,100187,"【SSR】めぐみん[エクスプロージョン]",11001,"エクスプロージョン","この素晴らしい世界に祝福を!2","Fire","-",3,"55,252,275,290,302,312,0,0,0,317",105022,105023,"[O.N.G.E.K.I.]1.05-E-0055" -1,100188,"【SSR】アクア[花鳥風月]",11002,"花鳥風月","この素晴らしい世界に祝福を!2","Aqua","-",3,"55,252,275,290,302,312,0,0,0,317",100144,100145,"[O.N.G.E.K.I.]1.05-E-0056" -1,100189,"【SSR】ダクネス[クルセイダーシールド]",11003,"クルセイダーシールド","この素晴らしい世界に祝福を!2","Leaf","-",3,"55,252,275,290,302,312,0,0,0,317",100144,100145,"[O.N.G.E.K.I.]1.05-E-0057" -0,100190,"【R】日向 美海[青の世界]",20001,"青の世界","アンジュ・ヴィエルジュ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100003,100044,"[O.N.G.E.K.I.]1.00-E-0057" -0,100191,"【SR】日向 美海[謹賀新年]",20001,"謹賀新年","アンジュ・ヴィエルジュ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100086,100087,"[O.N.G.E.K.I.]1.00-E-0058" -0,100192,"【SR】日向 美海[無限の可能性]",20001,"無限の可能性","アンジュ・ヴィエルジュ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.00-E-0063" -0,100193,"【SSR】日向 美海[メリークリスマス!]",20001,"メリークリスマス!","アンジュ・ヴィエルジュ","Aqua","-",3,"55,252,275,290,302,312,0,0,0,317",100033,100074,"[O.N.G.E.K.I.]1.00-E-0066" -0,100194,"【SSR】日向 美海[常夏の可能性]",20001,"常夏の可能性","アンジュ・ヴィエルジュ","Aqua","-",3,"55,252,275,290,302,312,0,0,0,317",100088,100089,"[O.N.G.E.K.I.]1.00-E-0067" -0,100195,"【R】彩城 天音[青の世界]",20002,"青の世界","アンジュ・ヴィエルジュ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.00-E-0059" -0,100196,"【SR】彩城 天音[謹賀新年]",20002,"謹賀新年","アンジュ・ヴィエルジュ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100018,100059,"[O.N.G.E.K.I.]1.00-E-0060" -0,100197,"【SR】彩城 天音[みんなの希望]",20002,"みんなの希望","アンジュ・ヴィエルジュ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100028,100069,"[O.N.G.E.K.I.]1.00-E-0064" -0,100198,"【R】東条 遥[青の世界]",20003,"青の世界","アンジュ・ヴィエルジュ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.00-E-0061" -0,100199,"【SR】東条 遥[謹賀新年]",20003,"謹賀新年","アンジュ・ヴィエルジュ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100028,100069,"[O.N.G.E.K.I.]1.00-E-0062" -0,100200,"【SR】東条 遥[希望の光]",20003,"希望の光","アンジュ・ヴィエルジュ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.00-E-0065" -0,100201,"【SR】星咲 あかり[ONGEKI Vocal Collection 01]",1000,"ONGEKI Vocal Collection 01","プロモーション","Fire","高校2年生",2,"50,222,237,252,267,282,0,0,0,282",100092,100093,"[O.N.G.E.K.I.]1.00-P-0001" -0,100202,"【SR】藤沢 柚子[ONGEKI Vocal Collection 01]",1001,"ONGEKI Vocal Collection 01","プロモーション","Leaf","高校2年生",2,"50,222,237,252,267,282,0,0,0,282",100094,100095,"[O.N.G.E.K.I.]1.00-P-0002" -0,100203,"【SR】三角 葵[ONGEKI Vocal Collection 01]",1002,"ONGEKI Vocal Collection 01","プロモーション","Aqua","高校2年生",2,"50,222,237,252,267,282,0,0,0,282",100096,100097,"[O.N.G.E.K.I.]1.00-P-0003" -0,100204,"【SR】高瀬 梨緒[ONGEKI Vocal Collection 02]",1003,"ONGEKI Vocal Collection 02","プロモーション","Aqua","高校2年生",2,"50,222,237,252,267,282,0,0,0,282",100102,100103,"[O.N.G.E.K.I.]1.00-P-0004" -0,100205,"【SR】結城 莉玖[ONGEKI Vocal Collection 02]",1004,"ONGEKI Vocal Collection 02","プロモーション","Fire","高校1年生",2,"50,222,237,252,267,282,0,0,0,282",100100,100101,"[O.N.G.E.K.I.]1.00-P-0005" -0,100206,"【SR】藍原 椿[ONGEKI Vocal Collection 02]",1005,"ONGEKI Vocal Collection 02","プロモーション","Leaf","高校1年生",2,"50,222,237,252,267,282,0,0,0,282",100098,100099,"[O.N.G.E.K.I.]1.00-P-0006" -2,100208,"【N】柏木 美亜",1013,"","奏坂学園","Fire","中学2年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"[O.N.G.E.K.I.]1.10-0005" -2,100209,"【R】柏木 美亜[シューター・ドレス]",1013,"シューター・ドレス","奏坂学園","Fire","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",110002,110003,"[O.N.G.E.K.I.]1.10-0006" -1,100210,"【R】星咲 あかり[私服2]",1000,"私服2","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",105100,105101,"[O.N.G.E.K.I.]1.05-0002" -1,100211,"【R】藤沢 柚子[私服2]",1001,"私服2","奏坂学園","Leaf","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",105102,105103,"[O.N.G.E.K.I.]1.05-0006" -1,100212,"【R】三角 葵[私服2]",1002,"私服2","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",105104,105105,"[O.N.G.E.K.I.]1.05-0009" -1,100213,"【R】高瀬 梨緒[私服]",1003,"私服","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",105110,105111,"[O.N.G.E.K.I.]1.05-0020" -1,100214,"【R】結城 莉玖[私服]",1004,"私服","奏坂学園","Fire","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",105106,105107,"[O.N.G.E.K.I.]1.05-0022" -1,100215,"【R】藍原 椿[私服]",1005,"私服","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",105108,105109,"[O.N.G.E.K.I.]1.05-0024" -1,100216,"【R】早乙女 彩華[私服]",1006,"私服","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",105060,105061,"[O.N.G.E.K.I.]1.05-0033" -1,100217,"【R】桜井 春菜[私服]",1007,"私服","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",105060,105061,"[O.N.G.E.K.I.]1.05-0031" -1,100218,"【R】九條 楓[私服]",1008,"私服","奏坂学園","Leaf","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",105114,105115,"[O.N.G.E.K.I.]1.05-0057" -1,100219,"【R】柏木 咲姫[私服]",1009,"私服","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100003,100044,"[O.N.G.E.K.I.]1.05-0047" -1,100220,"【R】井之原 小星[私服]",1010,"私服","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100002,100043,"[O.N.G.E.K.I.]1.05-0049" -1,100221,"【R】逢坂 茜[私服]",1011,"私服","奏坂学園","Fire","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",105114,105115,"[O.N.G.E.K.I.]1.05-0059" -1,100222,"【R】珠洲島 有栖[私服]",1012,"私服","奏坂学園","Aqua","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",105114,105115,"[O.N.G.E.K.I.]1.05-0061" -1,100223,"【R】星咲 あかり[私服 表情:げげっ]",1000,"私服 表情:げげっ","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.05-0001" -1,100224,"【R】藤沢 柚子[私服 表情:涙目]",1001,"私服 表情:涙目","奏坂学園","Leaf","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.05-0005" -1,100225,"【R】三角 葵[私服 表情:笑顔]",1002,"私服 表情:笑顔","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.05-0028" -1,100226,"【R】高瀬 梨緒[私服 表情:ぐぬぬ]",1003,"私服 表情:ぐぬぬ","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",105060,105061,"[O.N.G.E.K.I.]1.05-0038" -1,100227,"【R】結城 莉玖[私服 表情:むー]",1004,"私服 表情:むー","奏坂学園","Fire","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",105060,105061,"[O.N.G.E.K.I.]1.05-0029" -1,100228,"【R】藍原 椿[私服 表情:目線を逸らす]",1005,"私服 表情:目線を逸らす","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",105060,105061,"[O.N.G.E.K.I.]1.05-0040" -1,100229,"【R】早乙女 彩華[私服 表情:照れ]",1006,"私服 表情:照れ","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",105118,105119,"[O.N.G.E.K.I.]1.05-0045" -1,100230,"【R】桜井 春菜[私服 表情:恥じらい]",1007,"私服 表情:恥じらい","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",105120,105121,"[O.N.G.E.K.I.]1.05-0046" -1,100231,"【R】九條 楓[私服 表情:恥じらい]",1008,"私服 表情:恥じらい","奏坂学園","Leaf","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",105068,105069,"[O.N.G.E.K.I.]1.05-0066" -1,100232,"【R】柏木 咲姫[私服 表情:得意気]",1009,"私服 表情:得意気","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",105124,105125,"[O.N.G.E.K.I.]1.05-0055" -1,100233,"【R】井之原 小星[私服 表情:悲しい]",1010,"私服 表情:悲しい","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",105122,105123,"[O.N.G.E.K.I.]1.05-0054" -1,100234,"【R】逢坂 茜[私服 表情:わっはっは!]",1011,"私服 表情:わっはっは!","奏坂学園","Fire","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",105068,105069,"[O.N.G.E.K.I.]1.05-0077" -1,100235,"【R】珠洲島 有栖[私服 表情:微笑み]",1012,"私服 表情:微笑み","奏坂学園","Aqua","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",105068,105069,"[O.N.G.E.K.I.]1.05-0068" -1,100236,"【SR】星咲 あかり[わくわくバイト・ミッション]",1000,"わくわくバイト・ミッション","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105078,105079,"[O.N.G.E.K.I.]1.05-0004" -1,100237,"【SR】藤沢 柚子[わくわくバイト・ミッション]",1001,"わくわくバイト・ミッション","奏坂学園","Leaf","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105080,105081,"[O.N.G.E.K.I.]1.05-0008" -1,100238,"【SR】三角 葵[わくわくバイト・ミッション]",1002,"わくわくバイト・ミッション","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105082,105083,"[O.N.G.E.K.I.]1.05-0011" -1,100239,"【SR】高瀬 梨緒[ゲーセン☆チャンス!?]",1003,"ゲーセン☆チャンス!?","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105084,105085,"[O.N.G.E.K.I.]1.05-0021" -1,100240,"【SR】結城 莉玖[ゲーセン☆チャンス!?]",1004,"ゲーセン☆チャンス!?","奏坂学園","Fire","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",105084,105085,"[O.N.G.E.K.I.]1.05-0023" -1,100241,"【SR】藍原 椿[ゲーセン☆チャンス!?]",1005,"ゲーセン☆チャンス!?","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",105084,105085,"[O.N.G.E.K.I.]1.05-0025" -1,100242,"【SR】早乙女 彩華[妄想×動揺×大暴走]",1006,"妄想×動揺×大暴走","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",105088,105089,"[O.N.G.E.K.I.]1.05-0034" -1,100243,"【SR】桜井 春菜[妄想×動揺×大暴走]",1007,"妄想×動揺×大暴走","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105086,105087,"[O.N.G.E.K.I.]1.05-0032" -1,100244,"【SR】九條 楓[ドタバタ子守り大作戦]",1008,"ドタバタ子守り大作戦","奏坂学園","Leaf","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",105096,105097,"[O.N.G.E.K.I.]1.05-0058" -1,100245,"【SR】柏木 咲姫[おかえりなさいませ! ご主人さま♪]",1009,"おかえりなさいませ! ご主人さま♪","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",105092,105093,"[O.N.G.E.K.I.]1.05-0048" -1,100246,"【SR】井之原 小星[おかえりなさいませ! ご主人さま♪]",1010,"おかえりなさいませ! ご主人さま♪","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",105090,105091,"[O.N.G.E.K.I.]1.05-0050" -1,100247,"【SR】逢坂 茜[ドタバタ子守り大作戦]",1011,"ドタバタ子守り大作戦","奏坂学園","Fire","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",105094,105095,"[O.N.G.E.K.I.]1.05-0060" -1,100248,"【SR】珠洲島 有栖[ドタバタ子守り大作戦]",1012,"ドタバタ子守り大作戦","奏坂学園","Aqua","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",105098,105099,"[O.N.G.E.K.I.]1.05-0062" -1,100249,"【SR】星咲 あかり[Jump!! Jump!! Jump!!]",1000,"Jump!! Jump!! Jump!!","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105040,105041,"[O.N.G.E.K.I.]1.05-0003" -1,100250,"【SR】藤沢 柚子[Jump!! Jump!! Jump!!]",1001,"Jump!! Jump!! Jump!!","奏坂学園","Leaf","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105040,105041,"[O.N.G.E.K.I.]1.05-0007" -1,100251,"【SR】三角 葵[Jump!! Jump!! Jump!!]",1002,"Jump!! Jump!! Jump!!","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105040,105041,"[O.N.G.E.K.I.]1.05-0010" -1,100252,"【SR】高瀬 梨緒[Jump!! Jump!! Jump!!]",1003,"Jump!! Jump!! Jump!!","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105040,105041,"[O.N.G.E.K.I.]1.05-0012" -2,100253,"【SR】結城 莉玖[Jump!! Jump!! Jump!!]",1004,"Jump!! Jump!! Jump!!","奏坂学園","Fire","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",105040,105041,"[O.N.G.E.K.I.]1.10-0028" -2,100254,"【SR】藍原 椿[Jump!! Jump!! Jump!!]",1005,"Jump!! Jump!! Jump!!","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",105040,105041,"[O.N.G.E.K.I.]1.10-0055" -2,100255,"【SR】早乙女 彩華[Jump!! Jump!! Jump!!]",1006,"Jump!! Jump!! Jump!!","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",105040,105041,"[O.N.G.E.K.I.]1.10-0039" -2,100256,"【SR】桜井 春菜[Jump!! Jump!! Jump!!]",1007,"Jump!! Jump!! Jump!!","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105040,105041,"[O.N.G.E.K.I.]1.10-0029" -2,100257,"【SR】九條 楓[Jump!! Jump!! Jump!!]",1008,"Jump!! Jump!! Jump!!","奏坂学園","Leaf","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",105040,105041,"[O.N.G.E.K.I.]1.10-0056" -2,100258,"【SR】柏木 咲姫[Jump!! Jump!! Jump!!]",1009,"Jump!! Jump!! Jump!!","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",105040,105041,"[O.N.G.E.K.I.]1.10-0041" -2,100259,"【SR】井之原 小星[Jump!! Jump!! Jump!!]",1010,"Jump!! Jump!! Jump!!","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",105040,105041,"[O.N.G.E.K.I.]1.10-0057" -1,100260,"【SR】逢坂 茜[Jump!! Jump!! Jump!!]",1011,"Jump!! Jump!! Jump!!","奏坂学園","Fire","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",105040,105041,"[O.N.G.E.K.I.]1.05-0017" -2,100261,"【SR】珠洲島 有栖[Jump!! Jump!! Jump!!]",1012,"Jump!! Jump!! Jump!!","奏坂学園","Aqua","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",105040,105041,"[O.N.G.E.K.I.]1.10-0042" -1,100262,"【SR】星咲 あかり[どっちがどっち?]",1000,"どっちがどっち?","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105042,105043,"[O.N.G.E.K.I.]1.05-0026" -1,100263,"【SR】藤沢 柚子[華より団子]",1001,"華より団子","奏坂学園","Leaf","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100031,100072,"[O.N.G.E.K.I.]1.05-0027" -1,100264,"【SR】三角 葵[モテモテ!? ふれあい体験]",1002,"モテモテ!? ふれあい体験","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105044,105045,"[O.N.G.E.K.I.]1.05-0043" -1,100265,"【SR】高瀬 梨緒[巷で噂の看板娘]",1003,"巷で噂の看板娘","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105050,105051,"[O.N.G.E.K.I.]1.05-0039" -1,100266,"【SR】結城 莉玖[食べちゃうぞ♪]",1004,"食べちゃうぞ♪","奏坂学園","Fire","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",105046,105047,"[O.N.G.E.K.I.]1.05-0044" -1,100267,"【SR】藍原 椿[恥じらいバレンタイン]",1005,"恥じらいバレンタイン","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",105048,105049,"[O.N.G.E.K.I.]1.05-0013" -1,100268,"【SR】早乙女 彩華[レッツ、チャレンジ!?]",1006,"レッツ、チャレンジ!?","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",105052,105053,"[O.N.G.E.K.I.]1.05-0075" -1,100269,"【SR】桜井 春菜[びしょ濡れ雨宿り]",1007,"びしょ濡れ雨宿り","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100024,100065,"[O.N.G.E.K.I.]1.05-0053" -1,100270,"【SR】九條 楓[行雲流水の如く]",1008,"行雲流水の如く","奏坂学園","Leaf","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",100024,100065,"[O.N.G.E.K.I.]1.05-0042" -1,100271,"【SR】柏木 咲姫[南国ホリデー]",1009,"南国ホリデー","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",100031,100072,"[O.N.G.E.K.I.]1.05-0065" -1,100272,"【SR】井之原 小星[ぷかぷかコンティニュー]",1010,"ぷかぷかコンティニュー","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",105054,105055,"[O.N.G.E.K.I.]1.05-0076" -1,100273,"【SR】逢坂 茜[いたずらスプラッシュ]",1011,"いたずらスプラッシュ","奏坂学園","Fire","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",105056,105057,"[O.N.G.E.K.I.]1.05-0067" -1,100274,"【SR】珠洲島 有栖[雨上がりの空に]",1012,"雨上がりの空に","奏坂学園","Aqua","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",105058,105059,"[O.N.G.E.K.I.]1.05-0056" -1,100275,"【SSR】星咲 あかり[マーメイド・ランデブー]",1000,"マーメイド・ランデブー","奏坂学園","Fire","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",105028,105029,"[O.N.G.E.K.I.]1.05-0073" -1,100276,"【SSR】藤沢 柚子[ハジけて☆サマー]",1001,"ハジけて☆サマー","奏坂学園","Leaf","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",105028,105029,"[O.N.G.E.K.I.]1.05-0063" -1,100277,"【SSR】三角 葵[with you]",1002,"with you","奏坂学園","Aqua","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",105028,105029,"[O.N.G.E.K.I.]1.05-0064" -1,100278,"【SSR】高瀬 梨緒[黄昏ロマンス]",1003,"黄昏ロマンス","奏坂学園","Aqua","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",105030,105031,"[O.N.G.E.K.I.]1.05-0074" -1,100280,"【SSR】藍原 椿[狐の嫁入り]",1005,"狐の嫁入り","奏坂学園","Leaf","高校1年生",3,"60,257,280,295,307,317,0,0,0,322",105030,105031,"[O.N.G.E.K.I.]1.05-0052" -1,100282,"【SSR】桜井 春菜[スイート&スイート]",1007,"スイート&スイート","奏坂学園","Fire","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",105032,105033,"[O.N.G.E.K.I.]1.05-0014" -1,100284,"【SSR】柏木 咲姫[エキゾチック・トワイライト]",1009,"エキゾチック・トワイライト","奏坂学園","Aqua","高校3年生",3,"60,257,280,295,307,317,0,0,0,322",105036,105037,"[O.N.G.E.K.I.]1.05-0041" -1,100285,"【SSR】井之原 小星[おひな様は脱力系]",1010,"おひな様は脱力系","奏坂学園","Leaf","高校1年生",3,"60,257,280,295,307,317,0,0,0,322",105034,105035,"[O.N.G.E.K.I.]1.05-0030" -1,100286,"【SSR】逢坂 茜[それいけ! 猛獣マスター]",1011,"それいけ! 猛獣マスター","奏坂学園","Fire","高校3年生",3,"60,257,280,295,307,317,0,0,0,322",105038,105039,"[O.N.G.E.K.I.]1.05-0051" -1,100288,"【SSR】九條 楓[デイドリーム・フェアリーズ]",1008,"デイドリーム・フェアリーズ","奏坂学園","Leaf","高校3年生",3,"65,270,291,304,314,322,0,0,0,327",100039,100080,"[O.N.G.E.K.I.]1.05-0015" -1,100289,"【SSR】逢坂 茜[デイドリーム・フェアリーズ]",1011,"デイドリーム・フェアリーズ","奏坂学園","Fire","高校3年生",3,"65,270,291,304,314,322,0,0,0,327",100038,100079,"[O.N.G.E.K.I.]1.05-0016" -1,100290,"【SSR】珠洲島 有栖[デイドリーム・フェアリーズ]",1012,"デイドリーム・フェアリーズ","奏坂学園","Aqua","高校1年生",3,"65,270,291,304,314,322,0,0,0,327",100040,100081,"[O.N.G.E.K.I.]1.05-0019" -1,100291,"【SR】早乙女 彩華[ONGEKI Vocal Collection 03]",1006,"ONGEKI Vocal Collection 03","プロモーション","Aqua","高校3年生",2,"50,222,237,252,267,282,0,0,0,282",105120,105121,"[O.N.G.E.K.I.]1.05-P-0002" -1,100292,"【SR】桜井 春菜[ONGEKI Vocal Collection 03]",1007,"ONGEKI Vocal Collection 03","プロモーション","Fire","高校2年生",2,"50,222,237,252,267,282,0,0,0,282",105118,105119,"[O.N.G.E.K.I.]1.05-P-0003" -1,100293,"【SR】九條 楓[ONGEKI Vocal Collection 05]",1008,"ONGEKI Vocal Collection 05","プロモーション","Leaf","高校3年生",2,"50,222,237,252,267,282,0,0,0,282",105128,105129,"[O.N.G.E.K.I.]1.05-P-0006" -1,100294,"【SR】柏木 咲姫[ONGEKI Vocal Collection 04]",1009,"ONGEKI Vocal Collection 04","プロモーション","Aqua","高校3年生",2,"50,222,237,252,267,282,0,0,0,282",105124,105125,"[O.N.G.E.K.I.]1.05-P-0004" -1,100295,"【SR】井之原 小星[ONGEKI Vocal Collection 04]",1010,"ONGEKI Vocal Collection 04","プロモーション","Leaf","高校1年生",2,"50,222,237,252,267,282,0,0,0,282",105122,105123,"[O.N.G.E.K.I.]1.05-P-0005" -1,100296,"【SR】逢坂 茜[ONGEKI Vocal Collection 05]",1011,"ONGEKI Vocal Collection 05","プロモーション","Fire","高校3年生",2,"50,222,237,252,267,282,0,0,0,282",105126,105127,"[O.N.G.E.K.I.]1.05-P-0007" -1,100297,"【SR】珠洲島 有栖[ONGEKI Vocal Collection 05]",1012,"ONGEKI Vocal Collection 05","プロモーション","Aqua","高校1年生",2,"50,222,237,252,267,282,0,0,0,282",105130,105131,"[O.N.G.E.K.I.]1.05-P-0008" -1,100298,"【SSR】星咲 あかり[ONGEKI Sound Collection 01]",1000,"ONGEKI Sound Collection 01","プロモーション","Fire","高校2年生",3,"55,252,275,290,302,312,0,0,0,317",105144,105145,"[O.N.G.E.K.I.]1.05-P-0001" -1,100299,"【R】橙[東方妖々夢]",2008,"東方妖々夢","東方妖々夢","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100012,100053,"[O.N.G.E.K.I.]1.05-E-0039" -1,100300,"【R】魂魄 妖夢[東方妖々夢]",2009,"東方妖々夢","東方妖々夢","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100002,100043,"[O.N.G.E.K.I.]1.05-E-0041" -1,100301,"【R】西行寺 幽々子[東方妖々夢]",2010,"東方妖々夢","東方妖々夢","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.05-E-0044" -1,100302,"【R】八雲 藍[東方妖々夢]",2011,"東方妖々夢","東方妖々夢","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",105014,105015,"[O.N.G.E.K.I.]1.05-E-0047" -1,100303,"【R】八雲 紫[東方妖々夢]",2012,"東方妖々夢","東方妖々夢","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.05-E-0049" -1,100304,"【SR】橙[すきま妖怪の式の式]",2008,"すきま妖怪の式の式","東方妖々夢","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.05-E-0040" -1,100305,"【SR】魂魄 妖夢[半人半霊の庭師]",2009,"半人半霊の庭師","東方妖々夢","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100027,100068,"[O.N.G.E.K.I.]1.05-E-0042" -1,100306,"【SR】西行寺 幽々子[幽冥楼閣の亡霊少女]",2010,"幽冥楼閣の亡霊少女","東方妖々夢","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.05-E-0045" -1,100307,"【SR】八雲 藍[すきま妖怪の式]",2011,"すきま妖怪の式","東方妖々夢","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",105012,105013,"[O.N.G.E.K.I.]1.05-E-0048" -1,100308,"【SR】八雲 紫[幻想の境界]",2012,"幻想の境界","東方妖々夢","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.05-E-0050" -1,100309,"【SSR】西行寺 幽々子[桜符「完全なる墨染の桜 -開花-」]",2010,"桜符「完全なる墨染の桜 -開花-」","東方妖々夢","Leaf","-",3,"55,252,275,290,302,312,0,0,0,317",100033,100074,"[O.N.G.E.K.I.]1.05-E-0046" -1,100310,"【SSR】魂魄 妖夢[獄界剣「二百由旬の一閃」]",2009,"獄界剣「二百由旬の一閃」","東方妖々夢","Leaf","-",3,"55,252,275,290,302,312,0,0,0,317",100036,100077,"[O.N.G.E.K.I.]1.05-E-0043" -1,100311,"【R】御坂 美琴[常盤台中学]",21001,"常盤台中学","とある科学の超電磁砲S","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.05-E-0005" -1,100312,"【R】白井 黒子[常盤台中学]",21002,"常盤台中学","とある科学の超電磁砲S","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100002,100043,"[O.N.G.E.K.I.]1.05-E-0008" -1,100313,"【R】初春 飾利[柵川中学]",21003,"柵川中学","とある科学の超電磁砲S","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.05-E-0015" -1,100314,"【R】佐天 涙子[柵川中学]",21004,"柵川中学","とある科学の超電磁砲S","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.05-E-0018" -1,100315,"【SR】御坂 美琴[電撃使い]",21001,"電撃使い","とある科学の超電磁砲S","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.05-E-0006" -1,100316,"【SR】白井 黒子[空間移動]",21002,"空間移動","とある科学の超電磁砲S","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",105004,105005,"[O.N.G.E.K.I.]1.05-E-0009" -1,100317,"【SR】初春 飾利[定温保存]",21003,"定温保存","とある科学の超電磁砲S","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.05-E-0016" -1,100318,"【SR】佐天 涙子[空力使い]",21004,"空力使い","とある科学の超電磁砲S","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.05-E-0019" -1,100319,"【SR】御坂 美琴[常盤台のエース]",21001,"常盤台のエース","とある科学の超電磁砲S","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100027,100068,"[O.N.G.E.K.I.]1.05-E-0007" -1,100320,"【SR】白井 黒子[風紀委員ですの!]",21002,"風紀委員ですの!","とある科学の超電磁砲S","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100008,100049,"[O.N.G.E.K.I.]1.05-E-0010" -1,100321,"【SR】初春 飾利[いきなり何するんですか佐天さんっ!]",21003,"いきなり何するんですか佐天さんっ!","とある科学の超電磁砲S","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",105002,105003,"[O.N.G.E.K.I.]1.05-E-0017" -1,100322,"【SR】佐天 涙子[うーいーはーるーん!!]",21004,"うーいーはーるーん!!","とある科学の超電磁砲S","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100008,100049,"[O.N.G.E.K.I.]1.05-E-0020" -1,100323,"【SSR】御坂 美琴[超電磁砲]",21001,"超電磁砲","とある科学の超電磁砲S","Leaf","-",3,"55,252,275,290,302,312,0,0,0,317",100033,100074,"[O.N.G.E.K.I.]1.05-E-0013" -1,100324,"【SSR】御坂 美琴[学園都市の電撃姫]",21001,"学園都市の電撃姫","とある科学の超電磁砲S","Leaf","-",3,"55,252,275,290,302,312,0,0,0,317",105000,105001,"[O.N.G.E.K.I.]1.05-E-0014" -1,100325,"【R】ペコリーヌ[美食殿]",22001,"美食殿","プリンセスコネクト!Re:Dive","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.05-E-0024" -1,100326,"【R】コッコロ[美食殿]",22002,"美食殿","プリンセスコネクト!Re:Dive","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.05-E-0027" -1,100327,"【R】キャル[美食殿]",22003,"美食殿","プリンセスコネクト!Re:Dive","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.05-E-0030" -1,100328,"【SR】ペコリーヌ[ユースティアナ・フォン・アストライア]",22001,"ユースティアナ・フォン・アストライア","プリンセスコネクト!Re:Dive","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.05-E-0025" -1,100329,"【SR】コッコロ[棗こころ]",22002,"棗こころ","プリンセスコネクト!Re:Dive","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.05-E-0028" -1,100330,"【SR】キャル[百地希留耶]",22003,"百地希留耶","プリンセスコネクト!Re:Dive","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",105004,105005,"[O.N.G.E.K.I.]1.05-E-0031" -1,100331,"【SR】ペコリーヌ[プリンセスストライク]",22001,"プリンセスストライク","プリンセスコネクト!Re:Dive","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",105008,105009,"[O.N.G.E.K.I.]1.05-E-0026" -1,100332,"【SR】コッコロ[オーロラ]",22002,"オーロラ","プリンセスコネクト!Re:Dive","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100028,100069,"[O.N.G.E.K.I.]1.05-E-0029" -1,100333,"【SR】キャル[グリムバースト]",22003,"グリムバースト","プリンセスコネクト!Re:Dive","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.05-E-0032" -1,100334,"【SSR】ペコリーヌ[プリンセスフォース]",22001,"プリンセスフォース","プリンセスコネクト!Re:Dive","Aqua","-",3,"55,252,275,290,302,312,0,0,0,317",100036,100077,"[O.N.G.E.K.I.]1.05-E-0033" -1,100335,"【SSR】コッコロ[精霊の啓示]",22002,"精霊の啓示","プリンセスコネクト!Re:Dive","Aqua","-",3,"55,252,275,290,302,312,0,0,0,317",105006,105007,"[O.N.G.E.K.I.]1.05-E-0034" -1,100336,"【SR】星咲 あかり[ギーコギーコでトントントン]",1000,"ギーコギーコでトントントン","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105142,105143,"[O.N.G.E.K.I.]1.05-0069" -1,100337,"【SR】藤沢 柚子[ギーコギーコでトントントン]",1001,"ギーコギーコでトントントン","奏坂学園","Leaf","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105142,105143,"[O.N.G.E.K.I.]1.05-0070" -1,100338,"【SR】三角 葵[ギーコギーコでトントントン]",1002,"ギーコギーコでトントントン","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105142,105143,"[O.N.G.E.K.I.]1.05-0071" -0,100339,"【R】星咲 あかり[シュータードレス:全身ver.]",1000,"シュータードレス:全身ver.","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100110,100111,"[O.N.G.E.K.I.]1.00-0006" -0,100340,"【R】藤沢 柚子[シュータードレス:全身ver.]",1001,"シュータードレス:全身ver.","奏坂学園","Leaf","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100112,100113,"[O.N.G.E.K.I.]1.00-0016" -0,100341,"【R】三角 葵[シュータードレス:全身ver.]",1002,"シュータードレス:全身ver.","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100114,100115,"[O.N.G.E.K.I.]1.00-0026" -0,100342,"【R】高瀬 梨緒[シュータードレス:全身ver.]",1003,"シュータードレス:全身ver.","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100120,100121,"[O.N.G.E.K.I.]1.00-0035" -0,100343,"【R】結城 莉玖[シュータードレス:全身ver.]",1004,"シュータードレス:全身ver.","奏坂学園","Fire","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100116,100117,"[O.N.G.E.K.I.]1.00-0044" -0,100344,"【R】藍原 椿[シュータードレス:全身ver.]",1005,"シュータードレス:全身ver.","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100118,100119,"[O.N.G.E.K.I.]1.00-0053" -0,100345,"【R】早乙女 彩華[シュータードレス:全身ver.]",1006,"シュータードレス:全身ver.","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100126,100127,"[O.N.G.E.K.I.]1.00-0066" -0,100346,"【R】桜井 春菜[シュータードレス:全身ver.]",1007,"シュータードレス:全身ver.","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",100122,100123,"[O.N.G.E.K.I.]1.00-0061" -0,100347,"【R】九條 楓[シュータードレス:全身ver.]",1008,"シュータードレス:全身ver.","奏坂学園","Leaf","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100124,100125,"[O.N.G.E.K.I.]1.00-0071" -0,100348,"【R】柏木 咲姫[シュータードレス:全身ver.]",1009,"シュータードレス:全身ver.","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100132,100133,"[O.N.G.E.K.I.]1.00-0092" -0,100349,"【R】井之原 小星[シュータードレス:全身ver.]",1010,"シュータードレス:全身ver.","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100130,100131,"[O.N.G.E.K.I.]1.00-0085" -0,100350,"【R】逢坂 茜[シュータードレス:全身ver.]",1011,"シュータードレス:全身ver.","奏坂学園","Fire","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100128,100129,"[O.N.G.E.K.I.]1.00-0101" -0,100351,"【R】珠洲島 有栖[シュータードレス:全身ver.]",1012,"シュータードレス:全身ver.","奏坂学園","Aqua","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",100134,100135,"[O.N.G.E.K.I.]1.00-0113" -1,100352,"【SR】御坂 美琴[ゲコラー]",21001,"ゲコラー","とある科学の超電磁砲S","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",105138,105139,"[O.N.G.E.K.I.]1.05-E-0011" -1,100353,"【SR】御坂 美琴[雷撃の槍]",21001,"雷撃の槍","とある科学の超電磁砲S","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",105140,105141,"[O.N.G.E.K.I.]1.05-E-0012" -1,100375,"【R】アリサ[森の弓使い]",22004,"森の弓使い","プリンセスコネクト!Re:Dive","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.05-E-0035" -1,100376,"【SR】アリサ[乙女な転校生]",22004,"乙女な転校生","プリンセスコネクト!Re:Dive","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",105010,105011,"[O.N.G.E.K.I.]1.05-E-0036" -1,100377,"【SR】アリサ[深緑の射手]",22004,"深緑の射手","プリンセスコネクト!Re:Dive","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100027,100068,"[O.N.G.E.K.I.]1.05-E-0037" -1,100391,"【R】ゆんゆん[アークウィザード]",11004,"アークウィザード","この素晴らしい世界に祝福を!2","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.05-E-0058" -1,100392,"【SR】ゆんゆん[我が名はゆんゆん]",11004,"我が名はゆんゆん","この素晴らしい世界に祝福を!2","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",105024,105025,"[O.N.G.E.K.I.]1.05-E-0059" -1,100393,"【SSR】ゆんゆん[紅魔族の長となるもの]",11004,"紅魔族の長となるもの","この素晴らしい世界に祝福を!2","Fire","-",3,"55,252,275,290,302,312,0,0,0,317",100138,100139,"[O.N.G.E.K.I.]1.05-E-0061" -1,100394,"【SSR】めぐみん[紅魔族随一の天才]",11001,"紅魔族随一の天才","この素晴らしい世界に祝福を!2","Fire","-",3,"55,252,275,290,302,312,0,0,0,317",105020,105021,"[O.N.G.E.K.I.]1.05-E-0060" -2,100395,"【R】因幡 てゐ[東方永夜抄]",2013,"東方永夜抄","東方永夜抄","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0044" -2,100396,"【R】鈴仙・優曇華院・イナバ[東方永夜抄]",2014,"東方永夜抄","東方永夜抄","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.10-E-0042" -2,100397,"【R】八意 永琳[東方永夜抄]",2015,"東方永夜抄","東方永夜抄","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100003,100044,"[O.N.G.E.K.I.]1.10-E-0039" -2,100398,"【R】蓬莱山 輝夜[東方永夜抄]",2016,"東方永夜抄","東方永夜抄","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0036" -2,100399,"【R】藤原 妹紅[東方永夜抄]",2017,"東方永夜抄","東方永夜抄","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0046" -2,100400,"【SR】因幡 てゐ[幸運の素兎]",2013,"幸運の素兎","東方永夜抄","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",105042,105043,"[O.N.G.E.K.I.]1.10-E-0045" -2,100401,"【SR】鈴仙・優曇華院・イナバ[狂気の月の兎]",2014,"狂気の月の兎","東方永夜抄","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.10-E-0043" -2,100402,"【SR】八意 永琳[月の頭脳]",2015,"月の頭脳","東方永夜抄","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100028,100069,"[O.N.G.E.K.I.]1.10-E-0040" -2,100403,"【SR】蓬莱山 輝夜[永遠と須臾の罪人]",2016,"永遠と須臾の罪人","東方永夜抄","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.10-E-0037" -2,100404,"【SR】藤原 妹紅[蓬莱の人の形]",2017,"蓬莱の人の形","東方永夜抄","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110002,110003,"[O.N.G.E.K.I.]1.10-E-0047" -2,100405,"【SSR】八意 永琳[天呪「アポロ13」]",2015,"天呪「アポロ13」","東方永夜抄","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",100033,100074,"[O.N.G.E.K.I.]1.10-E-0041" -2,100406,"【SSR】蓬莱山 輝夜[神宝「蓬莱の玉の枝 -夢色の郷-」]",2016,"神宝「蓬莱の玉の枝 -夢色の郷-」","東方永夜抄","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]1.10-E-0038" -2,100407,"【SSR】柏木 美亜[にゃんにゃんファンファーレ]",1013,"にゃんにゃんファンファーレ","奏坂学園","Fire","中学2年生",3,"65,262,285,300,312,322,0,0,0,327",110004,110005,"[O.N.G.E.K.I.]1.10-0023" -1,100408,"【SR】日向 美海[絶対の可能性]",20001,"絶対の可能性","アンジュ・ヴィエルジュ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",105016,105017,"[O.N.G.E.K.I.]1.05-E-0052" -1,100409,"【SR】彩城 天音[サンシャイン・ヒロイン]",20002,"サンシャイン・ヒロイン","アンジュ・ヴィエルジュ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",105010,105011,"[O.N.G.E.K.I.]1.05-E-0053" -1,100410,"【SR】東条 遥[蒼輝絢爛]",20003,"蒼輝絢爛","アンジュ・ヴィエルジュ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",105018,105019,"[O.N.G.E.K.I.]1.05-E-0054" -2,100411,"【R】高瀬 梨緒[クリスマス]",1003,"クリスマス","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",110006,110007,"[O.N.G.E.K.I.]1.10-0046" -2,100412,"【R】結城 莉玖[クリスマス]",1004,"クリスマス","奏坂学園","Fire","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",110006,110007,"[O.N.G.E.K.I.]1.10-0048" -2,100413,"【R】藍原 椿[クリスマス]",1005,"クリスマス","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",110006,110007,"[O.N.G.E.K.I.]1.10-0050" -2,100414,"【R】桜井 春菜[着物]",1007,"着物","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",110008,110009,"[O.N.G.E.K.I.]1.10-0060" -2,100415,"【R】早乙女 彩華[着物]",1006,"着物","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",110010,110011,"[O.N.G.E.K.I.]1.10-0059" -2,100416,"【R】井之原 小星[私服・冬]",1010,"私服・冬","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",110012,110013,"[O.N.G.E.K.I.]1.10-0069" -2,100417,"【R】柏木 咲姫[私服・冬]",1009,"私服・冬","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",110014,110015,"[O.N.G.E.K.I.]1.10-0068" -2,100418,"【SR】星咲 あかり[あま~いひと時]",1000,"あま~いひと時","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",110016,110017,"[O.N.G.E.K.I.]1.10-0001" -2,100419,"【SR】藍原 椿[街角ランデブー]",1005,"街角ランデブー","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",110018,110019,"[O.N.G.E.K.I.]1.10-0024" -2,100420,"【SR】早乙女 彩華[次はこっち!]",1006,"次はこっち!","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",100031,100072,"[O.N.G.E.K.I.]1.10-0020" -2,100421,"【SR】珠洲島 有栖[ぐるぐるヴァンパイア]",1012,"ぐるぐるヴァンパイア","奏坂学園","Aqua","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",110030,110031,"[O.N.G.E.K.I.]1.10-0032" -2,100422,"【SR】桜井 春菜[パンプキン・メランコリー]",1007,"パンプキン・メランコリー","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100031,100072,"[O.N.G.E.K.I.]1.10-0031" -2,100423,"【SR】藤沢 柚子[ただいま捜査中]",1001,"ただいま捜査中","奏坂学園","Leaf","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",110034,110035,"[O.N.G.E.K.I.]1.10-0038" -2,100424,"【SR】日向 千夏[謎は全て解けたよ!]",1014,"謎は全て解けたよ!","奏坂学園","Leaf","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",110022,110023,"[O.N.G.E.K.I.]1.10-0044" -2,100425,"【SR】柏木 咲姫[Let's Party!]",1009,"Let's Party!","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",110024,110025,"[O.N.G.E.K.I.]1.10-0052" -2,100426,"【SR】逢坂 茜[エクストリーム・クリスマス]",1011,"エクストリーム・クリスマス","奏坂学園","Fire","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",110026,110027,"[O.N.G.E.K.I.]1.10-0053" -2,100427,"【SR】井之原 小星[だらだらニューイヤー]",1010,"だらだらニューイヤー","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",110028,110029,"[O.N.G.E.K.I.]1.10-0062" -2,100428,"【SR】東雲 つむぎ[次は負けませんから!]",1015,"次は負けませんから!","奏坂学園","Aqua","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",105044,105045,"[O.N.G.E.K.I.]1.10-0063" -2,100429,"【SR】柏木 美亜[ア・ゲ・ル♡]",1013,"ア・ゲ・ル♡","奏坂学園","Fire","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",110032,110033,"[O.N.G.E.K.I.]1.10-0070" -2,100430,"【SR】三角 葵[スイート・イルミネーション]",1002,"スイート・イルミネーション","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",110020,110021,"[O.N.G.E.K.I.]1.10-0066" -2,100431,"【SSR】日向 千夏[全力リトルマーチ]",1014,"全力リトルマーチ","奏坂学園","Leaf","中学2年生",3,"65,262,285,300,312,322,0,0,0,327",110004,110005,"[O.N.G.E.K.I.]1.10-0015" -2,100432,"【SSR】東雲 つむぎ[張り切りゴシックリズム]",1015,"張り切りゴシックリズム","奏坂学園","Aqua","中学2年生",3,"65,262,285,300,312,322,0,0,0,327",110004,110005,"[O.N.G.E.K.I.]1.10-0027" -2,100433,"【SSR】結城 莉玖[オオカミは食いしん坊]",1004,"オオカミは食いしん坊","奏坂学園","Fire","高校1年生",3,"65,262,285,300,312,322,0,0,0,327",105030,105031,"[O.N.G.E.K.I.]1.10-0030" -2,100434,"【SSR】早乙女 彩華[闇夜の挑戦状]",1006,"闇夜の挑戦状","奏坂学園","Aqua","高校3年生",3,"65,262,285,300,312,322,0,0,0,327",110036,110037,"[O.N.G.E.K.I.]1.10-0040" -2,100435,"【SSR】珠洲島 有栖[しあわせデリバリー]",1012,"しあわせデリバリー","奏坂学園","Aqua","高校1年生",3,"65,262,285,300,312,322,0,0,0,327",110038,110039,"[O.N.G.E.K.I.]1.10-0054" -2,100436,"【SSR】結城 莉玖[ホーリーナイト・セッション]",1004,"ホーリーナイト・セッション","奏坂学園","Fire","高校1年生",3,"65,262,285,300,312,322,0,0,0,327",110040,110041,"[O.N.G.E.K.I.]1.10-0049" -2,100437,"【SSR】星咲 あかり[お願い!開運巫女!]",1000,"お願い!開運巫女!","奏坂学園","Fire","高校2年生",3,"65,262,285,300,312,322,0,0,0,327",110042,110043,"[O.N.G.E.K.I.]1.10-0058" -2,100438,"【SSR】九條 楓[風流韻事]",1008,"風流韻事","奏坂学園","Leaf","高校3年生",3,"65,262,285,300,312,322,0,0,0,327",110044,110045,"[O.N.G.E.K.I.]1.10-0061" -2,100439,"【SSR】高瀬 梨緒[アタシがプレゼント!?]",1003,"アタシがプレゼント!?","奏坂学園","Aqua","高校2年生",3,"65,262,285,300,312,322,0,0,0,327",110046,110047,"[O.N.G.E.K.I.]1.10-0067" -2,100440,"【R】日向 千夏[シューター・ドレス]",1014,"シューター・ドレス","奏坂学園","Leaf","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",110012,110013,"[O.N.G.E.K.I.]1.10-0010" -2,100441,"【R】東雲 つむぎ[シューター・ドレス]",1015,"シューター・ドレス","奏坂学園","Aqua","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",110014,110015,"[O.N.G.E.K.I.]1.10-0017" -2,100442,"【SR+】柏木 美亜[いきなりシューターフェス決勝!+]",1013,"いきなりシューターフェス決勝!+","奏坂学園","Fire","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",110048,110049,"[O.N.G.E.K.I.]1.10-0008" -2,100443,"【SR+】日向 千夏[いきなりシューターフェス決勝!+]",1014,"いきなりシューターフェス決勝!+","奏坂学園","Leaf","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",110050,110051,"[O.N.G.E.K.I.]1.10-0014" -2,100444,"【SR+】東雲 つむぎ[いきなりシューターフェス決勝!+]",1015,"いきなりシューターフェス決勝!+","奏坂学園","Aqua","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",110052,110053,"[O.N.G.E.K.I.]1.10-0019" -2,100445,"【R】柏木 美亜[私服]",1013,"私服","奏坂学園","Fire","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",110054,110055,"[O.N.G.E.K.I.]1.10-0043" -2,100446,"【SR+】珠洲島 有栖[ぷれぜんと・ふぉー・ゆー+]",1012,"ぷれぜんと・ふぉー・ゆー+","奏坂学園","Aqua","高校1年生",12,"53,240,263,278,290,300,0,0,0,300",110056,110057,"[O.N.G.E.K.I.]1.10-0072" -2,100447,"【R】日向 千夏[私服]",1014,"私服","奏坂学園","Leaf","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",110058,110059,"[O.N.G.E.K.I.]1.10-0033" -3,100448,"【SR+】日向 千夏[ひよこたちのダ・カーポ+]",1014,"ひよこたちのダ・カーポ+","奏坂学園","Leaf","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",115046,115047,"[O.N.G.E.K.I.]1.15-0039" -2,100449,"【R】東雲 つむぎ[私服]",1015,"私服","奏坂学園","Aqua","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",110060,110061,"[O.N.G.E.K.I.]1.10-0045" -3,100450,"【SR+】東雲 つむぎ[ひよこたちのダ・カーポ+]",1015,"ひよこたちのダ・カーポ+","奏坂学園","Aqua","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",115050,115051,"[O.N.G.E.K.I.]1.15-0052" -2,100451,"【N】日向 千夏",1014,"","奏坂学園","Leaf","中学2年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"[O.N.G.E.K.I.]1.10-0009" -2,100452,"【N】東雲 つむぎ",1015,"","奏坂学園","Aqua","中学2年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"[O.N.G.E.K.I.]1.10-0016" -1,100453,"【SR】茜ニャン[(世界の半分はいただくにゃん!)]",5,"(世界の半分はいただくにゃん!)","プロモーション","Fire","-",2,"1,30,35,40,45,50,0,0,0,50",105132,105133,"[O.N.G.E.K.I.]1.05-0035" -1,100454,"【SSR】星咲 あかり[1st Anniversary]",1000,"1st Anniversary","プロモーション","Fire","高校2年生",3,"52,237,252,267,282,297,0,0,0,302",105146,105146,"[O.N.G.E.K.I.]1.05-0072" -1,100455,"【SR】茜ニャン[(とりあえず爆発させとくにゃん?)]",5,"(とりあえず爆発させとくにゃん?)","プロモーション","Fire","-",2,"1,30,35,40,45,50,0,0,0,50",105134,105135,"[O.N.G.E.K.I.]1.05-0036" -1,100456,"【SR】茜ニャン[(まったくネコ使いが荒いにゃん……)]",5,"(まったくネコ使いが荒いにゃん……)","プロモーション","Fire","-",2,"1,30,35,40,45,50,0,0,0,50",105136,105137,"[O.N.G.E.K.I.]1.05-0037" -1,100457,"【R】あおニャン[松丸君]",2,"松丸君","-","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",0,0,"[O.N.G.E.K.I.]1.05-****" -2,100458,"【SSR】高瀬 梨緒[デイドリーム・フェアリーズ]",1003,"デイドリーム・フェアリーズ","奏坂学園","Aqua","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",100040,100081,"[O.N.G.E.K.I.]1.10-0002" -2,100459,"【SSR】結城 莉玖[デイドリーム・フェアリーズ]",1004,"デイドリーム・フェアリーズ","奏坂学園","Fire","高校1年生",3,"65,270,291,304,314,322,0,0,0,327",100038,100079,"[O.N.G.E.K.I.]1.10-0003" -2,100460,"【SSR】藍原 椿[デイドリーム・フェアリーズ]",1005,"デイドリーム・フェアリーズ","奏坂学園","Leaf","高校1年生",3,"65,270,291,304,314,322,0,0,0,327",100039,100080,"[O.N.G.E.K.I.]1.10-0004" -2,100461,"【SSR】高瀬 梨緒[聖夜のハッピーベル]",1003,"聖夜のハッピーベル","奏坂学園","Aqua","高校2年生",3,"65,262,285,300,312,322,0,0,0,327",110062,110063,"[O.N.G.E.K.I.]1.10-0047" -2,100462,"【SSR】藍原 椿[ハッピー・ゴー・ラウンド]",1005,"ハッピー・ゴー・ラウンド","奏坂学園","Leaf","高校1年生",3,"65,262,285,300,312,322,0,0,0,327",110064,110065,"[O.N.G.E.K.I.]1.10-0051" -2,100463,"【R】アリス・マーガトロイド[東方Project]",2018,"東方Project","東方Project","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0135" -2,100464,"【SR】アリス・マーガトロイド[七色の人形使い]",2018,"七色の人形使い","東方Project","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.10-E-0136" -2,100465,"【SSR】かなで[エンジェルズウィング]",23001,"エンジェルズウィング","Angel Beats!","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",110070,110071,"[O.N.G.E.K.I.]1.10-E-0124" -2,100466,"【SSR】かなで[水辺の天使]",23001,"水辺の天使","Angel Beats!","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",110068,110069,"[O.N.G.E.K.I.]1.10-E-0123" -2,100472,"【SR+】柏木 美亜[ガールズ・ウォッチ+]",1013,"ガールズ・ウォッチ+","奏坂学園","Fire","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",110074,110075,"[O.N.G.E.K.I.]1.10-0035" -2,100473,"【SR+】日向 千夏[大好き☆アタック+]",1014,"大好き☆アタック+","奏坂学園","Leaf","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",110076,110077,"[O.N.G.E.K.I.]1.10-0065" -2,100474,"【SR+】東雲 つむぎ[背伸びしたいお年頃+]",1015,"背伸びしたいお年頃+","奏坂学園","Aqua","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",110078,110079,"[O.N.G.E.K.I.]1.10-0037" -3,100475,"【SR+】柏木 咲姫[コスプレ喫茶へようこそ+]",1009,"コスプレ喫茶へようこそ+","奏坂学園","Aqua","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",115042,115043,"[O.N.G.E.K.I.]1.15-0006" -3,100476,"【SR+】桜井 春菜[クレープはママの味+]",1007,"クレープはママの味+","奏坂学園","Fire","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",115044,115045,"[O.N.G.E.K.I.]1.15-0015" -3,100477,"【SR+】逢坂 茜[逢坂茜の挑戦状+]",1011,"逢坂茜の挑戦状+","奏坂学園","Fire","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",110134,110135,"[O.N.G.E.K.I.]1.15-0024" -2,100483,"【R】かなで[死んだ世界戦線]",23001,"死んだ世界戦線","Angel Beats!","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.10-E-0120" -2,100484,"【R】ゆり[死んだ世界戦線]",23002,"死んだ世界戦線","Angel Beats!","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.10-E-0125" -2,100485,"【R】ユイ[死んだ世界戦線]",23003,"死んだ世界戦線","Angel Beats!","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0128" -2,100486,"【SR】かなで[ハンドソニック]",23001,"ハンドソニック","Angel Beats!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.10-E-0121" -2,100487,"【SR】ゆり[運命と神に抗う少女]",23002,"運命と神に抗う少女","Angel Beats!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.10-E-0126" -2,100488,"【SR】ユイ[Girls Dead Monster]",23003,"Girls Dead Monster","Angel Beats!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110138,110139,"[O.N.G.E.K.I.]1.10-E-0129" -1,100489,"【SSR】星咲 あかり[「オンゲキ」LIVE vol.1]",1000,"「オンゲキ」LIVE vol.1","プロモーション","Fire","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",100033,100074,"[O.N.G.E.K.I.]Special Card" -2,100490,"【SR】かなで[私は天使なんかじゃないわ]",23001,"私は天使なんかじゃないわ","Angel Beats!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110080,110081,"[O.N.G.E.K.I.]1.10-E-0122" -2,100491,"【SR】ゆり[本当は馬鹿よ]",23002,"本当は馬鹿よ","Angel Beats!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.10-E-0127" -2,100492,"【SR】ユイ[要はアホですねっ!]",23003,"要はアホですねっ!","Angel Beats!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110082,110083,"[O.N.G.E.K.I.]1.10-E-0130" -2,100493,"【R】春日部 ハル[777☆SISTERS]",24001,"777☆SISTERS","Tokyo 7th シスターズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0075" -2,100494,"【R】天堂寺 ムスビ[777☆SISTERS]",24001,"777☆SISTERS","Tokyo 7th シスターズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",105004,105005,"[O.N.G.E.K.I.]1.10-E-0078" -2,100495,"【R】角森 ロナ[777☆SISTERS]",24001,"777☆SISTERS","Tokyo 7th シスターズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0080" -2,100496,"【R】野ノ原 ヒメ [777☆SISTERS]",24001,"777☆SISTERS","Tokyo 7th シスターズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100001,100042,"[O.N.G.E.K.I.]1.10-E-0082" -2,100497,"【R】芹沢 モモカ[777☆SISTERS]",24001,"777☆SISTERS","Tokyo 7th シスターズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.10-E-0084" -2,100498,"【R】臼田 スミレ[777☆SISTERS]",24001,"777☆SISTERS","Tokyo 7th シスターズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",105112,105113,"[O.N.G.E.K.I.]1.10-E-0086" -2,100499,"【R】神城 スイ[777☆SISTERS]",24001,"777☆SISTERS","Tokyo 7th シスターズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.10-E-0088" -2,100500,"【R】久遠寺 シズカ [777☆SISTERS]",24001,"777☆SISTERS","Tokyo 7th シスターズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.10-E-0090" -2,100501,"【R】アレサンドラ・スース[777☆SISTERS]",24001,"777☆SISTERS","Tokyo 7th シスターズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.10-E-0092" -2,100502,"【R】晴海 サワラ[777☆SISTERS]",24001,"777☆SISTERS","Tokyo 7th シスターズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0094" -2,100503,"【R】晴海 カジカ[777☆SISTERS]",24001,"777☆SISTERS","Tokyo 7th シスターズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0096" -2,100504,"【R】晴海 シンジュ [777☆SISTERS]",24001,"777☆SISTERS","Tokyo 7th シスターズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110084,110085,"[O.N.G.E.K.I.]1.10-E-0098" -2,100505,"【SR】春日部 ハル[星屑☆シーカー]",24001,"星屑☆シーカー","Tokyo 7th シスターズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.10-E-0076" -2,100506,"【SR】天堂寺 ムスビ[Girls Talk!!]",24001,"Girls Talk!!","Tokyo 7th シスターズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.10-E-0079" -2,100507,"【SR】角森 ロナ[星屑☆シーカー]",24001,"星屑☆シーカー","Tokyo 7th シスターズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.10-E-0081" -2,100508,"【SR】野ノ原 ヒメ [星屑☆シーカー]",24001,"星屑☆シーカー","Tokyo 7th シスターズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100026,100067,"[O.N.G.E.K.I.]1.10-E-0083" -2,100509,"【SR】芹沢 モモカ[星屑☆シーカー]",24001,"星屑☆シーカー","Tokyo 7th シスターズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100016,100057,"[O.N.G.E.K.I.]1.10-E-0085" -2,100510,"【SR】臼田 スミレ[お願い☆My Boy]",24001,"お願い☆My Boy","Tokyo 7th シスターズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110086,110087,"[O.N.G.E.K.I.]1.10-E-0087" -2,100511,"【SR】神城 スイ[お願い☆My Boy]",24001,"お願い☆My Boy","Tokyo 7th シスターズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.10-E-0089" -2,100512,"【SR】久遠寺 シズカ [お願い☆My Boy]",24001,"お願い☆My Boy","Tokyo 7th シスターズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.10-E-0091" -2,100513,"【SR】アレサンドラ・スース[Girls Talk!!]",24001,"Girls Talk!!","Tokyo 7th シスターズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110088,110089,"[O.N.G.E.K.I.]1.10-E-0093" -2,100514,"【SR】晴海 サワラ[セカイのヒミツ]",24001,"セカイのヒミツ","Tokyo 7th シスターズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.10-E-0095" -2,100515,"【SR】晴海 カジカ[セカイのヒミツ]",24001,"セカイのヒミツ","Tokyo 7th シスターズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.10-E-0097" -2,100516,"【SR】晴海 シンジュ [セカイのヒミツ]",24001,"セカイのヒミツ","Tokyo 7th シスターズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100027,100068,"[O.N.G.E.K.I.]1.10-E-0099" -2,100517,"【SR+】春日部 ハル[スタートライン]",24001,"スタートライン","Tokyo 7th シスターズ","Fire","-",12,"53,240,263,278,290,300,0,0,0,300",110090,110091,"[O.N.G.E.K.I.]1.10-E-0077" -2,100518,"【R】草津 結衣奈[SPRiNGS]",25001,"SPRiNGS","温泉むすめ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0048" -2,100519,"【R】箱根 彩耶[SPRiNGS]",25001,"SPRiNGS","温泉むすめ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110054,110055,"[O.N.G.E.K.I.]1.10-E-0051" -2,100520,"【R】秋保 那菜子[SPRiNGS]",25001,"SPRiNGS","温泉むすめ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.10-E-0053" -2,100521,"【R】有馬 輪花[SPRiNGS]",25001,"SPRiNGS","温泉むすめ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110054,110055,"[O.N.G.E.K.I.]1.10-E-0055" -2,100522,"【R】道後 泉海[SPRiNGS]",25001,"SPRiNGS","温泉むすめ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100003,100044,"[O.N.G.E.K.I.]1.10-E-0057" -2,100523,"【R】登別 綾瀬[SPRiNGS]",25001,"SPRiNGS","温泉むすめ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0059" -2,100524,"【R】下呂 美月[SPRiNGS]",25001,"SPRiNGS","温泉むすめ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100003,100044,"[O.N.G.E.K.I.]1.10-E-0061" -2,100525,"【R】有馬 楓花[SPRiNGS]",25001,"SPRiNGS","温泉むすめ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.10-E-0063" -2,100526,"【R】奏・バーデン・由布院[SPRiNGS]",25001,"SPRiNGS","温泉むすめ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0065" -2,100527,"【SR】草津 結衣奈[SPicA]",25001,"SPicA","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.10-E-0049" -2,100528,"【SR】箱根 彩耶[雪月花]",25001,"雪月花","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110094,110095,"[O.N.G.E.K.I.]1.10-E-0052" -2,100529,"【SR】秋保 那菜子[しゃんぷーはっと]",25001,"しゃんぷーはっと","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",105052,105053,"[O.N.G.E.K.I.]1.10-E-0054" -2,100530,"【SR】有馬 輪花[雪月花]",25001,"雪月花","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.10-E-0056" -2,100531,"【SR】道後 泉海[SPicA]",25001,"SPicA","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110096,110097,"[O.N.G.E.K.I.]1.10-E-0058" -2,100532,"【SR】登別 綾瀬[しゃんぷーはっと]",25001,"しゃんぷーはっと","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110022,110023,"[O.N.G.E.K.I.]1.10-E-0060" -2,100533,"【SR】下呂 美月[しゃんぷーはっと]",25001,"しゃんぷーはっと","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100018,100059,"[O.N.G.E.K.I.]1.10-E-0062" -2,100534,"【SR】有馬 楓花[SPicA]",25001,"SPicA","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100028,100069,"[O.N.G.E.K.I.]1.10-E-0064" -2,100535,"【SR】奏・バーデン・由布院[雪月花]",25001,"雪月花","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.10-E-0066" -2,100536,"【SR】鬼怒川 日向[AKATSUKI]",25001,"AKATSUKI","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",105042,105043,"[O.N.G.E.K.I.]1.10-E-0137" -2,100537,"【SR】熱海 初夏[LUSH STAR☆]",25001,"LUSH STAR☆","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100031,100072,"[O.N.G.E.K.I.]1.10-E-0138" -2,100538,"【SR】黒川 姫楽[Adhara]",25001,"Adhara","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110022,110023,"[O.N.G.E.K.I.]1.10-E-0139" -2,100539,"【SR】伊香保 葉凪[petit corolla]",25001,"petit corolla","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",105052,105053,"[O.N.G.E.K.I.]1.10-E-0140" -2,100540,"【SR+】草津 結衣奈[ゆのはな選抜]",25001,"ゆのはな選抜","温泉むすめ","Aqua","-",12,"53,240,263,278,290,300,0,0,0,300",105010,105011,"[O.N.G.E.K.I.]1.10-E-0050" -2,100541,"【SSR】草津 結衣奈[NOW ON☆SENSATION!!]",25001,"NOW ON☆SENSATION!!","プロモーション","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",110098,110099,"Special Card" -2,100542,"【R】愛城 華恋[聖翔音楽学園 第99期生]",26001,"聖翔音楽学園 第99期生","レヴュースタァライト","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0021" -2,100543,"【R】神楽 ひかり[聖翔音楽学園 第99期生]",26002,"聖翔音楽学園 第99期生","レヴュースタァライト","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0027" -2,100544,"【SR】愛城 華恋[ポジションゼロ]",26001,"ポジションゼロ","レヴュースタァライト","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110100,110101,"[O.N.G.E.K.I.]1.10-E-0022" -2,100545,"【SR】神楽 ひかり[ポジションゼロ]",26002,"ポジションゼロ","レヴュースタァライト","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110102,110103,"[O.N.G.E.K.I.]1.10-E-0028" -2,100546,"【SR】愛城 華恋[2人のハロウィン]",26001,"2人のハロウィン","レヴュースタァライト","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100026,100067,"[O.N.G.E.K.I.]1.10-E-0023" -2,100547,"【SR】神楽 ひかり[2人のハロウィン]",26002,"2人のハロウィン","レヴュースタァライト","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",105094,105095,"[O.N.G.E.K.I.]1.10-E-0029" -2,100548,"【SR】愛城 華恋[私のキラめき]",26001,"私のキラめき","レヴュースタァライト","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.10-E-0024" -2,100549,"【SR】神楽 ひかり[私のキラめき]",26002,"私のキラめき","レヴュースタァライト","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100016,100057,"[O.N.G.E.K.I.]1.10-E-0030" -2,100550,"【SR】愛城 華恋[舞台少女は日々進化中!]",26001,"舞台少女は日々進化中!","レヴュースタァライト","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",105042,105043,"[O.N.G.E.K.I.]1.10-E-0025" -2,100551,"【SSR】愛城 華恋[Show Must Go On]",26001,"Show Must Go On","レヴュースタァライト","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",100033,100074,"[O.N.G.E.K.I.]1.10-E-0026" -2,100552,"【SSR】神楽 ひかり[『運命』の交換]",26002,"『運命』の交換","レヴュースタァライト","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",110104,110105,"[O.N.G.E.K.I.]1.10-E-0031" -2,100553,"【R】サクラ[人形の感覚]",27001,"人形の感覚","プロジェクト東京ドールズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0005" -2,100554,"【R】ミサキ[死神]",27001,"死神","プロジェクト東京ドールズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110106,110107,"[O.N.G.E.K.I.]1.10-E-0007" -2,100555,"【R】シオリ[クロユリの花言葉]",27001,"クロユリの花言葉","プロジェクト東京ドールズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110108,110109,"[O.N.G.E.K.I.]1.10-E-0009" -2,100556,"【R】レイナ[美しき狩人]",27001,"美しき狩人","プロジェクト東京ドールズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110108,110109,"[O.N.G.E.K.I.]1.10-E-0011" -2,100557,"【R】ヒヨ[もう一人の、ワタシ]",27001,"もう一人の、ワタシ","プロジェクト東京ドールズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110106,110107,"[O.N.G.E.K.I.]1.10-E-0013" -2,100558,"【R】ナナミ[魂の叫喚]",27001,"魂の叫喚","プロジェクト東京ドールズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0015" -2,100559,"【R】アヤ[人形の本懐]",27001,"人形の本懐","プロジェクト東京ドールズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110108,110109,"[O.N.G.E.K.I.]1.10-E-0017" -2,100560,"【R】ユキ[人形の呪縛]",27001,"人形の呪縛","プロジェクト東京ドールズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0001" -2,100561,"【R】ヤマダ[全てを壊すもの]",27001,"全てを壊すもの","プロジェクト東京ドールズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110106,110107,"[O.N.G.E.K.I.]1.10-E-0019" -2,100562,"【SR】サクラ[弔イノ仇華]",27001,"弔イノ仇華","プロジェクト東京ドールズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.10-E-0006" -2,100563,"【SR】ミサキ[陰掃ウ眼光]",27001,"陰掃ウ眼光","プロジェクト東京ドールズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110110,110111,"[O.N.G.E.K.I.]1.10-E-0008" -2,100564,"【SR】シオリ[罪ヲ嘆ク聲]",27001,"罪ヲ嘆ク聲","プロジェクト東京ドールズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100026,100067,"[O.N.G.E.K.I.]1.10-E-0010" -2,100565,"【SR】レイナ[深淵ヲ穿ツ]",27001,"深淵ヲ穿ツ","プロジェクト東京ドールズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100028,100069,"[O.N.G.E.K.I.]1.10-E-0012" -2,100566,"【SR】ヒヨ[鮮血ノ暴風]",27001,"鮮血ノ暴風","プロジェクト東京ドールズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110110,110111,"[O.N.G.E.K.I.]1.10-E-0014" -2,100567,"【SR】ナナミ[哀切ノ檻]",27001,"哀切ノ檻","プロジェクト東京ドールズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.10-E-0016" -2,100568,"【SR】アヤ[暗闇ヲ照ラス]",27001,"暗闇ヲ照ラス","プロジェクト東京ドールズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100027,100068,"[O.N.G.E.K.I.]1.10-E-0018" -2,100569,"【SR】ユキ[血涙ヲ注グ]",27001,"血涙ヲ注グ","プロジェクト東京ドールズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.10-E-0002" -2,100570,"【SR】ヤマダ[爛レル猛毒]",27001,"爛レル猛毒","プロジェクト東京ドールズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110110,110111,"[O.N.G.E.K.I.]1.10-E-0020" -2,100571,"【SSR】ユキ[静謐の花園]",27001,"静謐の花園","プロジェクト東京ドールズ","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",110112,110113,"[O.N.G.E.K.I.]1.10-E-0004" -2,100572,"【SR+】ユキ[Sweet♡Wedding]",27001,"Sweet♡Wedding","プロジェクト東京ドールズ","Leaf","-",12,"53,240,263,278,290,300,0,0,0,300",105010,105011,"[O.N.G.E.K.I.]1.10-E-0003" -2,100573,"【R】明坂 芹菜[イロドリミドリ]",6001,"イロドリミドリ","イロドリミドリ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0105" -2,100574,"【R】御形 アリシアナ[イロドリミドリ]",6001,"イロドリミドリ","イロドリミドリ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110084,110085,"[O.N.G.E.K.I.]1.10-E-0108" -2,100575,"【R】天王洲 なずな[イロドリミドリ]",6001,"イロドリミドリ","イロドリミドリ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100032,100073,"[O.N.G.E.K.I.]1.10-E-0110" -2,100576,"【R】小仏 凪[イロドリミドリ]",6001,"イロドリミドリ","イロドリミドリ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110142,110143,"[O.N.G.E.K.I.]1.10-E-0112" -2,100577,"【R】箱部 なる[イロドリミドリ]",6001,"イロドリミドリ","イロドリミドリ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0114" -2,100578,"【R】月鈴 那知[イロドリミドリ]",6001,"イロドリミドリ","イロドリミドリ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0116" -2,100579,"【R】月鈴 白奈[イロドリミドリ]",6001,"イロドリミドリ","イロドリミドリ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100002,100043,"[O.N.G.E.K.I.]1.10-E-0118" -2,100580,"【SR】明坂 芹菜[試練の三送会編]",6001,"試練の三送会編","イロドリミドリ","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",100025,100066,"[O.N.G.E.K.I.]1.10-E-0106" -2,100581,"【SR】御形 アリシアナ[試練の三送会編]",6001,"試練の三送会編","イロドリミドリ","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",100025,100066,"[O.N.G.E.K.I.]1.10-E-0109" -2,100582,"【SR】天王洲 なずな[試練の三送会編]",6001,"試練の三送会編","イロドリミドリ","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",110114,110115,"[O.N.G.E.K.I.]1.10-E-0111" -2,100583,"【SR】小仏 凪[試練の三送会編]",6001,"試練の三送会編","イロドリミドリ","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",100027,100068,"[O.N.G.E.K.I.]1.10-E-0113" -2,100584,"【SR】箱部 なる[試練の三送会編]",6001,"試練の三送会編","イロドリミドリ","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",110116,110117,"[O.N.G.E.K.I.]1.10-E-0115" -2,100585,"【SR】月鈴 那知[試練の三送会編]",6001,"試練の三送会編","イロドリミドリ","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",110116,110117,"[O.N.G.E.K.I.]1.10-E-0117" -2,100586,"【SR】月鈴 白奈[試練の三送会編]",6001,"試練の三送会編","イロドリミドリ","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",100017,100058,"[O.N.G.E.K.I.]1.10-E-0119" -2,100587,"【SR+】明坂 芹菜[7つのメロディ]",6001,"7つのメロディ","イロドリミドリ","Leaf","-",12,"53,240,263,278,290,300,0,0,0,300",110118,110119,"[O.N.G.E.K.I.]1.10-E-0107" -2,100588,"【SR】博麗 霊夢[Halloween]",2000,"Halloween","東方Project","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.10-E-0068" -2,100589,"【SR】霧雨 魔理沙[Halloween]",2001,"Halloween","東方Project","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100027,100068,"[O.N.G.E.K.I.]1.10-E-0069" -2,100590,"【SR】紅 美鈴[Halloween]",2005,"Halloween","東方Project","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.10-E-0073" -2,100591,"【SR】パチュリー・ノーレッジ[Halloween]",2006,"Halloween","東方Project","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110082,110083,"[O.N.G.E.K.I.]1.10-E-0074" -2,100592,"【SR】十六夜 咲夜[Halloween]",2002,"Halloween","東方Project","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110082,110083,"[O.N.G.E.K.I.]1.10-E-0070" -2,100593,"【SR】レミリア・スカーレット[Halloween]",2003,"Halloween","東方Project","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100026,100067,"[O.N.G.E.K.I.]1.10-E-0071" -2,100594,"【SR】フランドール・スカーレット[Halloween]",2004,"Halloween","東方Project","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110122,110123,"[O.N.G.E.K.I.]1.10-E-0072" -2,100595,"【SR+】博麗 霊夢[幻想郷のハロウィン]",2000,"幻想郷のハロウィン","東方Project","Fire","-",12,"53,240,263,278,290,300,0,0,0,300",110124,110125,"[O.N.G.E.K.I.]1.10-E-0067" -2,100596,"【R】キズナアイ[バーチャルYouTuber]",28001,"バーチャルYouTuber","キズナアイ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.10-E-0032" -2,100597,"【SR】キズナアイ[ぴょこぴょこ]",28001,"ぴょこぴょこ","キズナアイ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100029,100070,"[O.N.G.E.K.I.]1.10-E-0033" -2,100598,"【SR】キズナアイ[インテリジェントなスーパーAI]",28001,"インテリジェントなスーパーAI","キズナアイ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100026,100067,"[O.N.G.E.K.I.]1.10-E-0034" -2,100599,"【SR+】キズナアイ[はい、どうもー!]",28001,"はい、どうもー!","キズナアイ","Fire","-",12,"53,240,263,278,290,300,0,0,0,300",105010,105011,"[O.N.G.E.K.I.]1.10-E-0035" -2,100600,"【SR+】初音ミク[Project DIVA - Future Tone -]",5000,"Project DIVA - Future Tone -","バーチャル・シンガー","Aqua","-",12,"53,240,263,278,290,300,0,0,0,300",110126,110127,"[O.N.G.E.K.I.]1.10-E-0134" -2,100601,"【SR】初春 飾利[私だって風紀委員なんだから!]",21003,"私だって風紀委員なんだから!","とある科学の超電磁砲S","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",105096,105097,"[O.N.G.E.K.I.]1.10-E-0103" -2,100602,"【SR】御坂 美琴[逃げられるとでも思ってんの?]",21001,"逃げられるとでも思ってんの?","とある科学の超電磁砲S","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110128,110129,"[O.N.G.E.K.I.]1.10-E-0100" -2,100603,"【SR】佐天 涙子[どーんといってみよーか!]",21004,"どーんといってみよーか!","とある科学の超電磁砲S","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.10-E-0104" -2,100604,"【SR】白井 黒子[おっねえさまーんっ!]",21002,"おっねえさまーんっ!","とある科学の超電磁砲S","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110082,110083,"[O.N.G.E.K.I.]1.10-E-0102" -2,100605,"【SR+】御坂 美琴[じゃあ行くよっ!]",21001,"じゃあ行くよっ!","とある科学の超電磁砲S","Leaf","-",12,"53,240,263,278,290,300,0,0,0,300",110056,110057,"[O.N.G.E.K.I.]1.10-E-0101" -2,100606,"【SR】星咲 あかり[最強 the サマータイム!!!!!]",1000,"最強 the サマータイム!!!!!","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",110130,110131,"[O.N.G.E.K.I.]1.10-0073" -2,100607,"【SR】高瀬 梨緒[最強 the サマータイム!!!!!]",1003,"最強 the サマータイム!!!!!","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",110130,110131,"[O.N.G.E.K.I.]1.10-0074" -2,100608,"【SR】柏木 美亜[最強 the サマータイム!!!!!]",1013,"最強 the サマータイム!!!!!","奏坂学園","Fire","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",110130,110131,"[O.N.G.E.K.I.]1.10-0022" -2,100609,"【SR】日向 千夏[最強 the サマータイム!!!!!]",1014,"最強 the サマータイム!!!!!","奏坂学園","Leaf","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",110130,110131,"[O.N.G.E.K.I.]1.10-0012" -2,100610,"【SR】東雲 つむぎ[最強 the サマータイム!!!!!]",1015,"最強 the サマータイム!!!!!","奏坂学園","Aqua","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",110130,110131,"[O.N.G.E.K.I.]1.10-0026" -2,100611,"【SSR】日向 千夏[ONGEKI Sound Collection 02]",1014,"ONGEKI Sound Collection 02","プロモーション","Leaf","中学2年生",3,"65,262,285,300,312,322,0,0,0,327",105144,105145,"[O.N.G.E.K.I.]1.10-P0001" -2,100612,"【SR+】日向 千夏[ONGEKI Memorial Soundtrack Himawari]",1014,"ONGEKI Memorial Soundtrack Himawari","奏坂学園","Leaf","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",110132,110133,"[O.N.G.E.K.I.]1.10-0075" -2,100613,"【SR+】柏木 美亜[ONGEKI Vocal Collection 06]",1013,"ONGEKI Vocal Collection 06","プロモーション","Fire","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",110134,110135,"[O.N.G.E.K.I.]1.10-P0003" -2,100614,"【SR+】日向 千夏[ONGEKI Vocal Collection 06]",1014,"ONGEKI Vocal Collection 06","プロモーション","Leaf","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",110134,110135,"[O.N.G.E.K.I.]1.10-P0002" -2,100615,"【SR+】東雲 つむぎ[ONGEKI Vocal Collection 06]",1015,"ONGEKI Vocal Collection 06","プロモーション","Aqua","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",110134,110135,"[O.N.G.E.K.I.]1.10-P0004" -3,100616,"【SR+】早乙女 彩華[ONGEKI Vocal Collection 07]",1006,"ONGEKI Vocal Collection 07","プロモーション","Aqua","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",110146,110147,"[O.N.G.E.K.I.]1.15-P-0002" -3,100617,"【SR+】井之原 小星[ONGEKI Vocal Collection 07]",1010,"ONGEKI Vocal Collection 07","プロモーション","Leaf","高校1年生",12,"53,240,263,278,290,300,0,0,0,300",110146,110147,"[O.N.G.E.K.I.]1.15-P-0003" -3,100618,"【SR+】逢坂 茜[ONGEKI Vocal Collection 07]",1011,"ONGEKI Vocal Collection 07","プロモーション","Fire","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",110146,110147,"[O.N.G.E.K.I.]1.15-P-0001" -4,100619,"【R】射命丸 文[東方Project]",2019,"東方Project","東方Project","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0137" -4,100620,"【R】古明地 さとり[東方Project]",2020,"東方Project","東方Project 古明地姉妹編","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0071" -4,100621,"【R】古明地 こいし[東方Project]",2021,"東方Project","東方Project 古明地姉妹編","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100001,100042,"[O.N.G.E.K.I.]1.20-E-0074" -4,100622,"【SR】射命丸 文[里に最も近い天狗]",2019,"里に最も近い天狗","東方Project","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0138" -4,100623,"【SR】古明地 さとり[怨霊も恐れ怯む少女]",2020,"怨霊も恐れ怯む少女","東方Project 古明地姉妹編","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110136,110137,"[O.N.G.E.K.I.]1.20-E-0072" -4,100624,"【SR】古明地 こいし[閉じた恋の瞳]",2021,"閉じた恋の瞳","東方Project 古明地姉妹編","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0075" -4,100625,"【SSR】射命丸 文[「無双風神」]",2019,"「無双風神」","東方Project","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",110070,110071,"[O.N.G.E.K.I.]1.20-E-0191" -4,100626,"【SSR】古明地 さとり[想起「テリブルスーヴニール」]",2020,"想起「テリブルスーヴニール」","東方Project 古明地姉妹編","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",115030,115031,"[O.N.G.E.K.I.]1.20-E-0073" -4,100627,"【SSR】古明地 こいし[本能「イドの解放」]",2021,"本能「イドの解放」","東方Project 古明地姉妹編","Fire","-",3,"60,265,286,299,309,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]1.20-E-0076" -3,100631,"【SR+】星咲 あかり[仲直りのフィナーレ+]",1000,"仲直りのフィナーレ+","奏坂学園","Fire","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",115054,115055,"[O.N.G.E.K.I.]1.15-0054" -3,100632,"【SR+】藤沢 柚子[仲直りのフィナーレ+]",1001,"仲直りのフィナーレ+","奏坂学園","Leaf","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",115056,115057,"[O.N.G.E.K.I.]1.15-0064" -3,100633,"【SR+】三角 葵[仲直りのフィナーレ+]",1002,"仲直りのフィナーレ+","奏坂学園","Aqua","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",115058,115059,"[O.N.G.E.K.I.]1.15-0076" -3,100634,"【SR】日向 千夏[ひよこたちのダ・カーポ]",1014,"ひよこたちのダ・カーポ","奏坂学園","Leaf","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",100025,100066,"[O.N.G.E.K.I.]1.15-0040" -3,100636,"【SR】東雲 つむぎ[ひよこたちのダ・カーポ]",1015,"ひよこたちのダ・カーポ","奏坂学園","Aqua","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",100025,100066,"[O.N.G.E.K.I.]1.15-0041" -3,100637,"【SR】星咲 あかり[仲直りのフィナーレ]",1000,"仲直りのフィナーレ","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",115052,115053,"[O.N.G.E.K.I.]1.15-0055" -3,100638,"【SR】藤沢 柚子[仲直りのフィナーレ]",1001,"仲直りのフィナーレ","奏坂学園","Leaf","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",115052,115053,"[O.N.G.E.K.I.]1.15-0057" -3,100639,"【SR】三角 葵[仲直りのフィナーレ]",1002,"仲直りのフィナーレ","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",115052,115053,"[O.N.G.E.K.I.]1.15-0059" -2,100640,"【SR】柏木 美亜[いきなりシューターフェス決勝!]",1013,"いきなりシューターフェス決勝!","奏坂学園","Fire","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",110116,110117,"[O.N.G.E.K.I.]1.10-0007" -2,100641,"【SR】日向 千夏[いきなりシューターフェス決勝!]",1014,"いきなりシューターフェス決勝!","奏坂学園","Leaf","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",110116,110117,"[O.N.G.E.K.I.]1.10-0013" -2,100642,"【SR】東雲 つむぎ[いきなりシューターフェス決勝!]",1015,"いきなりシューターフェス決勝!","奏坂学園","Aqua","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",110116,110117,"[O.N.G.E.K.I.]1.10-0018" -2,100643,"【SR】珠洲島 有栖[ぷれぜんと・ふぉー・ゆー]",1012,"ぷれぜんと・ふぉー・ゆー","奏坂学園","Aqua","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",105042,105043,"[O.N.G.E.K.I.]1.10-0071" -2,100644,"【SR】柏木 美亜[ガールズ・ウォッチ]",1013,"ガールズ・ウォッチ","奏坂学園","Fire","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",110136,110137,"[O.N.G.E.K.I.]1.10-0034" -2,100645,"【SR】日向 千夏[大好き☆アタック]",1014,"大好き☆アタック","奏坂学園","Leaf","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",110138,110139,"[O.N.G.E.K.I.]1.10-0064" -2,100646,"【SR】東雲 つむぎ[背伸びしたいお年頃]",1015,"背伸びしたいお年頃","奏坂学園","Aqua","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",110140,110141,"[O.N.G.E.K.I.]1.10-0036" -3,100647,"【R】柏木 美亜[私服 表情:ふしゃぁぁぁぁっ!]",1013,"私服 表情:ふしゃぁぁぁぁっ!","奏坂学園","Fire","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",110058,110059,"[O.N.G.E.K.I.]1.15-0010" -3,100648,"【R】日向 千夏[私服 表情:ぎゅっ]",1014,"私服 表情:ぎゅっ","奏坂学園","Leaf","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",110060,110061,"[O.N.G.E.K.I.]1.15-0021" -3,100649,"【R】東雲 つむぎ[私服 表情:憤り]",1015,"私服 表情:憤り","奏坂学園","Aqua","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",110058,110059,"[O.N.G.E.K.I.]1.15-0046" -3,100650,"【R】柏木 美亜[シュータードレス:全身ver.]",1013,"シュータードレス:全身ver.","奏坂学園","Fire","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",115148,115149,"[O.N.G.E.K.I.]1.15-0011" -3,100651,"【R】日向 千夏[シュータードレス:全身ver.]",1014,"シュータードレス:全身ver.","奏坂学園","Leaf","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",110084,110085,"[O.N.G.E.K.I.]1.15-0012" -3,100652,"【R】東雲 つむぎ[シュータードレス:全身ver.]",1015,"シュータードレス:全身ver.","奏坂学園","Aqua","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",115146,115147,"[O.N.G.E.K.I.]1.15-0013" -2,100653,"【R】柏木 美亜[制服 表情:にゃふふふふ♡]",1013,"制服 表情:にゃふふふふ♡","奏坂学園","Fire","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",110142,110143,"[O.N.G.E.K.I.]1.10-0021" -2,100654,"【R】日向 千夏[制服 表情:わーい!]",1014,"制服 表情:わーい!","奏坂学園","Leaf","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",100032,100073,"[O.N.G.E.K.I.]1.10-0011" -2,100655,"【R】東雲 つむぎ[制服 表情:得意げ]",1015,"制服 表情:得意げ","奏坂学園","Aqua","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",105112,105113,"[O.N.G.E.K.I.]1.10-0025" -2,100656,"【SR】初音ミク[Project DIVA - f -]",5000,"Project DIVA - f -","バーチャル・シンガー","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.10-E-0131" -2,100657,"【SR】初音ミク[Project DIVA - F 2nd -]",5000,"Project DIVA - F 2nd -","バーチャル・シンガー","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110144,110145,"[O.N.G.E.K.I.]1.10-E-0132" -2,100658,"【SR】初音ミク[Project DIVA - X -]",5000,"Project DIVA - X -","バーチャル・シンガー","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100028,100069,"[O.N.G.E.K.I.]1.10-E-0133" -2,100659,"【SSR】星咲 あかり[「これから」を奏でていこうよ!!]",1000,"「これから」を奏でていこうよ!!","プロモーション","Fire","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"Special Card" -2,100660,"【SSR】藤沢 柚子[「これから」を奏でていこうよ!!]",1001,"「これから」を奏でていこうよ!!","プロモーション","Leaf","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",100033,100074,"Special Card" -3,100661,"【SSR】三角 葵[「これから」を奏でていこうよ!!]",1002,"「これから」を奏でていこうよ!!","プロモーション","Aqua","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",100033,100074,"Special Card" -3,100662,"【R】日向 千夏[体操着]",1014,"体操着","奏坂学園","Leaf","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.15-0044" -3,100663,"【R】東雲 つむぎ[体操着]",1015,"体操着","奏坂学園","Aqua","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.15-0022" -3,100664,"【R】柏木 美亜[体操着]",1013,"体操着","奏坂学園","Fire","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.15-0050" -3,100665,"【SR】柏木 咲姫[コスプレ喫茶へようこそ]",1009,"コスプレ喫茶へようこそ","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",110116,110117,"[O.N.G.E.K.I.]1.15-0008" -3,100666,"【SR】桜井 春菜[クレープはママの味]",1007,"クレープはママの味","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",110116,110117,"[O.N.G.E.K.I.]1.15-0004" -3,100667,"【SR】逢坂 茜[逢坂茜の挑戦状]",1011,"逢坂茜の挑戦状","奏坂学園","Fire","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",110116,110117,"[O.N.G.E.K.I.]1.15-0025" -3,100681,"【SR+】高瀬 梨緒[うきうきサイクリング+]",1003,"うきうきサイクリング+","奏坂学園","Aqua","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",115060,115061,"[O.N.G.E.K.I.]1.15-0016" -3,100682,"【SR+】井之原 小星[暑がりクールタイム+]",1010,"暑がりクールタイム+","奏坂学園","Leaf","高校1年生",12,"53,240,263,278,290,300,0,0,0,300",110066,110067,"[O.N.G.E.K.I.]1.15-0074" -3,100688,"【SR】高瀬 梨緒[うきうきサイクリング]",1003,"うきうきサイクリング","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100025,100066,"[O.N.G.E.K.I.]1.15-0017" -3,100691,"【R】九條 楓[水着]",1008,"水着","奏坂学園","Leaf","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",115138,115139,"[O.N.G.E.K.I.]1.15-0073" -3,100692,"【R】星咲 あかり[水着]",1000,"水着","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",115138,115139,"[O.N.G.E.K.I.]1.15-0056" -3,100693,"【R】藤沢 柚子[水着]",1001,"水着","奏坂学園","Leaf","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",115138,115139,"[O.N.G.E.K.I.]1.15-0058" -3,100694,"【R】三角 葵[水着]",1002,"水着","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",115138,115139,"[O.N.G.E.K.I.]1.15-0060" -3,100695,"【SSR】柏木 咲姫[絢爛たる正義の審判]",1009,"絢爛たる正義の審判","奏坂学園","Aqua","高校3年生",3,"65,262,285,300,312,322,0,0,0,327",115122,115123,"[O.N.G.E.K.I.]1.15-0027" -3,100696,"【SSR】柏木 美亜[混沌たる欲深き災厄]",1013,"混沌たる欲深き災厄","奏坂学園","Fire","中学2年生",3,"65,262,285,300,312,322,0,0,0,327",115128,115129,"[O.N.G.E.K.I.]1.15-0034" -4,100701,"【R】高瀬 梨緒[水着]",1003,"水着","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",115138,115139,"[O.N.G.E.K.I.]1.20-0001" -4,100711,"【SR】結城 莉玖[きーーーーん!]",1004,"きーーーーん!","奏坂学園","Fire","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",105044,105045,"[O.N.G.E.K.I.]1.20-0002" -4,100712,"【SR】珠洲島 有栖[小さな建築家]",1012,"小さな建築家","奏坂学園","Aqua","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",115080,115081,"[O.N.G.E.K.I.]1.20-0008" -4,100725,"【SSR】藍原 椿[宵闇シークレット]",1005,"宵闇シークレット","奏坂学園","Leaf","高校1年生",3,"65,270,291,304,314,322,0,0,0,327",115124,115125,"[O.N.G.E.K.I.]1.20-0003" -4,100726,"【SSR】桜井 春菜[ひと夏の思い出]",1007,"ひと夏の思い出","奏坂学園","Fire","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",115120,115121,"[O.N.G.E.K.I.]1.20-0006" -3,100729,"【SSR】柏木 咲姫[デイドリーム・フェアリーズ]",1009,"デイドリーム・フェアリーズ","奏坂学園","Aqua","高校3年生",3,"65,270,291,304,314,322,0,0,0,327",100040,100081,"[O.N.G.E.K.I.]1.15-0005" -3,100730,"【SSR】井之原 小星[デイドリーム・フェアリーズ]",1010,"デイドリーム・フェアリーズ","奏坂学園","Leaf","高校1年生",3,"65,270,291,304,314,322,0,0,0,327",100039,100080,"[O.N.G.E.K.I.]1.15-0009" -4,100740,"【SR】星咲 あかり[Splash Dance!!]",1000,"Splash Dance!!","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",115084,115085,"[O.N.G.E.K.I.]Special Card" -4,100741,"【SR】高瀬 梨緒[Splash Dance!!]",1003,"Splash Dance!!","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",115084,115085,"[O.N.G.E.K.I.]Special Card" -4,100742,"【SR】桜井 春菜[Splash Dance!!]",1007,"Splash Dance!!","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",115084,115085,"[O.N.G.E.K.I.]Special Card" -4,100743,"【SR】井之原 小星[Splash Dance!!]",1010,"Splash Dance!!","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",115084,115085,"[O.N.G.E.K.I.]Special Card" -4,100744,"【SR】日向 千夏[Splash Dance!!]",1014,"Splash Dance!!","奏坂学園","Leaf","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",115084,115085,"[O.N.G.E.K.I.]Special Card" -3,100745,"【SSR】日向 千夏[ONGEKI Sound Collection 03]",1014,"ONGEKI Sound Collection 03","プロモーション","Leaf","中学2年生",3,"65,262,285,300,312,322,0,0,0,327",115026,115027,"[O.N.G.E.K.I.]1.15-P-0004" -3,100746,"【SR+】藍原 椿[ONGEKI Vocal Party 01]",1005,"ONGEKI Vocal Party 01","プロモーション","Leaf","高校1年生",12,"53,240,263,278,290,300,0,0,0,300",115090,115091,"[O.N.G.E.K.I.]VP01-0001" -3,100747,"【SR+】珠洲島 有栖[ONGEKI Vocal Party 01]",1012,"ONGEKI Vocal Party 01","プロモーション","Aqua","高校1年生",12,"53,240,263,278,290,300,0,0,0,300",115092,115093,"[O.N.G.E.K.I.]VP01-0002" -3,100748,"【SR+】柏木 美亜[ONGEKI Vocal Party 01]",1013,"ONGEKI Vocal Party 01","プロモーション","Fire","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",115094,115095,"[O.N.G.E.K.I.]VP01-0003" -3,100749,"【SR+】結城 莉玖[ONGEKI Vocal Party 02]",1004,"ONGEKI Vocal Party 02","プロモーション","Fire","高校1年生",12,"53,240,263,278,290,300,0,0,0,300",115026,115027,"[O.N.G.E.K.I.]VP02-0002" -3,100750,"【SR+】柏木 咲姫[ONGEKI Vocal Party 02]",1009,"ONGEKI Vocal Party 02","プロモーション","Aqua","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",115026,115027,"[O.N.G.E.K.I.]VP02-0003" -3,100751,"【SR+】藤沢 柚子[ONGEKI Vocal Party 02]",1001,"ONGEKI Vocal Party 02","プロモーション","Leaf","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",115026,115027,"[O.N.G.E.K.I.]VP02-0001" -4,100752,"【SR+】逢坂 茜[ONGEKI Memorial Soundtrack Momiji]",1011,"ONGEKI Memorial Soundtrack Momiji","奏坂学園","Fire","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",100006,100047,"[O.N.G.E.K.I.]Special Card" -3,100754,"【R】逢坂 茜[CHUNITHM衣装]",1011,"CHUNITHM衣装","奏坂学園","Fire","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",115140,115141,"[O.N.G.E.K.I.]1.15-0032" -3,100755,"【R】柏木 咲姫[CHUNITHM衣装]",1009,"CHUNITHM衣装","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",115140,115141,"[O.N.G.E.K.I.]1.15-0028" -3,100756,"【R】柏木 美亜[CHUNITHM衣装]",1013,"CHUNITHM衣装","奏坂学園","Fire","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",115140,115141,"[O.N.G.E.K.I.]1.15-0035" -3,100757,"【SR】桜井 春菜[くるくるパニック]",1007,"くるくるパニック","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",105042,105043,"[O.N.G.E.K.I.]1.15-0003" -3,100758,"【SR】柏木 咲姫[二人きり]",1009,"二人きり","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",115064,115065,"[O.N.G.E.K.I.]1.15-0007" -3,100759,"【SR】東雲 つむぎ[夜更かし羊は今日もおねむ]",1015,"夜更かし羊は今日もおねむ","奏坂学園","Aqua","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",115072,115073,"[O.N.G.E.K.I.]1.15-0045" -3,100760,"【SSR】藤沢 柚子[DREAM PARADE]",1001,"DREAM PARADE","奏坂学園","Leaf","高校2年生",3,"65,262,285,300,312,322,0,0,0,327",115116,115117,"[O.N.G.E.K.I.]1.15-0001" -3,100761,"【SSR】三角 葵[DREAM PARADE]",1002,"DREAM PARADE","奏坂学園","Aqua","高校2年生",3,"65,262,285,300,312,322,0,0,0,327",115116,115117,"[O.N.G.E.K.I.]1.15-0002" -3,100762,"【SR】九條 楓[キメてゴージャス]",1008,"キメてゴージャス","奏坂学園","Leaf","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",105044,105045,"[O.N.G.E.K.I.]1.15-0042" -3,100763,"【SR】早乙女 彩華[そっと目を閉じて]",1006,"そっと目を閉じて","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",115074,115075,"[O.N.G.E.K.I.]1.15-0048" -3,100764,"【SR】日向 千夏[はつらつブーケトス]",1014,"はつらつブーケトス","奏坂学園","Leaf","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",115076,115077,"[O.N.G.E.K.I.]1.15-0051" -3,100765,"【SR】藤沢 柚子[花びらキャッチング]",1001,"花びらキャッチング","奏坂学園","Leaf","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",115066,115067,"[O.N.G.E.K.I.]1.15-0018" -3,100766,"【SR】藍原 椿[はい、あ~ん♪]",1005,"はい、あ~ん♪","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",105042,105043,"[O.N.G.E.K.I.]1.15-0019" -3,100767,"【SSR】柏木 咲姫[祝福のマリアージュ]",1009,"祝福のマリアージュ","奏坂学園","Aqua","高校3年生",3,"65,262,285,300,312,322,0,0,0,327",115120,115121,"[O.N.G.E.K.I.]1.15-0049" -3,100768,"【SSR】井之原 小星[パンダ親分はサボりたい]",1010,"パンダ親分はサボりたい","奏坂学園","Leaf","高校1年生",3,"65,262,285,300,312,322,0,0,0,327",115120,115121,"[O.N.G.E.K.I.]1.15-0043" -3,100769,"【R】藤沢 柚子[浴衣]",1001,"浴衣","奏坂学園","Leaf","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",115142,115143,"[O.N.G.E.K.I.]1.15-0063" -3,100770,"【R】早乙女 彩華[浴衣]",1006,"浴衣","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",115142,115143,"[O.N.G.E.K.I.]1.15-0070" -4,100771,"【R】早乙女 彩華[水着]",1006,"水着","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",115138,115139,"[O.N.G.E.K.I.]1.20-0004" -3,100772,"【SR】柏木 美亜[ひよこたちのダ・カーポ]",1013,"ひよこたちのダ・カーポ","奏坂学園","Fire","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",100025,100066,"[O.N.G.E.K.I.]1.15-0038" -3,100773,"【SR+】柏木 美亜[ひよこたちのダ・カーポ+]",1013,"ひよこたちのダ・カーポ+","奏坂学園","Fire","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",115048,115049,"[O.N.G.E.K.I.]1.15-0047" -3,100774,"【R】井之原 小星[CHUNITHM衣装]",1010,"CHUNITHM衣装","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",115144,115145,"[O.N.G.E.K.I.]1.15-0030" -3,100775,"【R】星咲 あかり[CHUNITHM衣装]",1000,"CHUNITHM衣装","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",115144,115145,"[O.N.G.E.K.I.]1.15-0026" -3,100776,"【R】珠洲島 有栖[CHUNITHM衣装]",1012,"CHUNITHM衣装","奏坂学園","Aqua","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",115144,115145,"[O.N.G.E.K.I.]1.15-0033" -3,100777,"【SR】高瀬 梨緒[短冊に願いを]",1003,"短冊に願いを","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",115136,115137,"[O.N.G.E.K.I.]1.15-0061" -3,100778,"【SR】柏木 美亜[蛍火センチメンタル]",1013,"蛍火センチメンタル","奏坂学園","Fire","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",115078,115079,"[O.N.G.E.K.I.]1.15-0062" -3,100779,"【SSR】逢坂 茜[ひらり、宵桜]",1011,"ひらり、宵桜","奏坂学園","Fire","高校3年生",3,"65,262,285,300,312,322,0,0,0,327",115118,115119,"[O.N.G.E.K.I.]1.15-0020" -3,100780,"【SSR】星咲 あかり[届けて!ミルキーウェイ]",1000,"届けて!ミルキーウェイ","奏坂学園","Fire","高校2年生",3,"65,262,285,300,312,322,0,0,0,327",110062,110063,"[O.N.G.E.K.I.]1.15-0053" -3,100781,"【SSR】藤沢 柚子[オリエンタル・サマーナイト]",1001,"オリエンタル・サマーナイト","奏坂学園","Leaf","高校2年生",3,"65,262,285,300,312,322,0,0,0,327",115130,115131,"[O.N.G.E.K.I.]1.15-0065" -3,100782,"【SSR】早乙女 彩華[オリエンタル・サマーナイト]",1006,"オリエンタル・サマーナイト","奏坂学園","Aqua","高校3年生",3,"65,262,285,300,312,322,0,0,0,327",115132,115133,"[O.N.G.E.K.I.]1.15-0071" -3,100783,"【SR】逢坂 茜[魔王(仮)、爆誕!]",1011,"魔王(仮)、爆誕!","奏坂学園","Fire","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",115068,115069,"[O.N.G.E.K.I.]1.15-0031" -3,100784,"【SSR】日向 千夏[CHINATSU DiVE]",1014,"CHINATSU DiVE","奏坂学園","Leaf","中学2年生",3,"65,262,285,300,312,322,0,0,0,327",115122,115123,"[O.N.G.E.K.I.]1.15-0036" -5,100785,"【SR】三角 葵[夢見るマーメイド]",1002,"夢見るマーメイド","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",125064,125065,"[O.N.G.E.K.I.]1.25-0034" -5,100786,"【SR】星咲 あかり[るんるんスキップ]",1000,"るんるんスキップ","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",125062,125063,"[O.N.G.E.K.I.]1.25-0033" -5,100787,"【SSR】東雲 つむぎ[シンデレラ・マジック]",1015,"シンデレラ・マジック","奏坂学園","Aqua","中学2年生",3,"65,270,291,304,314,322,0,0,0,327",125044,125045,"[O.N.G.E.K.I.]1.25-0035" -3,100788,"【SSR】星咲 あかり[ずっと、夏!]",1000,"ずっと、夏!","プロモーション","Fire","高校2年生",3,"55,252,275,290,302,312,0,0,0,317",110070,110071,"[O.N.G.E.K.I.]Special Card" -3,100789,"【SSR】高瀬 梨緒[ずっと、夏!]",1003,"ずっと、夏!","プロモーション","Aqua","高校2年生",3,"55,252,275,290,302,312,0,0,0,317",110070,110071,"[O.N.G.E.K.I.]Special Card" -3,100790,"【SSR】日向 千夏[ずっと、夏!]",1014,"ずっと、夏!","プロモーション","Leaf","中学2年生",3,"55,252,275,290,302,312,0,0,0,317",110070,110071,"[O.N.G.E.K.I.]Special Card" -3,100791,"【SSR】柏木 美亜[ずっと、夏!]",1013,"ずっと、夏!","プロモーション","Fire","中学2年生",3,"55,252,275,290,302,312,0,0,0,317",110070,110071,"[O.N.G.E.K.I.]Special Card" -3,100792,"【SSR】東雲 つむぎ[ずっと、夏!]",1015,"ずっと、夏!","プロモーション","Aqua","中学2年生",3,"55,252,275,290,302,312,0,0,0,317",110070,110071,"[O.N.G.E.K.I.]Special Card" -3,100793,"【SSR】由比ヶ浜 結衣[夢と魔法の夜]",31002,"夢と魔法の夜","やはり俺の青春ラブコメはまちがっている。続","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",100037,100078,"[O.N.G.E.K.I.]1.15-E-0007" -3,100794,"【SSR】一色 いろは[夢と魔法の夜]",31003,"夢と魔法の夜","やはり俺の青春ラブコメはまちがっている。続","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]1.15-E-0011" -3,100795,"【SR】珠洲島 有栖[今日は天丼の気分]",1012,"今日は天丼の気分","奏坂学園","Aqua","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",115096,115097,"[O.N.G.E.K.I.]1.15-0023" -3,100796,"【SR】ステーキ[赤身と脂身のバランスがとれた]",11,"赤身と脂身のバランスがとれた","奏坂学園","Fire","-",2,"55,227,242,257,272,287,0,0,0,287",115098,115099,"Special Menu" -3,100797,"【SR】エビフライ[サクッと衣にプリッとジューシー]",9,"サクッと衣にプリッとジューシー","奏坂学園","Fire","-",2,"55,227,242,257,272,287,0,0,0,287",115100,115101,"Special Menu" -3,100798,"【SR】ピザ[カリカリ生地に海の幸を乗せて]",10,"カリカリ生地に海の幸を乗せて","奏坂学園","Fire","-",2,"55,227,242,257,272,287,0,0,0,287",115102,115103,"Special Menu" -3,100799,"【SR】カレー[ごろっと牛肉の赤ワイン煮込み]",11,"ごろっと牛肉の赤ワイン煮込み","奏坂学園","Aqua","-",2,"55,227,242,257,272,287,0,0,0,287",115104,115105,"Special Menu" -3,100800,"【SR】牛丼[無添加国産牛の極うま]",10,"無添加国産牛の極うま","奏坂学園","Aqua","-",2,"55,227,242,257,272,287,0,0,0,287",115106,115107,"Special Menu" -3,100801,"【SR】うな重[創業40年老舗の]",9,"創業40年老舗の","奏坂学園","Aqua","-",2,"55,227,242,257,272,287,0,0,0,287",115108,115109,"Special Menu" -3,100802,"【SR】麻婆豆腐[シビれる辛さの]",11,"シビれる辛さの","奏坂学園","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",115110,115111,"Special Menu" -3,100803,"【SR】豆腐[納豆とオクラかけてチン]",10,"納豆とオクラかけてチン","奏坂学園","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",115112,115113,"Special Menu" -3,100804,"【SR】蕎麦[石臼挽き自家製二八]",9,"石臼挽き自家製二八","奏坂学園","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",115114,115115,"Special Menu" -3,100805,"【SR】井之原 小星[リトルスターの大冒険]",1010,"リトルスターの大冒険","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",115070,115071,"[O.N.G.E.K.I.]1.15-0029" -3,100806,"【SSR】初音ミク[Project DIVA 10th Anniversary]",5000,"Project DIVA 10th Anniversary","バーチャル・シンガー","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",100033,100074,"[O.N.G.E.K.I.]1.15-E-0109" -3,100807,"【R】雪ノ下 雪乃[総武高校奉仕部]",31001,"総武高校奉仕部","やはり俺の青春ラブコメはまちがっている。続","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0001" -3,100808,"【R】由比ヶ浜 結衣[総武高校奉仕部]",31002,"総武高校奉仕部","やはり俺の青春ラブコメはまちがっている。続","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0004" -3,100809,"【R】一色 いろは[総武高校生徒会長]",31003,"総武高校生徒会長","やはり俺の青春ラブコメはまちがっている。続","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.15-E-0008" -3,100810,"【SR】雪ノ下 雪乃[貴方もいかが]",31001,"貴方もいかが","やはり俺の青春ラブコメはまちがっている。続","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0002" -3,100811,"【SR】由比ヶ浜 結衣[食べる?]",31002,"食べる?","やはり俺の青春ラブコメはまちがっている。続","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",115024,115025,"[O.N.G.E.K.I.]1.15-E-0005" -3,100812,"【SR】一色 いろは[口説いてるんですか?]",31003,"口説いてるんですか?","やはり俺の青春ラブコメはまちがっている。続","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100016,100057,"[O.N.G.E.K.I.]1.15-E-0009" -3,100813,"【SR】雪ノ下 雪乃[はんなり花景色]",31001,"はんなり花景色","やはり俺の青春ラブコメはまちがっている。続","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110082,110083,"[O.N.G.E.K.I.]1.15-E-0003" -3,100814,"【SR】由比ヶ浜 結衣[ゆきのんと一緒]",31002,"ゆきのんと一緒","やはり俺の青春ラブコメはまちがっている。続","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.15-E-0006" -3,100815,"【SR】一色 いろは[せーんぱい!]",31003,"せーんぱい!","やはり俺の青春ラブコメはまちがっている。続","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",115000,115001,"[O.N.G.E.K.I.]1.15-E-0010" -3,100816,"【R】ロザリー[ゴシックドレス]",34001,"ゴシックドレス","ゴシックは魔法乙女","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0012" -3,100817,"【R】プルメリア[ゴシックドレス]",34001,"ゴシックドレス","ゴシックは魔法乙女","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.15-E-0015" -3,100818,"【R】ラナン[ゴシックドレス]",34001,"ゴシックドレス","ゴシックは魔法乙女","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100001,100042,"[O.N.G.E.K.I.]1.15-E-0017" -3,100819,"【R】カトレア[ゴシックドレス]",34001,"ゴシックドレス","ゴシックは魔法乙女","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0019" -3,100820,"【R】スフレ[ゴシックドレス]",34001,"ゴシックドレス","ゴシックは魔法乙女","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0021" -3,100821,"【R】ジギタリス[ゴシックパンクス]",34001,"ゴシックパンクス","ゴシックは魔法乙女","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0023" -3,100822,"【R】リリー[ゴシックパンクス]",34001,"ゴシックパンクス","ゴシックは魔法乙女","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.15-E-0025" -3,100823,"【R】ルチカ[ゴシックパンクス]",34001,"ゴシックパンクス","ゴシックは魔法乙女","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0027" -3,100824,"【R】カルミア[ゴシックパンクス]",34001,"ゴシックパンクス","ゴシックは魔法乙女","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",105004,105005,"[O.N.G.E.K.I.]1.15-E-0029" -3,100825,"【R】ダチュラ[ゴシックパンクス]",34001,"ゴシックパンクス","ゴシックは魔法乙女","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0031" -3,100826,"【SR】ロザリー[闇星夜]",34001,"闇星夜","ゴシックは魔法乙女","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0013" -3,100827,"【SR】プルメリア[光星夜]",34001,"光星夜","ゴシックは魔法乙女","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110138,110139,"[O.N.G.E.K.I.]1.15-E-0016" -3,100828,"【SR】ラナン[炎星夜]",34001,"炎星夜","ゴシックは魔法乙女","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100016,100057,"[O.N.G.E.K.I.]1.15-E-0018" -3,100829,"【SR】カトレア[水星夜]",34001,"水星夜","ゴシックは魔法乙女","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.15-E-0020" -3,100830,"【SR】スフレ[冬遊]",34001,"冬遊","ゴシックは魔法乙女","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0022" -3,100831,"【SR】ジギタリス[頬張]",34001,"頬張","ゴシックは魔法乙女","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.15-E-0024" -3,100832,"【SR】リリー[夢夜]",34001,"夢夜","ゴシックは魔法乙女","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100018,100059,"[O.N.G.E.K.I.]1.15-E-0026" -3,100833,"【SR】ルチカ[空星夜]",34001,"空星夜","ゴシックは魔法乙女","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.15-E-0028" -3,100834,"【SR】カルミア[輝瞳]",34001,"輝瞳","ゴシックは魔法乙女","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100017,100058,"[O.N.G.E.K.I.]1.15-E-0030" -3,100835,"【SR】ダチュラ[妖星夜]",34001,"妖星夜","ゴシックは魔法乙女","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0032" -3,100836,"【SR+】ロザリー[真夏のLOVEMAX!]",34001,"真夏のLOVEMAX!","ゴシックは魔法乙女","Aqua","-",12,"53,240,263,278,290,300,0,0,0,300",115004,115005,"[O.N.G.E.K.I.]1.15-E-0014" -3,100837,"【SR】サクラ[結実した努力]",27001,"結実した努力","プロジェクト東京ドールズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.15-E-0033" -3,100838,"【SR】ミサキ[力強い眼差し]",27001,"力強い眼差し","プロジェクト東京ドールズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",115006,115007,"[O.N.G.E.K.I.]1.15-E-0034" -3,100839,"【SR】シオリ[強くて優しいお姉さん]",27001,"強くて優しいお姉さん","プロジェクト東京ドールズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",115008,115009,"[O.N.G.E.K.I.]1.15-E-0035" -3,100840,"【SR】レイナ[ミス・ビューティー]",27001,"ミス・ビューティー","プロジェクト東京ドールズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",115010,115011,"[O.N.G.E.K.I.]1.15-E-0036" -3,100841,"【SR】ヒヨ[底抜けハイテンション娘]",27001,"底抜けハイテンション娘","プロジェクト東京ドールズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",115006,115007,"[O.N.G.E.K.I.]1.15-E-0037" -3,100842,"【SR】ナナミ[素直になれないお年頃]",27001,"素直になれないお年頃","プロジェクト東京ドールズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.15-E-0038" -3,100843,"【SR】アヤ[私についてきて!]",27001,"私についてきて!","プロジェクト東京ドールズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115012,115013,"[O.N.G.E.K.I.]1.15-E-0039" -3,100844,"【SR】ユキ[不思議系居眠り姫]",27001,"不思議系居眠り姫","プロジェクト東京ドールズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.15-E-0040" -3,100845,"【SR】ヤマダ[まあ悪くはないっす…]",27001,"まあ悪くはないっす…","プロジェクト東京ドールズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115006,115007,"[O.N.G.E.K.I.]1.15-E-0041" -3,100846,"【SR+】ユキ[あなたの笑顔こそ未来]",27001,"あなたの笑顔こそ未来","プロジェクト東京ドールズ","Leaf","-",12,"53,240,263,278,290,300,0,0,0,300",110072,110073,"[O.N.G.E.K.I.]1.15-E-0042" -3,100847,"【SSR】高瀬 梨緒[プロジェクト東京ドールズ]",1003,"プロジェクト東京ドールズ","奏坂学園","Aqua","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",115126,115127,"[O.N.G.E.K.I.]1.15-0014" -3,100848,"【R】星月 みき[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0043" -3,100849,"【R】若葉 昴[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0046" -3,100850,"【R】成海 遥香[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",105004,105005,"[O.N.G.E.K.I.]1.15-E-0048" -3,100851,"【R】天野 望[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0050" -3,100852,"【R】火向井 ゆり[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0052" -3,100853,"【R】常磐 くるみ[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",105004,105005,"[O.N.G.E.K.I.]1.15-E-0054" -3,100854,"【R】粒咲 あんこ[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.15-E-0056" -3,100855,"【R】芹沢 蓮華[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0058" -3,100856,"【R】楠 明日葉[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0060" -3,100857,"【SR】星月 みき[バースデー'19]",33001,"バースデー'19","バトルガール ハイスクール","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0044" -3,100858,"【SR】若葉 昴[バースデー'18]",33001,"バースデー'18","バトルガール ハイスクール","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0047" -3,100859,"【SR】成海 遥香[バースデー'18]",33001,"バースデー'18","バトルガール ハイスクール","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110136,110137,"[O.N.G.E.K.I.]1.15-E-0049" -3,100860,"【SR】天野 望[バースデー'18]",33001,"バースデー'18","バトルガール ハイスクール","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0051" -3,100861,"【SR】火向井 ゆり[バースデー'18]",33001,"バースデー'18","バトルガール ハイスクール","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0053" -3,100862,"【SR】常磐 くるみ[バースデー'18]",33001,"バースデー'18","バトルガール ハイスクール","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110140,110141,"[O.N.G.E.K.I.]1.15-E-0055" -3,100863,"【SR】粒咲 あんこ[バースデー'18]",33001,"バースデー'18","バトルガール ハイスクール","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110086,110087,"[O.N.G.E.K.I.]1.15-E-0057" -3,100864,"【SR】芹沢 蓮華[バースデー'18]",33001,"バースデー'18","バトルガール ハイスクール","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.15-E-0059" -3,100865,"【SR】楠 明日葉[バースデー'19]",33001,"バースデー'19","バトルガール ハイスクール","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.15-E-0061" -3,100866,"【R】藤宮 桜[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0062" -3,100867,"【R】南 ひなた[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0064" -3,100868,"【R】千導院 楓[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",105004,105005,"[O.N.G.E.K.I.]1.15-E-0066" -3,100869,"【R】綿木 ミシェル[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.15-E-0068" -3,100870,"【R】朝比奈 心美[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0070" -3,100871,"【R】蓮見 うらら[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0072" -3,100872,"【R】サドネ[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.15-E-0074" -3,100873,"【R】煌上 花音[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0076" -3,100874,"【R】国枝 詩穂[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0079" -3,100875,"【R】ミサキ[星衣フローラ]",33001,"星衣フローラ","バトルガール ハイスクール","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.15-E-0081" -3,100876,"【SR】藤宮 桜[バースデー'19]",33001,"バースデー'19","バトルガール ハイスクール","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0063" -3,100877,"【SR】南 ひなた[バースデー'19]",33001,"バースデー'19","バトルガール ハイスクール","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0065" -3,100878,"【SR】千導院 楓[バースデー'18]",33001,"バースデー'18","バトルガール ハイスクール","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110138,110139,"[O.N.G.E.K.I.]1.15-E-0067" -3,100879,"【SR】綿木 ミシェル[バースデー'18]",33001,"バースデー'18","バトルガール ハイスクール","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110088,110089,"[O.N.G.E.K.I.]1.15-E-0069" -3,100880,"【SR】朝比奈 心美[バースデー'18]",33001,"バースデー'18","バトルガール ハイスクール","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.15-E-0071" -3,100881,"【SR】蓮見 うらら[バースデー'19]",33001,"バースデー'19","バトルガール ハイスクール","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.15-E-0073" -3,100882,"【SR】サドネ[バースデー'18]",33001,"バースデー'18","バトルガール ハイスクール","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110082,110083,"[O.N.G.E.K.I.]1.15-E-0075" -3,100883,"【SR】煌上 花音[バースデー'18]",33001,"バースデー'18","バトルガール ハイスクール","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.15-E-0077" -3,100884,"【SR】国枝 詩穂[バースデー'18]",33001,"バースデー'18","バトルガール ハイスクール","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.15-E-0080" -3,100885,"【SR】ミサキ[バースデー'18]",33001,"バースデー'18","バトルガール ハイスクール","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110082,110083,"[O.N.G.E.K.I.]1.15-E-0082" -3,100886,"【SR+】煌上 花音[復活ライブ]",33001,"復活ライブ","バトルガール ハイスクール","Leaf","-",12,"53,240,263,278,290,300,0,0,0,300",115028,115029,"[O.N.G.E.K.I.]1.15-E-0078" -3,100887,"【SSR】星月 みき[5th Anniversary]",33001,"5th Anniversary","バトルガール ハイスクール","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.15-E-0045" -4,100897,"【SR】藤原 妹紅[full bloom]",2017,"full bloom","東方Project","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",105044,105045,"[O.N.G.E.K.I.]1.20-E-0188" -4,100898,"【SR】鈴仙・優曇華院・イナバ[full bloom]",2014,"full bloom","東方Project","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.20-E-0187" -4,100899,"【SR】古明地 さとり[full bloom]",2020,"full bloom","東方Project","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100016,100057,"[O.N.G.E.K.I.]1.20-E-0189" -4,100900,"【SR】古明地 こいし[full bloom]",2021,"full bloom","東方Project","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",120074,120075,"[O.N.G.E.K.I.]1.20-E-0190" -4,100901,"【SR】博麗 霊夢[full bloom]",2000,"full bloom","東方Project","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0184" -4,100902,"【SR】霧雨 魔理沙[full bloom]",2001,"full bloom","東方Project","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",120074,120075,"[O.N.G.E.K.I.]1.20-E-0185" -4,100903,"【SR+】霧雨 魔理沙[幻想郷の花吹雪]",2001,"幻想郷の花吹雪","東方Project","Leaf","-",12,"53,240,263,278,290,300,0,0,0,300",105010,105011,"[O.N.G.E.K.I.]1.20-E-0186" -3,100904,"【R】リムル=テンペスト[人型][転生者]",29001,"転生者","転生したらスライムだった件","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0092" -3,100905,"【R】ミリム・ナーヴァ[竜魔人(ドラゴノイド)]",29003,"竜魔人(ドラゴノイド)","転生したらスライムだった件","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0100" -3,100906,"【R】リムル=テンペスト[スライム][捕食者]",29002,"捕食者","転生したらスライムだった件","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100003,100044,"[O.N.G.E.K.I.]1.15-E-0096" -3,100907,"【SR】リムル=テンペスト[人型][暴風の紋章]",29001,"暴風の紋章","転生したらスライムだった件","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110086,110087,"[O.N.G.E.K.I.]1.15-E-0093" -3,100908,"【SR】ミリム・ナーヴァ[破壊の暴君(デストロイ)]",29003,"破壊の暴君(デストロイ)","転生したらスライムだった件","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0101" -3,100909,"【SR】リムル=テンペスト[人型][暴食者(グラトニー)]",29001,"暴食者(グラトニー)","転生したらスライムだった件","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110082,110083,"[O.N.G.E.K.I.]1.15-E-0094" -3,100910,"【SR】ミリム・ナーヴァ[竜星拡散爆(ドラゴ・バスター)]",29003,"竜星拡散爆(ドラゴ・バスター)","転生したらスライムだった件","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",115016,115017,"[O.N.G.E.K.I.]1.15-E-0102" -3,100911,"【SR】シズエ・イザワ[迸る炎]",29002,"迸る炎","転生したらスライムだった件","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0097" -3,100912,"【SR】大賢者[ユニークスキル]",29002,"ユニークスキル","転生したらスライムだった件","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",115018,115019,"[O.N.G.E.K.I.]1.15-E-0098" -3,100913,"【SR】ヴェルドラ=テンペスト[暴風竜]",29002,"暴風竜","転生したらスライムだった件","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115020,115021,"[O.N.G.E.K.I.]1.15-E-0099" -3,100914,"【SR+】リムル=テンペスト[人型][魔物を統べる者]",29001,"魔物を統べる者","転生したらスライムだった件","Aqua","-",12,"53,240,263,278,290,300,0,0,0,300",105144,105145,"[O.N.G.E.K.I.]1.15-E-0095" -3,100915,"【R】式宮 舞菜[KiRaRe]",32001,"KiRaRe","Re:ステージ!プリズムステップ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0110" -3,100916,"【R】月坂 紗由[KiRaRe]",32001,"KiRaRe","Re:ステージ!プリズムステップ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0113" -3,100917,"【R】市杵島 瑞葉[KiRaRe]",32001,"KiRaRe","Re:ステージ!プリズムステップ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.15-E-0115" -3,100918,"【R】柊 かえ[KiRaRe]",32001,"KiRaRe","Re:ステージ!プリズムステップ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100002,100043,"[O.N.G.E.K.I.]1.15-E-0117" -3,100919,"【R】本城 香澄[KiRaRe]",32001,"KiRaRe","Re:ステージ!プリズムステップ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0119" -3,100920,"【R】長谷川 みい[KiRaRe]",32001,"KiRaRe","Re:ステージ!プリズムステップ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0121" -3,100921,"【R】伊津村 紫[オルタンシア]",32001,"オルタンシア","Re:ステージ!プリズムステップ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0123" -3,100922,"【R】伊津村 陽花[オルタンシア]",32001,"オルタンシア","Re:ステージ!プリズムステップ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100003,100044,"[O.N.G.E.K.I.]1.15-E-0125" -3,100923,"【R】式宮 碧音[ステラマリス]",32001,"ステラマリス","Re:ステージ!プリズムステップ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0127" -3,100924,"【R】一条 瑠夏[ステラマリス]",32001,"ステラマリス","Re:ステージ!プリズムステップ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0129" -3,100925,"【R】岬 珊瑚[ステラマリス]",32001,"ステラマリス","Re:ステージ!プリズムステップ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.15-E-0131" -3,100926,"【SR】式宮 舞菜[ボクら×夢の数]",32001,"ボクら×夢の数","Re:ステージ!プリズムステップ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0111" -3,100927,"【SR】月坂 紗由[ボクら×夢の数]",32001,"ボクら×夢の数","Re:ステージ!プリズムステップ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0114" -3,100928,"【SR】市杵島 瑞葉[ボクら×夢の数]",32001,"ボクら×夢の数","Re:ステージ!プリズムステップ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110114,110115,"[O.N.G.E.K.I.]1.15-E-0116" -3,100929,"【SR】柊 かえ[ボクら×夢の数]",32001,"ボクら×夢の数","Re:ステージ!プリズムステップ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115024,115025,"[O.N.G.E.K.I.]1.15-E-0118" -3,100930,"【SR】本城 香澄[ボクら×夢の数]",32001,"ボクら×夢の数","Re:ステージ!プリズムステップ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.15-E-0120" -3,100931,"【SR】長谷川 みい[ボクら×夢の数]",32001,"ボクら×夢の数","Re:ステージ!プリズムステップ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.15-E-0122" -3,100932,"【SR】伊津村 紫[出会えたキセキ]",32001,"出会えたキセキ","Re:ステージ!プリズムステップ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0124" -3,100933,"【SR】伊津村 陽花[出会えたキセキ]",32001,"出会えたキセキ","Re:ステージ!プリズムステップ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0126" -3,100934,"【SR】式宮 碧音[Next:STAGE!!]",32001,"Next:STAGE!!","Re:ステージ!プリズムステップ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",105042,105043,"[O.N.G.E.K.I.]1.15-E-0128" -3,100935,"【SR】一条 瑠夏[Next:STAGE!!]",32001,"Next:STAGE!!","Re:ステージ!プリズムステップ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",105042,105043,"[O.N.G.E.K.I.]1.15-E-0130" -3,100936,"【SR】岬 珊瑚[Next:STAGE!!]",32001,"Next:STAGE!!","Re:ステージ!プリズムステップ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110088,110089,"[O.N.G.E.K.I.]1.15-E-0132" -3,100937,"【SR+】式宮 舞菜[KiRaRe!輝け、私たちの夢!!]",32001,"KiRaRe!輝け、私たちの夢!!","Re:ステージ!プリズムステップ","Leaf","-",12,"53,240,263,278,290,300,0,0,0,300",100031,100072,"[O.N.G.E.K.I.]1.15-E-0112" -3,100938,"【R】赤座 あかり[七森中 ごらく部]",30003,"七森中 ごらく部","ゆるゆり♪♪","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100007,100048,"[O.N.G.E.K.I.]1.15-E-0133" -3,100939,"【R】歳納 京子[七森中 ごらく部]",30001,"七森中 ごらく部","ゆるゆり♪♪","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0136" -3,100940,"【R】船見 結衣[七森中 ごらく部]",30001,"七森中 ごらく部","ゆるゆり♪♪","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0138" -3,100941,"【R】吉川 ちなつ[七森中 ごらく部]",30003,"七森中 ごらく部","ゆるゆり♪♪","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.15-E-0140" -3,100942,"【R】杉浦 綾乃[七森中 生徒会]",30002,"七森中 生徒会","ゆるゆり♪♪","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0142" -3,100943,"【R】池田 千歳[七森中 生徒会]",30002,"七森中 生徒会","ゆるゆり♪♪","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110084,110085,"[O.N.G.E.K.I.]1.15-E-0145" -3,100944,"【R】大室 櫻子[七森中 生徒会]",30002,"七森中 生徒会","ゆるゆり♪♪","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0147" -3,100945,"【R】古谷 向日葵[七森中 生徒会]",30002,"七森中 生徒会","ゆるゆり♪♪","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0149" -3,100946,"【SR】赤座 あかり[\アッカリ~ン/]",30003,"\アッカリ~ン/","ゆるゆり♪♪","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110114,110115,"[O.N.G.E.K.I.]1.15-E-0134" -3,100947,"【SR】歳納 京子[真の主人公]",30001,"真の主人公","ゆるゆり♪♪","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115026,115027,"[O.N.G.E.K.I.]1.15-E-0137" -3,100948,"【SR】船見 結衣[影の主人公]",30001,"影の主人公","ゆるゆり♪♪","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0139" -3,100949,"【SR】吉川 ちなつ[遅れてきた主人公]",30003,"遅れてきた主人公","ゆるゆり♪♪","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110138,110139,"[O.N.G.E.K.I.]1.15-E-0141" -3,100950,"【SR】杉浦 綾乃[罰金バッキンガムよ!]",30002,"罰金バッキンガムよ!","ゆるゆり♪♪","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115022,115023,"[O.N.G.E.K.I.]1.15-E-0143" -3,100951,"【SR】池田 千歳[今日も豊作やわ~♪♪]",30002,"今日も豊作やわ~♪♪","ゆるゆり♪♪","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100017,100058,"[O.N.G.E.K.I.]1.15-E-0146" -3,100952,"【SR】大室 櫻子[さみしくないモン!!]",30002,"さみしくないモン!!","ゆるゆり♪♪","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0148" -3,100953,"【SR】古谷 向日葵[嫌な予感しかしませんわ]",30002,"嫌な予感しかしませんわ","ゆるゆり♪♪","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",105008,105009,"[O.N.G.E.K.I.]1.15-E-0150" -3,100954,"【SSR】赤座 あかり[笑われればいいと思うよ。]",30003,"笑われればいいと思うよ。","ゆるゆり♪♪","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",115030,115031,"[O.N.G.E.K.I.]1.15-E-0135" -3,100955,"【SSR】杉浦 綾乃[Miracle Duet]",30002,"Miracle Duet","ゆるゆり♪♪","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]1.15-E-0144" -3,100956,"【R】倉科 明日香[オールラウンダー]",35001,"オールラウンダー","蒼の彼方のフォーリズム","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.15-E-0083" -3,100957,"【R】鳶沢 みさき[ファイター]",35001,"ファイター","蒼の彼方のフォーリズム","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0085" -3,100958,"【R】有坂 真白[スピーダー]",35001,"スピーダー","蒼の彼方のフォーリズム","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.15-E-0088" -3,100959,"【R】市ノ瀬 莉佳[スピーダー]",35001,"スピーダー","蒼の彼方のフォーリズム","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.15-E-0090" -3,100960,"【SR】倉科 明日香[エアキックターン]",35001,"エアキックターン","蒼の彼方のフォーリズム","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100018,100059,"[O.N.G.E.K.I.]1.15-E-0084" -3,100961,"【SR】鳶沢 みさき[ドッグファイト]",35001,"ドッグファイト","蒼の彼方のフォーリズム","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",115036,115037,"[O.N.G.E.K.I.]1.15-E-0086" -3,100962,"【SR】有坂 真白[ましろうどんの看板娘]",35001,"ましろうどんの看板娘","蒼の彼方のフォーリズム","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.15-E-0089" -3,100963,"【SR】市ノ瀬 莉佳[一年生同盟]",35001,"一年生同盟","蒼の彼方のフォーリズム","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",115038,115039,"[O.N.G.E.K.I.]1.15-E-0091" -3,100964,"【SR+】鳶沢 みさき[負けてからが、本当の勝負]",35001,"負けてからが、本当の勝負","蒼の彼方のフォーリズム","Aqua","-",12,"53,240,263,278,290,300,0,0,0,300",100031,100072,"[O.N.G.E.K.I.]1.15-E-0087" -5,100965,"【SSR】倉科 明日香[Beyond the sky]",35001,"Beyond the sky","蒼の彼方のフォーリズム","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",110070,110071,"[O.N.G.E.K.I.]1.25-E-0129" -5,100966,"【SSR】鳶沢 みさき[into the firmament]",35001,"into the firmament","蒼の彼方のフォーリズム","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",115040,115041,"[O.N.G.E.K.I.]1.25-E-0128" -3,100967,"【SR】春日部 ハル[777☆夏合宿]",24001,"777☆夏合宿","Tokyo 7th シスターズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110082,110083,"[O.N.G.E.K.I.]1.15-E-0158" -3,100968,"【SR】天堂寺 ムスビ[777☆夏合宿]",24001,"777☆夏合宿","Tokyo 7th シスターズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",115032,115033,"[O.N.G.E.K.I.]1.15-E-0160" -3,100969,"【SR】角森 ロナ[777☆夏合宿]",24001,"777☆夏合宿","Tokyo 7th シスターズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110082,110083,"[O.N.G.E.K.I.]1.15-E-0161" -3,100970,"【SR】野ノ原 ヒメ [777☆夏合宿]",24001,"777☆夏合宿","Tokyo 7th シスターズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110000,110001,"[O.N.G.E.K.I.]1.15-E-0162" -3,100971,"【SR】芹沢 モモカ[777☆夏合宿]",24001,"777☆夏合宿","Tokyo 7th シスターズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",115034,115035,"[O.N.G.E.K.I.]1.15-E-0163" -3,100972,"【SR】臼田 スミレ[777☆夏合宿]",24001,"777☆夏合宿","Tokyo 7th シスターズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100018,100059,"[O.N.G.E.K.I.]1.15-E-0164" -3,100973,"【SR】神城 スイ[777☆夏合宿]",24001,"777☆夏合宿","Tokyo 7th シスターズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0165" -3,100974,"【SR】久遠寺 シズカ [777☆夏合宿]",24001,"777☆夏合宿","Tokyo 7th シスターズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110116,110117,"[O.N.G.E.K.I.]1.15-E-0166" -3,100975,"【SR】アレサンドラ・スース[777☆夏合宿]",24001,"777☆夏合宿","Tokyo 7th シスターズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",115032,115033,"[O.N.G.E.K.I.]1.15-E-0167" -3,100976,"【SR】晴海 サワラ[777☆夏合宿]",24001,"777☆夏合宿","Tokyo 7th シスターズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110094,110095,"[O.N.G.E.K.I.]1.15-E-0168" -3,100977,"【SR】晴海 カジカ[777☆夏合宿]",24001,"777☆夏合宿","Tokyo 7th シスターズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.15-E-0169" -3,100978,"【SR】晴海 シンジュ [777☆夏合宿]",24001,"777☆夏合宿","Tokyo 7th シスターズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100017,100058,"[O.N.G.E.K.I.]1.15-E-0170" -3,100979,"【SR+】春日部 ハル[MELODY IN THE POCKET]",24001,"MELODY IN THE POCKET","Tokyo 7th シスターズ","Fire","-",12,"53,240,263,278,290,300,0,0,0,300",105010,105011,"[O.N.G.E.K.I.]1.15-E-0159" -3,100980,"【SSR】日向 千夏[2nd Anniversary]",1014,"2nd Anniversary","プロモーション","Leaf","中学2年生",3,"52,237,252,267,282,297,0,0,0,302",100033,100033,"[O.N.G.E.K.I.]Special Card" -4,100982,"【SSR】東雲 つむぎ[Summer☆Splash]",1015,"Summer☆Splash","奏坂学園","Aqua","中学2年生",3,"65,270,291,304,314,322,0,0,0,327",120000,120001,"[O.N.G.E.K.I.]1.20-0045" -4,100983,"【SSR】柏木 美亜[Summer☆Splash]",1013,"Summer☆Splash","奏坂学園","Fire","中学2年生",3,"65,270,291,304,314,322,0,0,0,327",120000,120001,"[O.N.G.E.K.I.]1.20-0044" -4,100984,"【SSR】結城 莉玖[目指せ!得点王]",1004,"目指せ!得点王","奏坂学園","Fire","高校1年生",3,"65,270,291,304,314,322,0,0,0,327",120078,120079,"[O.N.G.E.K.I.]1.20-0015" -4,100985,"【SSR】九條 楓[月夜のパンプキンマジック]",1008,"月夜のパンプキンマジック","奏坂学園","Leaf","高校3年生",3,"65,270,291,304,314,322,0,0,0,327",115120,115121,"[O.N.G.E.K.I.]1.20-0022" -4,100986,"【SSR】珠洲島 有栖[ぱくぱく宇宙飛行]",1012,"ぱくぱく宇宙飛行","奏坂学園","Aqua","高校1年生",3,"65,270,291,304,314,322,0,0,0,327",120006,120007,"[O.N.G.E.K.I.]1.20-0035" -4,100987,"【SSR】三角 葵[ビタースイート☆クリスマス]",1002,"ビタースイート☆クリスマス","奏坂学園","Aqua","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",120008,120009,"[O.N.G.E.K.I.]1.20-0027" -4,100988,"【SSR】井之原 小星[ビタースイート☆クリスマス]",1010,"ビタースイート☆クリスマス","奏坂学園","Leaf","高校1年生",3,"65,270,291,304,314,322,0,0,0,327",120010,120011,"[O.N.G.E.K.I.]1.20-0029" -4,100989,"【SSR】高瀬 梨緒[六花の妖精]",1003,"六花の妖精","奏坂学園","Aqua","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",120034,120035,"[O.N.G.E.K.I.]1.20-0040" -4,100990,"【SSR】星咲 あかり[無敵のツーマンセル]",1000,"無敵のツーマンセル","奏坂学園","Fire","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",120080,120081,"[O.N.G.E.K.I.]1.20-0011" -4,100991,"【SSR】高瀬 梨緒[無敵のツーマンセル]",1003,"無敵のツーマンセル","奏坂学園","Aqua","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",120082,120083,"[O.N.G.E.K.I.]1.20-0014" -4,100992,"【SSR】東雲 つむぎ[謎解きはおやつの前に]",1015,"謎解きはおやつの前に","奏坂学園","Aqua","中学2年生",3,"65,270,291,304,314,322,0,0,0,327",120006,120007,"[O.N.G.E.K.I.]1.20-0025" -4,100993,"【SSR】結城 莉玖[謎解きはおやつの前に]",1004,"謎解きはおやつの前に","奏坂学園","Fire","高校1年生",3,"65,270,291,304,314,322,0,0,0,327",120072,120073,"[O.N.G.E.K.I.]1.20-0020" -4,100994,"【SSR】桜井 春菜[おこたで鍋パ!]",1007,"おこたで鍋パ!","奏坂学園","Fire","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",120084,120085,"[O.N.G.E.K.I.]1.20-0033" -4,100995,"【SSR】珠洲島 有栖[おこたで鍋パ!]",1012,"おこたで鍋パ!","奏坂学園","Aqua","高校1年生",3,"65,270,291,304,314,322,0,0,0,327",120086,120087,"[O.N.G.E.K.I.]1.20-0036" -4,100996,"【SR】九條 楓[魅惑のプールサイド]",1008,"魅惑のプールサイド","奏坂学園","Leaf","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",120012,120013,"[O.N.G.E.K.I.]1.20-0043" -4,100997,"【SR】三角 葵[絡まりハプニング]",1002,"絡まりハプニング","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",120088,120089,"[O.N.G.E.K.I.]1.20-0012" -4,100998,"【SR】逢坂 茜[宣言通りの一等賞]",1011,"宣言通りの一等賞","奏坂学園","Fire","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",120090,120091,"[O.N.G.E.K.I.]1.20-0017" -4,100999,"【SR】日向 千夏[とりっく・おあ・ぎゅー!]",1014,"とりっく・おあ・ぎゅー!","奏坂学園","Leaf","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",100031,100072,"[O.N.G.E.K.I.]1.20-0024" -4,101000,"【SR】柏木 咲姫[なりきりヴァンパイア]",1009,"なりきりヴァンパイア","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",120018,120019,"[O.N.G.E.K.I.]1.20-0023" -4,101001,"【SR】桜井 春菜[検温のお時間です♪]",1007,"検温のお時間です♪","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",110066,110067,"[O.N.G.E.K.I.]1.20-0032" -4,101002,"【SR】藍原 椿[教えて!椿先生]",1005,"教えて!椿先生","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",115000,115001,"[O.N.G.E.K.I.]1.20-0031" -4,101003,"【SR】早乙女 彩華[サンタのお仕事]",1006,"サンタのお仕事","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",120020,120021,"[O.N.G.E.K.I.]1.20-0028" -4,101004,"【SR】柏木 美亜[雪と子猫とコーヒーと]",1013,"雪と子猫とコーヒーと","奏坂学園","Fire","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",120092,120093,"[O.N.G.E.K.I.]1.20-0041" -4,101005,"【SR】藤沢 柚子[一緒にポーズ!]",1001,"一緒にポーズ!","奏坂学園","Leaf","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",120092,120093,"[O.N.G.E.K.I.]1.20-0039" -4,101006,"【SR+】結城 莉玖[夢に向かって]",1004,"夢に向かって","奏坂学園","Fire","高校1年生",12,"53,240,263,278,290,300,0,0,0,300",120036,120037,"[O.N.G.E.K.I.]1.20-0018" -4,101007,"【SR+】三角 葵[穏やかな時間]",1002,"穏やかな時間","奏坂学園","Aqua","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",120038,120039,"[O.N.G.E.K.I.]1.20-0046" -5,101008,"【SR+】藤沢 柚子[お悩みシンキング]",1001,"お悩みシンキング","奏坂学園","Leaf","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",120040,120041,"" -4,101009,"【SR+】桜井 春菜[元気にしてた?]",1007,"元気にしてた?","奏坂学園","Fire","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",120042,120043,"[O.N.G.E.K.I.]1.20-0042" -4,101010,"【SR+】柏木 咲姫[お出かけホリデー]",1009,"お出かけホリデー","奏坂学園","Aqua","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",120044,120045,"[O.N.G.E.K.I.]1.20-0034" -4,101011,"【SR+】逢坂 茜[世界征服☆大作戦]",1011,"世界征服☆大作戦","奏坂学園","Fire","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",110066,110067,"[O.N.G.E.K.I.]1.20-0009" -4,101012,"【SR+】珠洲島 有栖[もふもふシエスタ]",1012,"もふもふシエスタ","奏坂学園","Aqua","高校1年生",12,"53,240,263,278,290,300,0,0,0,300",110066,110067,"[O.N.G.E.K.I.]1.20-0026" -5,101013,"【SR+】九條 楓[ぽんぽんクリーニング]",1008,"ぽんぽんクリーニング","奏坂学園","Leaf","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",110066,110067,"[O.N.G.E.K.I.]1.25-0036" -4,101020,"【R】星咲 あかり[体操着2]",1000,"体操着2","奏坂学園","Fire","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",120094,120095,"[O.N.G.E.K.I.]1.20-0010" -4,101021,"【R】高瀬 梨緒[体操着2]",1003,"体操着2","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",120094,120095,"[O.N.G.E.K.I.]1.20-0013" -4,101022,"【R】逢坂 茜[サバゲー]",1011,"サバゲー","奏坂学園","Fire","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",120094,120095,"[O.N.G.E.K.I.]1.20-0016" -4,101023,"【R】藍原 椿[体操着2]",1005,"体操着2","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",120048,120049,"[O.N.G.E.K.I.]1.20-0021" -4,101024,"【R】結城 莉玖[体操着2]",1004,"体操着2","奏坂学園","Fire","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",120050,120051,"[O.N.G.E.K.I.]1.20-0019" -4,101025,"【R】三角 葵[体操着2]",1002,"体操着2","奏坂学園","Aqua","高校2年生",1,"50,197,212,227,242,257,0,0,0,257",120052,120053,"[O.N.G.E.K.I.]1.20-0030" -4,101026,"【R】柏木 美亜[体操着2]",1013,"体操着2","奏坂学園","Fire","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",120054,120055,"[O.N.G.E.K.I.]1.20-0037" -4,101027,"【R】日向 千夏[体操着2]",1014,"体操着2","奏坂学園","Leaf","中学2年生",1,"50,197,212,227,242,257,0,0,0,257",120056,120057,"[O.N.G.E.K.I.]1.20-0038" -5,101038,"【R】洩矢 諏訪子[東方Project]",2023,"東方Project","東方Project 早苗&諏訪子編","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0201" -5,101039,"【R】東風谷 早苗[東方Project]",2022,"東方Project","東方Project 早苗&諏訪子編","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100002,100043,"[O.N.G.E.K.I.]1.25-E-0198" -5,101041,"【SR】洩矢 諏訪子[土着神の頂点]",2023,"土着神の頂点","東方Project 早苗&諏訪子編","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0200" -5,101042,"【SR】東風谷 早苗[祀られる風の人間]",2022,"祀られる風の人間","東方Project 早苗&諏訪子編","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110114,110115,"[O.N.G.E.K.I.]1.25-E-0197" -5,101044,"【SSR】洩矢 諏訪子[神具「洩矢の鉄の輪」]",2023,"神具「洩矢の鉄の輪」","東方Project 早苗&諏訪子編","Leaf","-",3,"60,265,286,299,309,317,0,0,0,322",110070,110071,"[O.N.G.E.K.I.]1.25-E-0199" -5,101045,"【SSR】東風谷 早苗[秘術「グレイソーマタージ」]",2022,"秘術「グレイソーマタージ」","東方Project 早苗&諏訪子編","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]1.25-E-0196" -4,101047,"【SSR】早乙女 彩華[デイドリーム・フェアリーズ]",1006,"デイドリーム・フェアリーズ","奏坂学園","Aqua","高校3年生",3,"65,270,291,304,314,322,0,0,0,327",100040,100081,"[O.N.G.E.K.I.]1.20-0005" -4,101048,"【SSR】桜井 春菜[デイドリーム・フェアリーズ]",1007,"デイドリーム・フェアリーズ","奏坂学園","Fire","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",100038,100079,"[O.N.G.E.K.I.]1.20-0007" -4,101049,"【N】皇城 セツナ",1016,"","奏坂学園","Leaf","高校3年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"" -4,101050,"【R】皇城 セツナ[シュータードレス]",1016,"シュータードレス","奏坂学園","Leaf","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" -3,101051,"【R】歳納 京子[対戦相手専用]",30001,"対戦相手専用","ゆるゆり♪♪","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100000,"" -3,101052,"【R】杉浦 綾乃[対戦相手専用]",30002,"対戦相手専用","ゆるゆり♪♪","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100000,"" -3,101053,"【R】リムル=テンペスト[人型][対戦相手専用]",29001,"対戦相手専用","転生したらスライムだった件","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100000,"" -3,101054,"【R】ミリム・ナーヴァ[対戦相手専用]",29003,"対戦相手専用","転生したらスライムだった件","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100000,"" -4,101055,"【R】涼風 青葉[イーグルジャンプ]",39001,"イーグルジャンプ","NEW GAME!!","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0165" -4,101056,"【R】八神 コウ[イーグルジャンプ]",39002,"イーグルジャンプ","NEW GAME!!","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0168" -4,101057,"【R】遠山 りん[イーグルジャンプ]",39006,"イーグルジャンプ","NEW GAME!!","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0176" -4,101058,"【R】滝本 ひふみ[イーグルジャンプ]",39003,"イーグルジャンプ","NEW GAME!!","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.20-E-0170" -4,101059,"【R】篠田 はじめ[イーグルジャンプ]",39004,"イーグルジャンプ","NEW GAME!!","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0172" -4,101060,"【R】飯島 ゆん[イーグルジャンプ]",39005,"イーグルジャンプ","NEW GAME!!","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110084,110085,"[O.N.G.E.K.I.]1.20-E-0174" -4,101061,"【R】桜 ねね[イーグルジャンプ]",39009,"イーグルジャンプ","NEW GAME!!","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0182" -4,101062,"【R】望月 紅葉[イーグルジャンプ]",39007,"イーグルジャンプ","NEW GAME!!","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0178" -4,101063,"【R】鳴海 ツバメ[イーグルジャンプ]",39008,"イーグルジャンプ","NEW GAME!!","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",115146,115147,"[O.N.G.E.K.I.]1.20-E-0180" -4,101064,"【SR】涼風 青葉[祭と言えばりんご飴です!]",39001,"祭と言えばりんご飴です!","NEW GAME!!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0166" -4,101065,"【SR】八神 コウ[こうすると簡単だよ]",39002,"こうすると簡単だよ","NEW GAME!!","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0169" -4,101066,"【SR】遠山 りん[相談はまず私にしてね]",39006,"相談はまず私にしてね","NEW GAME!!","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110088,110089,"[O.N.G.E.K.I.]1.20-E-0177" -4,101067,"【SR】滝本 ひふみ[……似合う?]",39003,"……似合う?","NEW GAME!!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110138,110139,"[O.N.G.E.K.I.]1.20-E-0171" -4,101068,"【SR】篠田 はじめ[メガ粒子レクエイムシュート!]",39004,"メガ粒子レクエイムシュート!","NEW GAME!!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0173" -4,101069,"【SR】飯島 ゆん[ええ後輩持てて幸せや]",39005,"ええ後輩持てて幸せや","NEW GAME!!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110114,110115,"[O.N.G.E.K.I.]1.20-E-0175" -4,101070,"【SR】桜 ねね[あおっち大人っぽい!]",39009,"あおっち大人っぽい!","NEW GAME!!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0183" -4,101071,"【SR】望月 紅葉[私も負けていられません]",39007,"私も負けていられません","NEW GAME!!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0179" -4,101072,"【SR】鳴海 ツバメ[今日の夕飯はお餅です!]",39008,"今日の夕飯はお餅です!","NEW GAME!!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110086,110087,"[O.N.G.E.K.I.]1.20-E-0181" -4,101073,"【SR】涼風 青葉[やっぱり海は最高です!]",39001,"やっぱり海は最高です!","NEW GAME!!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115022,115023,"[O.N.G.E.K.I.]1.20-E-0167" -4,101074,"【R】光[Arcaea]",36001,"Arcaea","Arcaea","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0077" -4,101075,"【R】対立[Arcaea]",36001,"Arcaea","Arcaea","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0081" -4,101076,"【R】紅[Arcaea]",36001,"Arcaea","Arcaea","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0085" -4,101077,"【R】レーテー[Arcaea]",36001,"Arcaea","Arcaea","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0086" -4,101078,"【R】エト[Arcaea]",36001,"Arcaea","Arcaea","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",115146,115147,"[O.N.G.E.K.I.]1.20-E-0087" -4,101079,"【R】ルナ[Arcaea]",36001,"Arcaea","Arcaea","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110014,110015,"[O.N.G.E.K.I.]1.20-E-0088" -4,101080,"【R】サヤ[Arcaea]",36001,"Arcaea","Arcaea","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110084,110085,"[O.N.G.E.K.I.]1.20-E-0089" -4,101081,"【R】叶永[Arcaea]",36001,"Arcaea","Arcaea","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0090" -4,101082,"【SR】光[Zero]",36001,"Zero","Arcaea","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115064,115065,"[O.N.G.E.K.I.]1.20-E-0078" -4,101083,"【SR】対立[Axium]",36001,"Axium","Arcaea","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0082" -4,101084,"【SR】光[Fracture]",36001,"Fracture","Arcaea","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.20-E-0079" -4,101085,"【SR】対立[Grievous Lady]",36001,"Grievous Lady","Arcaea","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",115036,115037,"[O.N.G.E.K.I.]1.20-E-0083" -4,101086,"【SSR】光&対立[Summer]",36001,"Summer","Arcaea","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",110070,110071,"[O.N.G.E.K.I.]1.20-E-0080" -4,101087,"【SSR】対立[Tempest]",36001,"Tempest","Arcaea","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.20-E-0084" -4,101088,"【R】パトリシア・オブ・エンド[冥界三姉妹]",41001,"冥界三姉妹","ノラと皇女と野良猫ハート","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0139" -4,101089,"【R】黒木 未知[桜ヶ淵学園2年生]",41002,"桜ヶ淵学園2年生","ノラと皇女と野良猫ハート","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100003,100044,"[O.N.G.E.K.I.]1.20-E-0143" -4,101090,"【R】夕莉 シャチ[桜ヶ淵学園2年生]",41003,"桜ヶ淵学園2年生","ノラと皇女と野良猫ハート","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.20-E-0146" -4,101091,"【R】明日原 ユウキ[桜ヶ淵学園1年生]",41004,"桜ヶ淵学園1年生","ノラと皇女と野良猫ハート","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0149" -4,101092,"【R】高田 ノブチナ[桜ヶ淵学園2年生]",41005,"桜ヶ淵学園2年生","ノラと皇女と野良猫ハート","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0152" -4,101093,"【R】ユウラシア・オブ・エンド[冥界三姉妹]",41006,"冥界三姉妹","ノラと皇女と野良猫ハート","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0155" -4,101094,"【R】ルーシア・オブ・エンド[冥界三姉妹]",41007,"冥界三姉妹","ノラと皇女と野良猫ハート","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.20-E-0158" -4,101095,"【R】アイリス・ディセンバー・アンクライ[忘国の皇女]",41008,"忘国の皇女","ノラと皇女と野良猫ハート","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0162" -4,101096,"【SR】パトリシア・オブ・エンド[海が好き!でもわたしは、音ゲーも好きなんだい!]",41001,"海が好き!でもわたしは、音ゲーも好きなんだい!","ノラと皇女と野良猫ハート","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0140" -4,101097,"【SR】黒木 未知[海が好き!でもわたしは、音ゲーも好きなんだい!]",41002,"海が好き!でもわたしは、音ゲーも好きなんだい!","ノラと皇女と野良猫ハート","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100018,100059,"[O.N.G.E.K.I.]1.20-E-0144" -4,101098,"【SR】夕莉 シャチ[海が好き!でもわたしは、音ゲーも好きなんだい!]",41003,"海が好き!でもわたしは、音ゲーも好きなんだい!","ノラと皇女と野良猫ハート","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110086,110087,"[O.N.G.E.K.I.]1.20-E-0147" -4,101099,"【SR】明日原 ユウキ[海が好き!でもわたしは、音ゲーも好きなんだい!]",41004,"海が好き!でもわたしは、音ゲーも好きなんだい!","ノラと皇女と野良猫ハート","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0150" -4,101100,"【SR】高田 ノブチナ[海が好き!でもわたしは、音ゲーも好きなんだい!]",41005,"海が好き!でもわたしは、音ゲーも好きなんだい!","ノラと皇女と野良猫ハート","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0153" -4,101101,"【SR】ユウラシア・オブ・エンド[海が好き!でもわたしは、音ゲーも好きなんだい!]",41006,"海が好き!でもわたしは、音ゲーも好きなんだい!","ノラと皇女と野良猫ハート","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110140,110141,"[O.N.G.E.K.I.]1.20-E-0156" -4,101102,"【SR】ルーシア・オブ・エンド[海が好き!でもわたしは、音ゲーも好きなんだい!]",41007,"海が好き!でもわたしは、音ゲーも好きなんだい!","ノラと皇女と野良猫ハート","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0159" -4,101103,"【SR】アイリス・ディセンバー・アンクライ[海が好き!でもわたしは、音ゲーも好きなんだい!]",41008,"海が好き!でもわたしは、音ゲーも好きなんだい!","ノラと皇女と野良猫ハート","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0163" -4,101104,"【SR】パトリシア・オブ・エンド[知りたがりの皇女様]",41001,"知りたがりの皇女様","ノラと皇女と野良猫ハート","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",120068,120069,"[O.N.G.E.K.I.]1.20-E-0141" -4,101105,"【SR】黒木 未知[恋に恋する風紀委員]",41002,"恋に恋する風紀委員","ノラと皇女と野良猫ハート","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.20-E-0145" -4,101106,"【SR】夕莉 シャチ[恋する環境装置]",41003,"恋する環境装置","ノラと皇女と野良猫ハート","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",120070,120071,"[O.N.G.E.K.I.]1.20-E-0148" -4,101107,"【SR】明日原 ユウキ[天真爛漫バイトギャル]",41004,"天真爛漫バイトギャル","ノラと皇女と野良猫ハート","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",120074,120075,"[O.N.G.E.K.I.]1.20-E-0151" -4,101108,"【SR】高田 ノブチナ[任侠はぐれオオカミ]",41005,"任侠はぐれオオカミ","ノラと皇女と野良猫ハート","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",120018,120019,"[O.N.G.E.K.I.]1.20-E-0154" -4,101109,"【SR】ユウラシア・オブ・エンド[みんなのアイドル]",41006,"みんなのアイドル","ノラと皇女と野良猫ハート","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",105052,105053,"[O.N.G.E.K.I.]1.20-E-0157" -4,101110,"【SR】ルーシア・オブ・エンド[武闘派お姉さま]",41007,"武闘派お姉さま","ノラと皇女と野良猫ハート","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110094,110095,"[O.N.G.E.K.I.]1.20-E-0160" -4,101111,"【SR】アイリス・ディセンバー・アンクライ[忘却を追い越す一歩]",41008,"忘却を追い越す一歩","ノラと皇女と野良猫ハート","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",120074,120075,"[O.N.G.E.K.I.]1.20-E-0164" -4,101112,"【SSR】パトリシア・オブ・エンド[臆病で勇敢なブレイブハート]",41001,"臆病で勇敢なブレイブハート","ノラと皇女と野良猫ハート","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",115030,115031,"[O.N.G.E.K.I.]1.20-E-0142" -4,101113,"【R】白鳥 天葉[トロワアンジュ]",32001,"トロワアンジュ","Re:ステージ!プリズムステップ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0116" -4,101114,"【R】帆風 奏[トロワアンジュ]",32001,"トロワアンジュ","Re:ステージ!プリズムステップ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0118" -4,101115,"【R】緋村 那岐咲[トロワアンジュ]",32001,"トロワアンジュ","Re:ステージ!プリズムステップ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.20-E-0120" -4,101116,"【SR】白鳥 天葉[On:STAGE!!]",32001,"On:STAGE!!","Re:ステージ!プリズムステップ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0117" -4,101117,"【SR】帆風 奏[On:STAGE!!]",32001,"On:STAGE!!","Re:ステージ!プリズムステップ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0119" -4,101118,"【SR】緋村 那岐咲[On:STAGE!!]",32001,"On:STAGE!!","Re:ステージ!プリズムステップ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110086,110087,"[O.N.G.E.K.I.]1.20-E-0121" -4,101119,"【R】坂東 美久龍[テトラルキア]",32001,"テトラルキア","Re:ステージ!プリズムステップ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0122" -4,101120,"【R】西館 ハク[テトラルキア]",32001,"テトラルキア","Re:ステージ!プリズムステップ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0124" -4,101121,"【R】南風野 朱莉[テトラルキア]",32001,"テトラルキア","Re:ステージ!プリズムステップ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0126" -4,101122,"【R】城北 玄刃[テトラルキア]",32001,"テトラルキア","Re:ステージ!プリズムステップ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0128" -4,101123,"【SR】坂東 美久龍[Next:STAGE!!]",32001,"Next:STAGE!!","Re:ステージ!プリズムステップ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0123" -4,101124,"【SR】西館 ハク[On:STAGE!!]",32001,"On:STAGE!!","Re:ステージ!プリズムステップ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0125" -4,101125,"【SR】南風野 朱莉[Make My Day]",32001,"Make My Day","Re:ステージ!プリズムステップ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0127" -4,101126,"【SR】城北 玄刃[On:STAGE!!]",32001,"On:STAGE!!","Re:ステージ!プリズムステップ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0129" -4,101127,"【SR】式宮 舞菜[PJs:PARTY!!]",32001,"PJs:PARTY!!","Re:ステージ!プリズムステップ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.20-E-0130" -4,101128,"【SR】月坂 紗由[PJs:PARTY!!]",32001,"PJs:PARTY!!","Re:ステージ!プリズムステップ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110094,110095,"[O.N.G.E.K.I.]1.20-E-0132" -4,101129,"【SR】市杵島 瑞葉[PJs:PARTY!!]",32001,"PJs:PARTY!!","Re:ステージ!プリズムステップ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110072,110073,"[O.N.G.E.K.I.]1.20-E-0133" -4,101130,"【SR】柊 かえ[PJs:PARTY!!]",32001,"PJs:PARTY!!","Re:ステージ!プリズムステップ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115052,115053,"[O.N.G.E.K.I.]1.20-E-0134" -4,101131,"【SR】本城 香澄[PJs:PARTY!!]",32001,"PJs:PARTY!!","Re:ステージ!プリズムステップ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.20-E-0135" -4,101132,"【SR】長谷川 みい[PJs:PARTY!!]",32001,"PJs:PARTY!!","Re:ステージ!プリズムステップ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110094,110095,"[O.N.G.E.K.I.]1.20-E-0136" -4,101133,"【SR+】式宮 舞菜[ハッピータイフーン]",32001,"ハッピータイフーン","Re:ステージ!プリズムステップ","Leaf","-",12,"53,240,263,278,290,300,0,0,0,300",115022,115023,"[O.N.G.E.K.I.]1.20-E-0131" -4,101146,"【R】六石 陽菜[Flower]",42001,"Flower","CUE!","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.20-E-0001" -4,101147,"【R】天童 悠希[Bird]",42002,"Bird","CUE!","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0005" -4,101148,"【R】夜峰 美晴[Wind]",42003,"Wind","CUE!","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0008" -4,101149,"【R】丸山 利恵[Moon]",42004,"Moon","CUE!","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0011" -4,101150,"【SR】六石 陽菜[Forever Friends]",42001,"Forever Friends","CUE!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100018,100059,"[O.N.G.E.K.I.]1.20-E-0002" -4,101151,"【SR】天童 悠希[Forever Friends]",42002,"Forever Friends","CUE!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0006" -4,101152,"【SR】夜峰 美晴[Forever Friends]",42003,"Forever Friends","CUE!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0009" -4,101153,"【SR】丸山 利恵[Forever Friends]",42004,"Forever Friends","CUE!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0012" -4,101154,"【SR】六石 陽菜[桜の花舞う教室で]",42001,"桜の花舞う教室で","CUE!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110086,110087,"[O.N.G.E.K.I.]1.20-E-0003" -4,101155,"【SR】天童 悠希[三兵衛、秋の感謝フェア]",42002,"三兵衛、秋の感謝フェア","CUE!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110094,110095,"[O.N.G.E.K.I.]1.20-E-0007" -4,101156,"【SR】夜峰 美晴[ゆっくり消える虹を見て]",42003,"ゆっくり消える虹を見て","CUE!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",120074,120075,"[O.N.G.E.K.I.]1.20-E-0010" -4,101157,"【SR】丸山 利恵[カストールの安息日]",42004,"カストールの安息日","CUE!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.20-E-0013" -4,101158,"【SSR】六石 陽菜[See You Everyday]",42001,"See You Everyday","CUE!","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",110070,110071,"[O.N.G.E.K.I.]1.20-E-0004" -4,101159,"【R】中野 一花[中野家の五つ子]",38001,"中野家の五つ子","五等分の花嫁","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0056" -4,101160,"【R】中野 二乃[中野家の五つ子]",38002,"中野家の五つ子","五等分の花嫁","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0059" -4,101161,"【R】中野 三玖[中野家の五つ子]",38003,"中野家の五つ子","五等分の花嫁","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100002,100043,"[O.N.G.E.K.I.]1.20-E-0062" -4,101162,"【R】中野 四葉[中野家の五つ子]",38004,"中野家の五つ子","五等分の花嫁","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110142,110143,"[O.N.G.E.K.I.]1.20-E-0065" -4,101163,"【R】中野 五月[中野家の五つ子]",38005,"中野家の五つ子","五等分の花嫁","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.20-E-0068" -4,101164,"【SR】中野 一花[頼れる長女]",38001,"頼れる長女","五等分の花嫁","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0057" -4,101165,"【SR】中野 二乃[まっすぐ次女]",38002,"まっすぐ次女","五等分の花嫁","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0060" -4,101166,"【SR】中野 三玖[ミステリアス三女]",38003,"ミステリアス三女","五等分の花嫁","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100017,100058,"[O.N.G.E.K.I.]1.20-E-0063" -4,101167,"【SR】中野 四葉[笑顔の四女]",38004,"笑顔の四女","五等分の花嫁","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0066" -4,101168,"【SR】中野 五月[しっかり五女]",38005,"しっかり五女","五等分の花嫁","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0069" -4,101169,"【SSR】中野 一花[五等分の花嫁]",38001,"五等分の花嫁","五等分の花嫁","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",105000,105001,"[O.N.G.E.K.I.]1.20-E-0058" -4,101170,"【SSR】中野 二乃[五等分の花嫁]",38002,"五等分の花嫁","五等分の花嫁","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",105000,105001,"[O.N.G.E.K.I.]1.20-E-0061" -4,101171,"【SSR】中野 三玖[五等分の花嫁]",38003,"五等分の花嫁","五等分の花嫁","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",105000,105001,"[O.N.G.E.K.I.]1.20-E-0064" -4,101172,"【SSR】中野 四葉[五等分の花嫁]",38004,"五等分の花嫁","五等分の花嫁","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",105000,105001,"[O.N.G.E.K.I.]1.20-E-0067" -4,101173,"【SSR】中野 五月[五等分の花嫁]",38005,"五等分の花嫁","五等分の花嫁","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",105000,105001,"[O.N.G.E.K.I.]1.20-E-0070" -4,101176,"【R】星乃 一歌[プロジェクトセカイ]",43001,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0097" -4,101177,"【R】天馬 咲希[プロジェクトセカイ]",43002,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100003,100044,"[O.N.G.E.K.I.]1.20-E-0099" -4,101178,"【R】望月 穂波[プロジェクトセカイ]",43003,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.20-E-0101" -4,101179,"【R】日野森 志歩[プロジェクトセカイ]",43004,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0103" -4,101180,"【R】桐谷 遥[プロジェクトセカイ]",43005,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0105" -4,101181,"【R】花里 みのり[プロジェクトセカイ]",43006,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0107" -4,101182,"【R】日野森 雫[プロジェクトセカイ]",43007,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.20-E-0109" -4,101183,"【R】桃井 愛莉[プロジェクトセカイ]",43008,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100002,100043,"[O.N.G.E.K.I.]1.20-E-0111" -4,101184,"【SR】星乃 一歌[Leo/need]",43001,"Leo/need","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0098" -4,101185,"【SR】天馬 咲希[Leo/need]",43002,"Leo/need","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0100" -4,101186,"【SR】望月 穂波[Leo/need]",43003,"Leo/need","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",120074,120075,"[O.N.G.E.K.I.]1.20-E-0102" -4,101187,"【SR】日野森 志歩[Leo/need]",43004,"Leo/need","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0104" -4,101188,"【SR】桐谷 遥[MORE MORE JUMP!]",43005,"MORE MORE JUMP!","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0106" -4,101189,"【SR】花里 みのり[MORE MORE JUMP!]",43006,"MORE MORE JUMP!","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",120074,120075,"[O.N.G.E.K.I.]1.20-E-0108" -4,101190,"【SR】日野森 雫[MORE MORE JUMP!]",43007,"MORE MORE JUMP!","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0110" -4,101191,"【SR】桃井 愛莉[MORE MORE JUMP!]",43008,"MORE MORE JUMP!","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0112" -4,101192,"【SR】初音ミク[Leo/need]",5000,"Leo/need","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110086,110087,"[O.N.G.E.K.I.]1.20-E-0113" -4,101193,"【SR】初音ミク[MORE MORE JUMP!]",5000,"MORE MORE JUMP!","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110114,110115,"[O.N.G.E.K.I.]1.20-E-0114" -4,101197,"【R】サンドリヨン[真夜中の戦姫]",44001,"真夜中の戦姫","Wonderland Wars","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0091" -4,101198,"【R】シュネーヴィッツェン[不滅の白雪]",44002,"不滅の白雪","Wonderland Wars","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0093" -4,101199,"【R】リトル・アリス[不思議の国の少女]",44003,"不思議の国の少女","Wonderland Wars","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0095" -4,101200,"【SR】サンドリヨン[「タイムオブプリンセス」]",44001,"「タイムオブプリンセス」","Wonderland Wars","Aqua","-",2,"55,227,242,257,272,287,0,0,0,287",100033,100074,"[O.N.G.E.K.I.]1.20-E-0092" -4,101201,"【SR】シュネーヴィッツェン[「ホワイトライトブレス」]",44002,"「ホワイトライトブレス」","Wonderland Wars","Fire","-",2,"55,227,242,257,272,287,0,0,0,287",115026,115027,"[O.N.G.E.K.I.]1.20-E-0094" -4,101202,"【SR】リトル・アリス[「おおきくなるよ!」]",44003,"「おおきくなるよ!」","Wonderland Wars","Aqua","-",2,"55,227,242,257,272,287,0,0,0,287",115026,115027,"[O.N.G.E.K.I.]1.20-E-0096" -4,101203,"【R】鹿目 まどか[魔法少女]",37001,"魔法少女","魔法少女まどか☆マギカ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0014" -4,101204,"【R】暁美 ほむら[魔法少女]",37002,"魔法少女","魔法少女まどか☆マギカ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0019" -4,101205,"【R】美樹 さやか[魔法少女]",37003,"魔法少女","魔法少女まどか☆マギカ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0027" -4,101206,"【R】巴 マミ[魔法少女]",37004,"魔法少女","魔法少女まどか☆マギカ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.20-E-0024" -4,101207,"【R】佐倉 杏子[魔法少女]",37005,"魔法少女","魔法少女まどか☆マギカ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110014,110015,"[O.N.G.E.K.I.]1.20-E-0030" -4,101208,"【SR】鹿目 まどか[見滝原の夏休み]",37001,"見滝原の夏休み","魔法少女まどか☆マギカ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0015" -4,101209,"【SR】暁美 ほむら[見滝原の夏休み]",37002,"見滝原の夏休み","魔法少女まどか☆マギカ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100016,100057,"[O.N.G.E.K.I.]1.20-E-0020" -4,101210,"【SR】美樹 さやか[見滝原の夏休み]",37003,"見滝原の夏休み","魔法少女まどか☆マギカ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100018,100059,"[O.N.G.E.K.I.]1.20-E-0028" -4,101211,"【SR】巴 マミ[見滝原の夏休み]",37004,"見滝原の夏休み","魔法少女まどか☆マギカ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0025" -4,101212,"【SR】佐倉 杏子[見滝原の夏休み]",37005,"見滝原の夏休み","魔法少女まどか☆マギカ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0031" -4,101213,"【SR】鹿目 まどか[弓道場にて]",37001,"弓道場にて","魔法少女まどか☆マギカ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.20-E-0016" -4,101214,"【SR】暁美 ほむら[夕焼けの丘で]",37002,"夕焼けの丘で","魔法少女まどか☆マギカ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",120062,120063,"[O.N.G.E.K.I.]1.20-E-0021" -4,101215,"【SR】美樹 さやか[パジャマパーティー]",37003,"パジャマパーティー","魔法少女まどか☆マギカ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.20-E-0029" -4,101216,"【SR】巴 マミ[氷上の先輩魔法少女]",37004,"氷上の先輩魔法少女","魔法少女まどか☆マギカ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115022,115023,"[O.N.G.E.K.I.]1.20-E-0026" -4,101217,"【SR】佐倉 杏子[分け合いっこ]",37005,"分け合いっこ","魔法少女まどか☆マギカ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",105052,105053,"[O.N.G.E.K.I.]1.20-E-0032" -4,101218,"【SR】鹿目 まどか[マジカル・アロー]",37001,"マジカル・アロー","魔法少女まどか☆マギカ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",120064,120065,"[O.N.G.E.K.I.]1.20-E-0017" -4,101219,"【SR】暁美 ほむら[繰り返す運命]",37002,"繰り返す運命","魔法少女まどか☆マギカ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",120066,120067,"[O.N.G.E.K.I.]1.20-E-0022" -4,101220,"【SSR】鹿目 まどか[円環の理]",37001,"円環の理","魔法少女まどか☆マギカ","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",115030,115031,"[O.N.G.E.K.I.]1.20-E-0018" -4,101221,"【SSR】暁美 ほむら[懐かしい笑顔]",37002,"懐かしい笑顔","魔法少女まどか☆マギカ","Fire","-",3,"60,265,286,299,309,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]1.20-E-0023" -4,101222,"【R】衛藤 可奈美[刀使]",45001,"刀使","刀使の巫女","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0041" -4,101223,"【R】十条 姫和[刀使]",45002,"刀使","刀使の巫女","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.20-E-0044" -4,101224,"【R】柳瀬 舞衣[刀使]",45003,"刀使","刀使の巫女","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.20-E-0046" -4,101225,"【R】糸見 沙耶香[刀使]",45004,"刀使","刀使の巫女","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0048" -4,101226,"【R】益子 薫[刀使]",45005,"刀使","刀使の巫女","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100001,100042,"[O.N.G.E.K.I.]1.20-E-0050" -4,101227,"【R】古波蔵 エレン[刀使]",45006,"刀使","刀使の巫女","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0052" -4,101228,"【R】燕 結芽[刀使]",45007,"刀使","刀使の巫女","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0054" -4,101229,"【SR】衛藤 可奈美[柳生新陰流]",45001,"柳生新陰流","刀使の巫女","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0042" -4,101230,"【SR】十条 姫和[鹿島新當流]",45002,"鹿島新當流","刀使の巫女","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110136,110137,"[O.N.G.E.K.I.]1.20-E-0045" -4,101231,"【SR】柳瀬 舞衣[北辰一刀流]",45003,"北辰一刀流","刀使の巫女","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110088,110089,"[O.N.G.E.K.I.]1.20-E-0047" -4,101232,"【SR】糸見 沙耶香[小野派一刀流]",45004,"小野派一刀流","刀使の巫女","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0049" -4,101233,"【SR】益子 薫[薬丸自顕流]",45005,"薬丸自顕流","刀使の巫女","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100016,100057,"[O.N.G.E.K.I.]1.20-E-0051" -4,101234,"【SR】古波蔵 エレン[タイ捨流]",45006,"タイ捨流","刀使の巫女","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",120074,120075,"[O.N.G.E.K.I.]1.20-E-0053" -4,101235,"【SR】燕 結芽[天然理心流]",45007,"天然理心流","刀使の巫女","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.20-E-0055" -4,101237,"【SSR】衛藤 可奈美[受け継いだ絆]",45001,"受け継いだ絆","刀使の巫女","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.20-E-0043" -4,101239,"【SR+】柏木 咲姫[ONGEKI Memorial Soundtrack Yuki]",1009,"ONGEKI Memorial Soundtrack Yuki","奏坂学園","Aqua","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",120058,120059,"[O.N.G.E.K.I.]Special Card" -4,101240,"【SR+】星咲 あかり[ONGEKI Vocal Party 03]",1000,"ONGEKI Vocal Party 03","プロモーション","Fire","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",100031,100072,"[O.N.G.E.K.I.]VP03-0002" -4,101241,"【SR+】高瀬 梨緒[ONGEKI Vocal Party 03]",1003,"ONGEKI Vocal Party 03","プロモーション","Aqua","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",100031,100072,"[O.N.G.E.K.I.]VP03-0001" -4,101242,"【SR+】桜井 春菜[ONGEKI Vocal Party 03]",1007,"ONGEKI Vocal Party 03","プロモーション","Fire","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",100031,100072,"[O.N.G.E.K.I.]VP03-0003" -4,101243,"【SR+】東雲 つむぎ[ONGEKI Vocal Party 04]",1015,"ONGEKI Vocal Party 04","プロモーション","Aqua","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",120076,120077,"[O.N.G.E.K.I.]VP04-0001" -4,101244,"【SR+】三角 葵[ONGEKI Vocal Party 04]",1002,"ONGEKI Vocal Party 04","プロモーション","Aqua","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",120076,120077,"[O.N.G.E.K.I.]VP04-0002" -4,101245,"【SR+】九條 楓[ONGEKI Vocal Party 04]",1008,"ONGEKI Vocal Party 04","プロモーション","Leaf","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",120076,120077,"[O.N.G.E.K.I.]VP04-0003" -5,101246,"【SR】星咲 あかり[最強 the Splash Dance!!]",1000,"最強 the Splash Dance!!","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",120060,120061,"" -5,101247,"【SR】藤沢 柚子[最強 the Splash Dance!!]",1001,"最強 the Splash Dance!!","奏坂学園","Leaf","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",120060,120061,"" -5,101248,"【SR】三角 葵[最強 the Splash Dance!!]",1002,"最強 the Splash Dance!!","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",120060,120061,"" -5,101249,"【SR】高瀬 梨緒[最強 the Splash Dance!!]",1003,"最強 the Splash Dance!!","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",120060,120061,"" -5,101250,"【SR】結城 莉玖[最強 the Splash Dance!!]",1004,"最強 the Splash Dance!!","奏坂学園","Fire","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",120060,120061,"" -5,101251,"【SR】藍原 椿[最強 the Splash Dance!!]",1005,"最強 the Splash Dance!!","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",120060,120061,"" -5,101252,"【SR】早乙女 彩華[最強 the Splash Dance!!]",1006,"最強 the Splash Dance!!","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",120060,120061,"" -5,101253,"【SR】桜井 春菜[最強 the Splash Dance!!]",1007,"最強 the Splash Dance!!","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",120060,120061,"" -5,101254,"【SR】九條 楓[最強 the Splash Dance!!]",1008,"最強 the Splash Dance!!","奏坂学園","Leaf","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",120060,120061,"" -5,101255,"【SR】柏木 咲姫[最強 the Splash Dance!!]",1009,"最強 the Splash Dance!!","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",120060,120061,"" -5,101256,"【SR】井之原 小星[最強 the Splash Dance!!]",1010,"最強 the Splash Dance!!","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",120060,120061,"" -5,101257,"【SR】逢坂 茜[最強 the Splash Dance!!]",1011,"最強 the Splash Dance!!","奏坂学園","Fire","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",120060,120061,"" -5,101258,"【SR】珠洲島 有栖[最強 the Splash Dance!!]",1012,"最強 the Splash Dance!!","奏坂学園","Aqua","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",120060,120061,"" -5,101259,"【SR】柏木 美亜[最強 the Splash Dance!!]",1013,"最強 the Splash Dance!!","奏坂学園","Fire","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",120060,120061,"" -5,101260,"【SR】日向 千夏[最強 the Splash Dance!!]",1014,"最強 the Splash Dance!!","奏坂学園","Leaf","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",120060,120061,"" -5,101261,"【SR】東雲 つむぎ[最強 the Splash Dance!!]",1015,"最強 the Splash Dance!!","奏坂学園","Aqua","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",120060,120061,"" -5,101262,"【SSR】星咲 あかり[「オンゲキ」LIVE vol.2]",1000,"「オンゲキ」LIVE vol.2","プロモーション","Fire","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",115030,115031,"[O.N.G.E.K.I.]Special Card" -4,101263,"【N】皇城 セツナ",1016,"対戦相手専用","奏坂学園","Fire","高校3年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"" -4,101264,"【R】皇城 セツナ[対戦相手専用]",1016,"対戦相手専用","奏坂学園","Fire","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100000,100000,"" -4,101265,"【N】皇城 セツナ",1016,"対戦相手専用","奏坂学園","Aqua","高校3年生",0,"50,197,212,227,242,257,287,317,347,347",100000,100041,"" -4,101266,"【R】皇城 セツナ[対戦相手専用]",1016,"対戦相手専用","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",100000,100000,"" -4,101267,"【SSR】ルーシア・オブ・エンド[動き出した鼓動]",41007,"動き出した鼓動","ノラと皇女と野良猫ハート","Aqua","-",3,"60,265,286,299,309,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]1.20-E-0161" -4,101269,"【R】初音ミク[対戦相手専用]",5000,"対戦相手専用","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100000,"" -4,101270,"【R】初音ミク[対戦相手専用]",5000,"対戦相手専用","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100000,"" -4,101271,"【SSR】初音ミク[一緒に歌おう!]",5000,"一緒に歌おう!","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.20-E-0115" -4,101272,"【R】吉田 優子[まぞく]",45008,"まぞく","まちカドまぞく","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.20-E-0033" -4,101273,"【R】千代田 桃[魔法少女]",45009,"魔法少女","まちカドまぞく","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0035" -4,101274,"【R】陽夏木 ミカン[魔法少女]",45010,"魔法少女","まちカドまぞく","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.20-E-0037" -4,101275,"【R】リリス[まぞく]",45011,"まぞく","まちカドまぞく","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.20-E-0039" -4,101276,"【SR】吉田 優子[これで勝ったと思うなよ~!]",45008,"これで勝ったと思うなよ~!","まちカドまぞく","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.20-E-0034" -4,101277,"【SR】千代田 桃[フレッシュピーチハートシャワー]",45009,"フレッシュピーチハートシャワー","まちカドまぞく","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.20-E-0036" -4,101278,"【SR】陽夏木 ミカン[魔法少女の休日]",45010,"魔法少女の休日","まちカドまぞく","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",120088,120089,"[O.N.G.E.K.I.]1.20-E-0038" -4,101279,"【SR】リリス[偉大なるごせんぞ様]",45011,"偉大なるごせんぞ様","まちカドまぞく","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100023,100064,"[O.N.G.E.K.I.]1.20-E-0040" -5,101293,"【SSR】柏木 咲姫[Daybreak Angels]",1009,"Daybreak Angels","奏坂学園","Aqua","高校3年生",3,"65,270,291,304,314,322,0,0,0,327",125032,125033,"[O.N.G.E.K.I.]1.25-0002" -5,101294,"【SSR】逢坂 茜[Daybreak Angels]",1011,"Daybreak Angels","奏坂学園","Fire","高校3年生",3,"65,270,291,304,314,322,0,0,0,327",125034,125035,"[O.N.G.E.K.I.]1.25-0003" -5,101295,"【SSR】三角 葵[秘宝を求めて]",1002,"秘宝を求めて","奏坂学園","Aqua","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",125036,125037,"[O.N.G.E.K.I.]1.25-0012" -5,101296,"【SSR】藤沢 柚子[ファンシー・ドリーミング]",1001,"ファンシー・ドリーミング","奏坂学園","Leaf","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",125060,125061,"[O.N.G.E.K.I.]1.25-0030" -5,101297,"【SSR】柏木 美亜[フルーツ・ブライダル]",1013,"フルーツ・ブライダル","奏坂学園","Fire","中学2年生",3,"65,270,291,304,314,322,0,0,0,327",125044,125045,"[O.N.G.E.K.I.]1.25-0023" -5,101298,"【SR】結城 莉玖[夜明けの缶コーヒー]",1004,"夜明けの缶コーヒー","奏坂学園","Fire","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",110066,110067,"[O.N.G.E.K.I.]1.25-0001" -5,101299,"【SR】珠洲島 有栖[進め!有栖隊員]",1012,"進め!有栖隊員","奏坂学園","Aqua","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",125038,125039,"[O.N.G.E.K.I.]1.25-0014" -5,101300,"【SR】井之原 小星[もくもくミッドナイト]",1010,"もくもくミッドナイト","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",125062,125063,"[O.N.G.E.K.I.]1.25-0031" -5,101301,"【SR】高瀬 梨緒[これからも、キミと]",1003,"これからも、キミと","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",125062,125063,"[O.N.G.E.K.I.]1.25-0018" -5,101302,"【SSR】日向 千夏[おいでよ!ジャングルツアーズ]",1014,"おいでよ!ジャングルツアーズ","奏坂学園","Leaf","中学2年生",3,"65,270,291,304,314,322,0,0,0,327",125036,125037,"[O.N.G.E.K.I.]1.25-0015" -5,101303,"【SSR】九條 楓[おいでよ!ジャングルツアーズ]",1008,"おいでよ!ジャングルツアーズ","奏坂学園","Leaf","高校3年生",3,"65,270,291,304,314,322,0,0,0,327",125040,125041,"[O.N.G.E.K.I.]1.25-0013" -5,101304,"【SSR】早乙女 彩華[Will you choose me?]",1006,"Will you choose me?","奏坂学園","Aqua","高校3年生",3,"65,270,291,304,314,322,0,0,0,327",125046,125047,"[O.N.G.E.K.I.]1.25-0021" -5,101305,"【SSR】藍原 椿[Will you choose me?]",1005,"Will you choose me?","奏坂学園","Leaf","高校1年生",3,"65,270,291,304,314,322,0,0,0,327",125048,125049,"[O.N.G.E.K.I.]1.25-0019" -5,101306,"【SR+】日向 千夏[くるりんスマイル]",1014,"くるりんスマイル","奏坂学園","Leaf","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",125066,125067,"[O.N.G.E.K.I.]1.25-0016" -5,101307,"【SR+】東雲 つむぎ[おさかなホリデー]",1015,"おさかなホリデー","奏坂学園","Aqua","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",125068,125069,"[O.N.G.E.K.I.]1.25-0029" -5,101308,"【SR+】柏木 美亜[sweets & me]",1013,"sweets & me","奏坂学園","Fire","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",125070,125071,"[O.N.G.E.K.I.]1.25-0005" -5,101309,"【SR+】藤沢 柚子[お願い!てるてる坊主]",1001,"お願い!てるてる坊主","奏坂学園","Leaf","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",100031,100072,"[O.N.G.E.K.I.]1.25-0017" -5,101310,"【SR+】早乙女 彩華[ぐらぐらエクササイズ]",1006,"ぐらぐらエクササイズ","奏坂学園","Aqua","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",125094,125095,"[O.N.G.E.K.I.]1.25-0024" -5,101311,"【SR+】藍原 椿[おでかけReady?]",1005,"おでかけReady?","奏坂学園","Leaf","高校1年生",12,"53,240,263,278,290,300,0,0,0,300",125096,125097,"[O.N.G.E.K.I.]1.25-0032" -5,101312,"【SSR】日向 千夏[デイドリーム・フェアリーズ]",1014,"デイドリーム・フェアリーズ","奏坂学園","Leaf","中学2年生",3,"65,270,291,304,314,322,0,0,0,327",100039,100080,"[O.N.G.E.K.I.]1.25-0006" -5,101313,"【SSR】柏木 美亜[デイドリーム・フェアリーズ]",1013,"デイドリーム・フェアリーズ","奏坂学園","Fire","中学2年生",3,"65,270,291,304,314,322,0,0,0,327",100038,100079,"[O.N.G.E.K.I.]1.25-0004" -5,101314,"【SSR】東雲 つむぎ[デイドリーム・フェアリーズ]",1015,"デイドリーム・フェアリーズ","奏坂学園","Aqua","中学2年生",3,"65,270,291,304,314,322,0,0,0,327",100040,100081,"[O.N.G.E.K.I.]1.25-0007" -5,101315,"【R】藍原 椿[ウエディングドレス]",1005,"ウエディングドレス","奏坂学園","Leaf","高校1年生",1,"50,197,212,227,242,257,0,0,0,257",125050,125051,"[O.N.G.E.K.I.]1.25-0020" -5,101316,"【R】早乙女 彩華[ウエディングドレス]",1006,"ウエディングドレス","奏坂学園","Aqua","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",125050,125051,"[O.N.G.E.K.I.]1.25-0022" -4,101317,"【SSR】星咲 あかり[ONGEKI Sound Collection 04]",1000,"ONGEKI Sound Collection 04","プロモーション","Fire","高校2年生",3,"65,262,285,300,312,322,0,0,0,327",105020,105021,"[O.N.G.E.K.I.]SC-0004" -5,101318,"【SSR】星咲 あかり[ONGEKI Sound Collection 05]",1000,"ONGEKI Sound Collection 05","プロモーション","Fire","高校2年生",3,"65,262,285,300,312,322,0,0,0,327",125082,125083,"[O.N.G.E.K.I.]SC-0005" -5,101319,"【SSR】高瀬 梨緒[自己評価が高い伝説の幽霊]",1003,"自己評価が高い伝説の幽霊","奏坂学園","Aqua","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",125058,125059,"[O.N.G.E.K.I.]1.25-0025" -5,101320,"【SSR】結城 莉玖[神に仇なす者]",1004,"神に仇なす者","奏坂学園","Fire","高校1年生",3,"65,270,291,304,314,322,0,0,0,327",125052,125053,"[O.N.G.E.K.I.]1.25-0026" -5,101321,"【SR】桜井 春菜[ごはんにする?]",1007,"ごはんにする?","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",125054,125055,"[O.N.G.E.K.I.]1.25-0027" -5,101340,"【SR+】柏木 咲姫[ONGEKI Vocal Party 05]",1009,"ONGEKI Vocal Party 05","プロモーション","Aqua","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",125084,125085,"[O.N.G.E.K.I.]VP05-0001" -5,101341,"【SR+】早乙女 彩華[ONGEKI Vocal Party 05]",1006,"ONGEKI Vocal Party 05","プロモーション","Aqua","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",125084,125085,"[O.N.G.E.K.I.]VP05-0002" -5,101342,"【SR+】日向 千夏[ONGEKI Vocal Party 05]",1014,"ONGEKI Vocal Party 05","プロモーション","Leaf","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",125084,125085,"[O.N.G.E.K.I.]VP05-0003" -6,101343,"【SR+】高瀬 梨緒[ONGEKI Vocal Party 06]",1003,"ONGEKI Vocal Party 06","プロモーション","Aqua","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",115126,115127,"[O.N.G.E.K.I.]VP06-0001" -6,101344,"【SR+】藍原 椿[ONGEKI Vocal Party 06]",1005,"ONGEKI Vocal Party 06","プロモーション","Leaf","高校1年生",12,"53,240,263,278,290,300,0,0,0,300",125086,125087,"[O.N.G.E.K.I.]VP06-0002" -6,101345,"【SR+】桜井 春菜[ONGEKI Vocal Party 06]",1007,"ONGEKI Vocal Party 06","プロモーション","Fire","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",125088,125089,"[O.N.G.E.K.I.]VP06-0003" -5,101346,"【SSR】東雲 つむぎ[気弱な新米防衛プログラム]",1015,"気弱な新米防衛プログラム","奏坂学園","Aqua","中学2年生",3,"65,270,291,304,314,322,0,0,0,327",125056,125057,"[O.N.G.E.K.I.]1.25-0028" -5,101347,"【R】ココア[Is the order a rabbit?]",45012,"Is the order a rabbit?","ご注文はうさぎですか?BLOOM","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0003" -5,101348,"【R】チノ[Is the order a rabbit?]",45013,"Is the order a rabbit?","ご注文はうさぎですか?BLOOM","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0007" -5,101349,"【R】リゼ[Is the order a rabbit?]",45014,"Is the order a rabbit?","ご注文はうさぎですか?BLOOM","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0010" -5,101350,"【R】千夜[Is the order a rabbit?]",45015,"Is the order a rabbit?","ご注文はうさぎですか?BLOOM","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",105004,105005,"[O.N.G.E.K.I.]1.25-E-0013" -5,101351,"【R】シャロ[Is the order a rabbit?]",45016,"Is the order a rabbit?","ご注文はうさぎですか?BLOOM","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",105004,105005,"[O.N.G.E.K.I.]1.25-E-0016" -5,101352,"【R】マヤ[Is the order a rabbit?]",45017,"Is the order a rabbit?","ご注文はうさぎですか?BLOOM","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110084,110085,"[O.N.G.E.K.I.]1.25-E-0019" -5,101353,"【R】メグ[Is the order a rabbit?]",45018,"Is the order a rabbit?","ご注文はうさぎですか?BLOOM","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110012,110013,"[O.N.G.E.K.I.]1.25-E-0022" -5,101354,"【SR】ココア[Dear My Sister]",45012,"Dear My Sister","ご注文はうさぎですか?BLOOM","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0001" -5,101355,"【SR】チノ[Dear My Sister]",45013,"Dear My Sister","ご注文はうさぎですか?BLOOM","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0005" -5,101356,"【SR】リゼ[Dear My Sister]",45014,"Dear My Sister","ご注文はうさぎですか?BLOOM","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.25-E-0008" -5,101357,"【SR】千夜[Dear My Sister]",45015,"Dear My Sister","ご注文はうさぎですか?BLOOM","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110114,110115,"[O.N.G.E.K.I.]1.25-E-0011" -5,101358,"【SR】シャロ[Dear My Sister]",45016,"Dear My Sister","ご注文はうさぎですか?BLOOM","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110072,110073,"[O.N.G.E.K.I.]1.25-E-0014" -5,101359,"【SR】マヤ[Dear My Sister]",45017,"Dear My Sister","ご注文はうさぎですか?BLOOM","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100017,100058,"[O.N.G.E.K.I.]1.25-E-0017" -5,101360,"【SR】メグ[Dear My Sister]",45018,"Dear My Sister","ご注文はうさぎですか?BLOOM","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",125006,125007,"[O.N.G.E.K.I.]1.25-E-0020" -5,101361,"【SR】ココア[ラビットハウスへようこそ]",45012,"ラビットハウスへようこそ","ご注文はうさぎですか?BLOOM","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",120074,120075,"[O.N.G.E.K.I.]1.25-E-0002" -5,101362,"【SR】チノ[狙いを定めて]",45013,"狙いを定めて","ご注文はうさぎですか?BLOOM","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110094,110095,"[O.N.G.E.K.I.]1.25-E-0006" -5,101363,"【SR】リゼ[うさみみ営業中]",45014,"うさみみ営業中","ご注文はうさぎですか?BLOOM","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.25-E-0009" -5,101364,"【SR】千夜[和洋衣装交換]",45015,"和洋衣装交換","ご注文はうさぎですか?BLOOM","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115022,115023,"[O.N.G.E.K.I.]1.25-E-0012" -5,101365,"【SR】シャロ[あなたはどうしてリゼなの]",45016,"あなたはどうしてリゼなの","ご注文はうさぎですか?BLOOM","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115028,115029,"[O.N.G.E.K.I.]1.25-E-0015" -5,101366,"【SR】マヤ[チマメ隊三人官女]",45017,"チマメ隊三人官女","ご注文はうさぎですか?BLOOM","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",125008,125009,"[O.N.G.E.K.I.]1.25-E-0018" -5,101367,"【SR】メグ[chimame march]",45018,"chimame march","ご注文はうさぎですか?BLOOM","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",125010,125011,"[O.N.G.E.K.I.]1.25-E-0021" -5,101368,"【SSR】チノ[お泊まりラビット]",45013,"お泊まりラビット","ご注文はうさぎですか?BLOOM","Leaf","-",3,"60,265,286,299,309,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]1.25-E-0046" -5,101369,"【SSR】ココア[かわいさだけを、ブレンドしました。]",45012,"かわいさだけを、ブレンドしました。","ご注文はうさぎですか?BLOOM","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",110068,110069,"[O.N.G.E.K.I.]1.25-E-0045" -5,101370,"【SSR】チノ[もふもふパジャマ]",45013,"もふもふパジャマ","ご注文はうさぎですか?BLOOM","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",110070,110071,"[O.N.G.E.K.I.]1.25-E-0004" -5,101371,"【R】Two for all[モンソニ!]",45019,"モンソニ!","モンソニ!","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0025" -5,101372,"【R】Angely Diva[モンソニ!]",45020,"モンソニ!","モンソニ!","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.25-E-0028" -5,101373,"【R】白雪姫リボン[モンソニ!]",45021,"モンソニ!","モンソニ!","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0031" -5,101374,"【SR】Two for all[全力アイドル]",45019,"全力アイドル","モンソニ!","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110088,110089,"[O.N.G.E.K.I.]1.25-E-0023" -5,101375,"【SR】Angely Diva[天界の歌姫]",45020,"天界の歌姫","モンソニ!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.25-E-0026" -5,101376,"【SR】白雪姫リボン[森の歌姫]",45021,"森の歌姫","モンソニ!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0029" -5,101377,"【SR】Two for all[二人のキズナ]",45019,"二人のキズナ","モンソニ!","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0024" -5,101378,"【SR】Angely Diva[祝福の歌声]",45020,"祝福の歌声","モンソニ!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0027" -5,101379,"【SR】白雪姫リボン[永遠不滅アイドル]",45021,"永遠不滅アイドル","モンソニ!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110114,110115,"[O.N.G.E.K.I.]1.25-E-0030" -5,101380,"【SSR】Two for all[LINK]",45019,"LINK","モンソニ!","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.25-E-0125" -5,101381,"【SSR】Angely Diva[キズナ Sparkling World]",45020,"キズナ Sparkling World","モンソニ!","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.25-E-0126" -5,101382,"【SSR】白雪姫リボン[初恋は君色メモリー]",45021,"初恋は君色メモリー","モンソニ!","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.25-E-0127" -5,101383,"【R】春音 あいら[オーロラドリーム]",45022,"オーロラドリーム","プリティーシリーズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0049" -5,101384,"【R】天宮 りずむ[オーロラドリーム]",45023,"オーロラドリーム","プリティーシリーズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.25-E-0051" -5,101385,"【R】高峰 みおん[オーロラドリーム]",45024,"オーロラドリーム","プリティーシリーズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0053" -5,101386,"【R】上葉 みあ[ディアマイフューチャー]",45025,"ディアマイフューチャー","プリティーシリーズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0055" -5,101387,"【R】深山 れいな[ディアマイフューチャー]",45026,"ディアマイフューチャー","プリティーシリーズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0057" -5,101388,"【R】志々美 かりん[ディアマイフューチャー]",45027,"ディアマイフューチャー","プリティーシリーズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.25-E-0059" -5,101389,"【R】大瑠璃 あやみ[ディアマイフューチャー]",45028,"ディアマイフューチャー","プリティーシリーズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",115146,115147,"[O.N.G.E.K.I.]1.25-E-0061" -5,101390,"【R】彩瀬 なる[レインボーライブ]",45029,"レインボーライブ","プリティーシリーズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0063" -5,101391,"【R】涼野 いと[レインボーライブ]",45030,"レインボーライブ","プリティーシリーズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0065" -5,101392,"【R】福原 あん[レインボーライブ]",45031,"レインボーライブ","プリティーシリーズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.25-E-0067" -5,101393,"【R】真中 らぁら[プリパラ]",45032,"プリパラ","プリティーシリーズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0085" -5,101394,"【R】南 みれぃ[プリパラ]",45033,"プリパラ","プリティーシリーズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0087" -5,101395,"【R】北条 そふぃ[プリパラ]",45034,"プリパラ","プリティーシリーズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.25-E-0089" -5,101396,"【R】夢川 ゆい[アイドルタイムプリパラ]",45035,"アイドルタイムプリパラ","プリティーシリーズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.25-E-0091" -5,101397,"【R】幸多 みちる[アイドルタイムプリパラ]",45037,"アイドルタイムプリパラ","プリティーシリーズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0093" -5,101398,"【R】虹色 にの[アイドルタイムプリパラ]",45036,"アイドルタイムプリパラ","プリティーシリーズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0095" -5,101399,"【R】桃山 みらい[キラッとプリ☆チャン]",45038,"キラッとプリ☆チャン","プリティーシリーズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0099" -5,101400,"【R】萌黄 えも[キラッとプリ☆チャン]",45039,"キラッとプリ☆チャン","プリティーシリーズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0101" -5,101401,"【R】青葉 りんか[キラッとプリ☆チャン]",45040,"キラッとプリ☆チャン","プリティーシリーズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.25-E-0103" -5,101402,"【SR】春音 あいら[MARs]",45022,"MARs","プリティーシリーズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0048" -5,101403,"【SR】天宮 りずむ[MARs]",45023,"MARs","プリティーシリーズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110086,110087,"[O.N.G.E.K.I.]1.25-E-0050" -5,101404,"【SR】高峰 みおん[MARs]",45024,"MARs","プリティーシリーズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0052" -5,101405,"【SR】上葉 みあ[Prizmmy☆]",45025,"Prizmmy☆","プリティーシリーズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.25-E-0054" -5,101406,"【SR】深山 れいな[Prizmmy☆]",45026,"Prizmmy☆","プリティーシリーズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.25-E-0056" -5,101407,"【SR】志々美 かりん[Prizmmy☆]",45027,"Prizmmy☆","プリティーシリーズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110140,110141,"[O.N.G.E.K.I.]1.25-E-0058" -5,101408,"【SR】大瑠璃 あやみ[Prizmmy☆]",45028,"Prizmmy☆","プリティーシリーズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100018,100059,"[O.N.G.E.K.I.]1.25-E-0060" -5,101409,"【SR】彩瀬 なる[ハッピーレイン♪]",45029,"ハッピーレイン♪","プリティーシリーズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.25-E-0062" -5,101410,"【SR】涼野 いと[ハッピーレイン♪]",45030,"ハッピーレイン♪","プリティーシリーズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.25-E-0064" -5,101411,"【SR】福原 あん[ハッピーレイン♪]",45031,"ハッピーレイン♪","プリティーシリーズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",115038,115039,"[O.N.G.E.K.I.]1.25-E-0066" -5,101412,"【SR】真中 らぁら[SoLaMi♡SMILE]",45032,"SoLaMi♡SMILE","プリティーシリーズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",105042,105043,"[O.N.G.E.K.I.]1.25-E-0084" -5,101413,"【SR】南 みれぃ[SoLaMi♡SMILE]",45033,"SoLaMi♡SMILE","プリティーシリーズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",105042,105043,"[O.N.G.E.K.I.]1.25-E-0086" -5,101414,"【SR】北条 そふぃ[SoLaMi♡SMILE]",45034,"SoLaMi♡SMILE","プリティーシリーズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115028,115029,"[O.N.G.E.K.I.]1.25-E-0088" -5,101415,"【SR】夢川 ゆい[MY☆DREAM]",45035,"MY☆DREAM","プリティーシリーズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110138,110139,"[O.N.G.E.K.I.]1.25-E-0090" -5,101416,"【SR】幸多 みちる[MY☆DREAM]",45037,"MY☆DREAM","プリティーシリーズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0092" -5,101417,"【SR】虹色 にの[MY☆DREAM]",45036,"MY☆DREAM","プリティーシリーズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0094" -5,101418,"【SR】桃山 みらい[Miracle☆Kiratts]",45038,"Miracle☆Kiratts","プリティーシリーズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0098" -5,101419,"【SR】萌黄 えも[Miracle☆Kiratts]",45039,"Miracle☆Kiratts","プリティーシリーズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0100" -5,101420,"【SR】青葉 りんか[Miracle☆Kiratts]",45040,"Miracle☆Kiratts","プリティーシリーズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110088,110089,"[O.N.G.E.K.I.]1.25-E-0102" -5,101421,"【SSR】春音 あいら[なりたい自分にプリズムジャンプ!]",45022,"なりたい自分にプリズムジャンプ!","プリティーシリーズ","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.25-E-0047" -5,101422,"【SSR】真中 らぁら[み~んなトモダチ!!み~んなアイドル!!]",45032,"み~んなトモダチ!!み~んなアイドル!!","プリティーシリーズ","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.25-E-0083" -5,101423,"【SSR】桃山 みらい[キラッと輝け!]",45038,"キラッと輝け!","プリティーシリーズ","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.25-E-0096" -5,101424,"【R】絢辻 詞[輝日東高校2年]",45049,"輝日東高校2年","アマガミSS","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0070" -5,101425,"【R】桜井 梨穂子[輝日東高校2年]",45050,"輝日東高校2年","アマガミSS","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100003,100044,"[O.N.G.E.K.I.]1.25-E-0072" -5,101426,"【R】棚町 薫[輝日東高校2年]",45051,"輝日東高校2年","アマガミSS","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0074" -5,101427,"【R】中多 紗江[輝日東高校1年]",45052,"輝日東高校1年","アマガミSS","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",105004,105005,"[O.N.G.E.K.I.]1.25-E-0076" -5,101428,"【R】七咲 逢[輝日東高校1年]",45053,"輝日東高校1年","アマガミSS","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0078" -5,101429,"【R】森島 はるか[輝日東高校3年]",45054,"輝日東高校3年","アマガミSS","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0080" -5,101430,"【R】橘 美也[輝日東高校1年]",45055,"輝日東高校1年","アマガミSS","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.25-E-0082" -5,101431,"【SR】絢辻 詞[天下無敵の仮面優等生]",45049,"天下無敵の仮面優等生","アマガミSS","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",115006,115007,"[O.N.G.E.K.I.]1.25-E-0069" -5,101432,"【SR】桜井 梨穂子[ぽっちゃり幼馴染]",45050,"ぽっちゃり幼馴染","アマガミSS","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100018,100059,"[O.N.G.E.K.I.]1.25-E-0071" -5,101433,"【SR】棚町 薫[気ままでさばさばした悪友]",45051,"気ままでさばさばした悪友","アマガミSS","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.25-E-0073" -5,101434,"【SR】中多 紗江[ふかふかボディの純情少女]",45052,"ふかふかボディの純情少女","アマガミSS","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",115064,115065,"[O.N.G.E.K.I.]1.25-E-0075" -5,101435,"【SR】七咲 逢[面倒見のよいクールな後輩]",45053,"面倒見のよいクールな後輩","アマガミSS","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0077" -5,101436,"【SR】森島 はるか[男殺しの天然女王]",45054,"男殺しの天然女王","アマガミSS","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.25-E-0079" -5,101437,"【SR】橘 美也[きまぐれ我侭甘えん坊]",45055,"きまぐれ我侭甘えん坊","アマガミSS","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110086,110087,"[O.N.G.E.K.I.]1.25-E-0081" -5,101438,"【SSR】絢辻 詞[私を見つけて]",45049,"私を見つけて","アマガミSS","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",105056,105057,"[O.N.G.E.K.I.]1.25-E-0068" -5,101439,"【R】水原 千鶴[レンカノ]",45041,"レンカノ","彼女、お借りします","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.25-E-0115" -5,101440,"【R】七海 麻美[元カノ]",45042,"元カノ","彼女、お借りします","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0118" -5,101441,"【R】更科 瑠夏[レンカノ]",45043,"レンカノ","彼女、お借りします","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0121" -5,101442,"【R】桜沢 墨[レンカノ]",45044,"レンカノ","彼女、お借りします","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",115148,115149,"[O.N.G.E.K.I.]1.25-E-0124" -5,101443,"【SR】水原 千鶴[遊園地デート]",45041,"遊園地デート","彼女、お借りします","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",125012,125013,"[O.N.G.E.K.I.]1.25-E-0113" -5,101444,"【SR】七海 麻美[砂浜デート]",45042,"砂浜デート","彼女、お借りします","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",120074,120075,"[O.N.G.E.K.I.]1.25-E-0116" -5,101445,"【SR】更科 瑠夏[ボルダリングデート]",45043,"ボルダリングデート","彼女、お借りします","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.25-E-0119" -5,101446,"【SR】桜沢 墨[ボウリングデート]",45044,"ボウリングデート","彼女、お借りします","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100016,100057,"[O.N.G.E.K.I.]1.25-E-0122" -5,101447,"【SR】水原 千鶴[清楚可憐な理想の彼女]",45041,"清楚可憐な理想の彼女","彼女、お借りします","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0114" -5,101448,"【SR】七海 麻美[ゆるふわ系小悪魔彼女]",45042,"ゆるふわ系小悪魔彼女","彼女、お借りします","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.25-E-0117" -5,101449,"【SR】更科 瑠夏[超積極的妹系アイドル彼女]",45043,"超積極的妹系アイドル彼女","彼女、お借りします","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110094,110095,"[O.N.G.E.K.I.]1.25-E-0120" -5,101450,"【SR】桜沢 墨[笑顔がかわいい頑張りや彼女]",45044,"笑顔がかわいい頑張りや彼女","彼女、お借りします","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",115064,115065,"[O.N.G.E.K.I.]1.25-E-0123" -5,101451,"【SSR】水原 千鶴[たった一度のレンタルで、輝き出すリアルがある!]",45041,"たった一度のレンタルで、輝き出すリアルがある!","彼女、お借りします","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",100138,100139,"[O.N.G.E.K.I.]1.25-E-0112" -5,101452,"【R】エミリア[王選候補者]",45056,"王選候補者","Re:ゼロから始める異世界生活","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.25-E-0144" -5,101453,"【R】レム[ロズワール邸メイド]",45057,"ロズワール邸メイド","Re:ゼロから始める異世界生活","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0148" -5,101454,"【R】ラム[ロズワール邸メイド]",45058,"ロズワール邸メイド","Re:ゼロから始める異世界生活","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0152" -5,101455,"【R】ベアトリス[禁書庫の司書]",45059,"禁書庫の司書","Re:ゼロから始める異世界生活","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0153" -5,101456,"【R】フェルト[王選候補者]",45060,"王選候補者","Re:ゼロから始める異世界生活","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0154" -5,101457,"【R】アナスタシア[王選候補者]",45061,"王選候補者","Re:ゼロから始める異世界生活","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110106,110107,"[O.N.G.E.K.I.]1.25-E-0155" -5,101458,"【R】プリシラ[王選候補者]",45062,"王選候補者","Re:ゼロから始める異世界生活","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110106,110107,"[O.N.G.E.K.I.]1.25-E-0156" -5,101459,"【R】クルシュ[王選候補者]",45063,"王選候補者","Re:ゼロから始める異世界生活","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110106,110107,"[O.N.G.E.K.I.]1.25-E-0157" -5,101460,"【SR】エミリア[ハーフエルフの少女]",45056,"ハーフエルフの少女","Re:ゼロから始める異世界生活","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115064,115065,"[O.N.G.E.K.I.]1.25-E-0141" -5,101461,"【SR】レム[鬼族のメイド]",45057,"鬼族のメイド","Re:ゼロから始める異世界生活","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0146" -5,101462,"【SR】ラム[鬼族のメイド]",45058,"鬼族のメイド","Re:ゼロから始める異世界生活","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.25-E-0149" -5,101463,"【SR】エミリア[お団子食べる?]",45056,"お団子食べる?","Re:ゼロから始める異世界生活","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",105008,105009,"[O.N.G.E.K.I.]1.25-E-0142" -5,101464,"【SR】レム[三人でお出かけ]",45057,"三人でお出かけ","Re:ゼロから始める異世界生活","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110094,110095,"[O.N.G.E.K.I.]1.25-E-0147" -5,101465,"【SR】ラム[姉妹の初詣]",45058,"姉妹の初詣","Re:ゼロから始める異世界生活","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",120074,120075,"[O.N.G.E.K.I.]1.25-E-0150" -5,101466,"【SR】エミリア[天使の休息]",45056,"天使の休息","Re:ゼロから始める異世界生活","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115036,115037,"[O.N.G.E.K.I.]1.25-E-0143" -5,101468,"【SR】ラム[制服姉妹]",45058,"制服姉妹","Re:ゼロから始める異世界生活","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",120088,120089,"[O.N.G.E.K.I.]1.25-E-0151" -5,101469,"【SSR】レム[純白の花嫁]",45057,"純白の花嫁","Re:ゼロから始める異世界生活","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",110070,110071,"[O.N.G.E.K.I.]1.25-E-0145" -5,101470,"【SSR】レム[常夏の異世界]",45057,"常夏の異世界","Re:ゼロから始める異世界生活","Aqua","-",3,"60,265,286,299,309,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]1.25-E-0159" -5,101471,"【SSR】エミリア[満開の花の下で]",45056,"満開の花の下で","Re:ゼロから始める異世界生活","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",125042,125043,"[O.N.G.E.K.I.]1.25-E-0158" -5,101472,"【R】ハチロク[レイルロオド]",45064,"レイルロオド","まいてつ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0164" -5,101473,"【R】右田 日々姫[駆け出しデザイナー]",45065,"駆け出しデザイナー","まいてつ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0167" -5,101474,"【R】雛衣 ポーレット[御一夜市長]",45066,"御一夜市長","まいてつ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.25-E-0170" -5,101475,"【R】れいな[レイルロオド]",45067,"レイルロオド","まいてつ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0172" -5,101476,"【R】蓑笠 凪[蓑笠鍛冶店の看板娘]",45068,"蓑笠鍛冶店の看板娘","まいてつ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0174" -5,101477,"【R】早瀬 ふかみ[クマ川下りの看板娘]",45069,"クマ川下りの看板娘","まいてつ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.25-E-0176" -5,101478,"【SR】ハチロク[絵に描いたような日ノ本撫子]",45064,"絵に描いたような日ノ本撫子","まいてつ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0161" -5,101479,"【SR】右田 日々姫[にぃにに恋する絵描きの少女]",45065,"にぃにに恋する絵描きの少女","まいてつ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.25-E-0165" -5,101480,"【SR】雛衣 ポーレット[市長で社長ながんばりやさん]",45066,"市長で社長ながんばりやさん","まいてつ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110086,110087,"[O.N.G.E.K.I.]1.25-E-0168" -5,101481,"【SR】れいな[静かなる開戦]",45067,"静かなる開戦","まいてつ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0171" -5,101482,"【SR】蓑笠 凪[凪の欲しいもの]",45068,"凪の欲しいもの","まいてつ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0173" -5,101483,"【SR】早瀬 ふかみ[クマ川から見上げて]",45069,"クマ川から見上げて","まいてつ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100018,100059,"[O.N.G.E.K.I.]1.25-E-0175" -5,101484,"【SR】ハチロク[8620形蒸気機関車]",45064,"8620形蒸気機関車","まいてつ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",125018,125019,"[O.N.G.E.K.I.]1.25-E-0163" -5,101485,"【SR】ハチロク[御一夜市物語]",45064,"御一夜市物語","まいてつ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.25-E-0162" -5,101486,"【SR】右田 日々姫[日ノ本を代表する車両デザイナー]",45065,"日ノ本を代表する車両デザイナー","まいてつ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",105042,105043,"[O.N.G.E.K.I.]1.25-E-0166" -5,101487,"【SR】雛衣 ポーレット[家族で足湯体験]",45066,"家族で足湯体験","まいてつ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",105042,105043,"[O.N.G.E.K.I.]1.25-E-0169" -5,101488,"【SSR】ハチロク[もう一度、出発進行!!]",45064,"もう一度、出発進行!!","まいてつ","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",110070,110071,"[O.N.G.E.K.I.]1.25-E-0160" -5,101489,"【SR】一筒[老頭牌]",12,"老頭牌","奏坂学園","Aqua","-",2,"55,227,242,257,272,287,0,0,0,287",125072,125073,"[O.N.G.E.K.I.]1.25-E-0032" -5,101490,"【SR】九筒[老頭牌]",12,"老頭牌","奏坂学園","Aqua","-",2,"55,227,242,257,272,287,0,0,0,287",125072,125073,"[O.N.G.E.K.I.]1.25-E-0033" -5,101491,"【SR】一萬[老頭牌]",12,"老頭牌","奏坂学園","Fire","-",2,"55,227,242,257,272,287,0,0,0,287",125072,125073,"[O.N.G.E.K.I.]1.25-E-0034" -5,101492,"【SR】九萬[老頭牌]",12,"老頭牌","奏坂学園","Fire","-",2,"55,227,242,257,272,287,0,0,0,287",125072,125073,"[O.N.G.E.K.I.]1.25-E-0035" -5,101493,"【SR】一索[老頭牌]",12,"老頭牌","奏坂学園","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",125072,125073,"[O.N.G.E.K.I.]1.25-E-0036" -5,101494,"【SR】九索[老頭牌]",12,"老頭牌","奏坂学園","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",125072,125073,"[O.N.G.E.K.I.]1.25-E-0037" -5,101495,"【SR】東[風牌]",13,"風牌","奏坂学園","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",125074,125075,"[O.N.G.E.K.I.]1.25-E-0038" -5,101496,"【SR】南 [風牌]",13,"風牌","奏坂学園","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",125074,125075,"[O.N.G.E.K.I.]1.25-E-0039" -5,101497,"【SR】西[風牌]",13,"風牌","奏坂学園","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",125074,125075,"[O.N.G.E.K.I.]1.25-E-0040" -5,101498,"【SR】北[風牌]",13,"風牌","奏坂学園","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",125074,125075,"[O.N.G.E.K.I.]1.25-E-0041" -5,101499,"【SR】白[三元牌]",14,"三元牌","奏坂学園","Aqua","-",2,"55,227,242,257,272,287,0,0,0,287",125076,125077,"[O.N.G.E.K.I.]1.25-E-0042" -5,101500,"【SR】發[三元牌]",14,"三元牌","奏坂学園","Leaf","-",2,"55,227,242,257,272,287,0,0,0,287",125076,125077,"[O.N.G.E.K.I.]1.25-E-0043" -5,101501,"【SR】中[三元牌]",14,"三元牌","奏坂学園","Fire","-",2,"55,227,242,257,272,287,0,0,0,287",125076,125077,"[O.N.G.E.K.I.]1.25-E-0044" -5,101502,"【SR】九條 楓[ツモ!]",1008,"ツモ!","奏坂学園","Leaf","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",125090,125091,"[O.N.G.E.K.I.]1.25-0008" -5,101503,"【SR】逢坂 茜[奏坂高校三年生]",1011,"奏坂高校三年生","奏坂学園","Fire","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",125092,125093,"[O.N.G.E.K.I.]1.25-0010" -5,101504,"【SR】珠洲島 有栖[奏坂高校一年生]",1012,"奏坂高校一年生","奏坂学園","Aqua","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",125092,125093,"[O.N.G.E.K.I.]1.25-0011" -5,101505,"【SR】九條 楓[奏坂高校三年生]",1008,"奏坂高校三年生","奏坂学園","Leaf","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",125092,125093,"[O.N.G.E.K.I.]1.25-0009" -5,101506,"【R】Two for all[対戦相手専用]",45019,"対戦相手専用","モンソニ!","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100000,"" -5,101507,"【R】Angely Diva[対戦相手専用]",45020,"対戦相手専用","モンソニ!","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100000,"" -5,101508,"【R】白雪姫リボン[対戦相手専用]",45021,"対戦相手専用","モンソニ!","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100000,"" -5,101509,"【R】メグメグ[ガンナー]",45045,"ガンナー","#コンパス","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0105" -5,101510,"【R】双挽 乃保[アタッカー]",45046,"アタッカー","#コンパス","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0107" -5,101511,"【R】魔法少女リリカ[ガンナー]",45047,"ガンナー","#コンパス","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.25-E-0109" -5,101512,"【R】コクリコット ブランシュ[スプリンター]",45048,"スプリンター","#コンパス","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.25-E-0111" -5,101513,"【SR】メグメグ[バイオレンストリガー]",45045,"バイオレンストリガー","#コンパス","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.25-E-0104" -5,101514,"【SR】双挽 乃保[キレキャリオン]",45046,"キレキャリオン","#コンパス","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0106" -5,101515,"【SR】魔法少女リリカ[アルカリレットウセイ]",45047,"アルカリレットウセイ","#コンパス","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110136,110137,"[O.N.G.E.K.I.]1.25-E-0108" -5,101516,"【SR】コクリコット ブランシュ[撥条少女時計]",45048,"撥条少女時計","#コンパス","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0110" -5,101517,"【SR】草津 結衣奈[湯夢色バトン]",25001,"湯夢色バトン","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125020,125021,"[O.N.G.E.K.I.]1.25-E-0177" -5,101518,"【SR】箱根 彩耶[湯夢色バトン]",25001,"湯夢色バトン","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125020,125021,"[O.N.G.E.K.I.]1.25-E-0179" -5,101519,"【SR】秋保 那菜子[湯夢色バトン]",25001,"湯夢色バトン","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125024,125025,"[O.N.G.E.K.I.]1.25-E-0181" -5,101520,"【SR】有馬 輪花[湯夢色バトン]",25001,"湯夢色バトン","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125022,125023,"[O.N.G.E.K.I.]1.25-E-0183" -5,101521,"【SR】道後 泉海[湯夢色バトン]",25001,"湯夢色バトン","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125026,125027,"[O.N.G.E.K.I.]1.25-E-0185" -5,101522,"【SR】登別 綾瀬[湯夢色バトン]",25001,"湯夢色バトン","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125022,125023,"[O.N.G.E.K.I.]1.25-E-0187" -5,101523,"【SR】下呂 美月[湯夢色バトン]",25001,"湯夢色バトン","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125024,125025,"[O.N.G.E.K.I.]1.25-E-0189" -5,101524,"【SR】有馬 楓花[湯夢色バトン]",25001,"湯夢色バトン","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125026,125027,"[O.N.G.E.K.I.]1.25-E-0192" -5,101525,"【SR】奏・バーデン・由布院[湯夢色バトン]",25001,"湯夢色バトン","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125020,125021,"[O.N.G.E.K.I.]1.25-E-0194" -5,101526,"【SSR】有馬 楓花[有馬涼風川座敷]",25001,"有馬涼風川座敷","温泉むすめ","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",125028,125029,"[O.N.G.E.K.I.]1.25-E-0191" -5,101527,"【SR】草津 結衣奈[草津温泉]",25001,"草津温泉","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125030,125031,"[O.N.G.E.K.I.]1.25-E-0178" -5,101528,"【SR】箱根 彩耶[箱根温泉]",25001,"箱根温泉","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125030,125031,"[O.N.G.E.K.I.]1.25-E-0180" -5,101529,"【SR】秋保 那菜子[秋保温泉]",25001,"秋保温泉","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125030,125031,"[O.N.G.E.K.I.]1.25-E-0182" -5,101530,"【SR】有馬 輪花[有馬温泉]",25001,"有馬温泉","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125030,125031,"[O.N.G.E.K.I.]1.25-E-0184" -5,101531,"【SR】道後 泉海[道後温泉]",25001,"道後温泉","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125030,125031,"[O.N.G.E.K.I.]1.25-E-0186" -5,101532,"【SR】登別 綾瀬[登別温泉]",25001,"登別温泉","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125030,125031,"[O.N.G.E.K.I.]1.25-E-0188" -5,101533,"【SR】下呂 美月[下呂温泉]",25001,"下呂温泉","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125030,125031,"[O.N.G.E.K.I.]1.25-E-0190" -5,101534,"【SR】有馬 楓花[有馬温泉]",25001,"有馬温泉","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125030,125031,"[O.N.G.E.K.I.]1.25-E-0193" -5,101535,"【SR】奏・バーデン・由布院[由布院温泉]",25001,"由布院温泉","温泉むすめ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125030,125031,"[O.N.G.E.K.I.]1.25-E-0195" -5,101536,"【SR】星咲 あかり[No Limit STARRED HEART]",1000,"No Limit STARRED HEART","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"" -5,101537,"【SR】藤沢 柚子[No Limit STARRED HEART]",1001,"No Limit STARRED HEART","奏坂学園","Leaf","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"[O.N.G.E.K.I.]Special Card" -5,101538,"【SR】三角 葵[No Limit STARRED HEART]",1002,"No Limit STARRED HEART","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"" -5,101539,"【SR】高瀬 梨緒[No Limit STARRED HEART]",1003,"No Limit STARRED HEART","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"" -5,101540,"【SR】結城 莉玖[No Limit STARRED HEART]",1004,"No Limit STARRED HEART","奏坂学園","Fire","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"" -5,101541,"【SR】藍原 椿[No Limit STARRED HEART]",1005,"No Limit STARRED HEART","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"" -5,101542,"【SR】早乙女 彩華[No Limit STARRED HEART]",1006,"No Limit STARRED HEART","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"" -5,101543,"【SR】桜井 春菜[No Limit STARRED HEART]",1007,"No Limit STARRED HEART","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"" -5,101544,"【SR】九條 楓[No Limit STARRED HEART]",1008,"No Limit STARRED HEART","奏坂学園","Leaf","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"" -5,101545,"【SR】柏木 咲姫[No Limit STARRED HEART]",1009,"No Limit STARRED HEART","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"" -5,101546,"【SR】井之原 小星[No Limit STARRED HEART]",1010,"No Limit STARRED HEART","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"" -5,101547,"【SR】逢坂 茜[No Limit STARRED HEART]",1011,"No Limit STARRED HEART","奏坂学園","Fire","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"" -5,101548,"【SR】珠洲島 有栖[No Limit STARRED HEART]",1012,"No Limit STARRED HEART","奏坂学園","Aqua","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"" -5,101549,"【SR】柏木 美亜[No Limit STARRED HEART]",1013,"No Limit STARRED HEART","奏坂学園","Fire","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"" -5,101550,"【SR】日向 千夏[No Limit STARRED HEART]",1014,"No Limit STARRED HEART","奏坂学園","Leaf","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"" -5,101551,"【SR】東雲 つむぎ[No Limit STARRED HEART]",1015,"No Limit STARRED HEART","奏坂学園","Aqua","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"" -5,101552,"【SR】皇城 セツナ[No Limit STARRED HEART]",1016,"No Limit STARRED HEART","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",125080,125081,"" -5,101553,"【SSR】星咲 あかり[3rd Anniversary]",1000,"3rd Anniversary","プロモーション","Fire","高校2年生",3,"52,237,252,267,282,297,0,0,0,302",110070,110071,"[O.N.G.E.K.I.]Special Card" -5,101554,"【SSR】星咲 あかり[私たちは、負けない!]",1000,"私たちは、負けない!","プロモーション","Fire","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",105020,105021,"[O.N.G.E.K.I.]Special Card" -5,101555,"【SSR】藤沢 柚子[私たちは、負けない!]",1001,"私たちは、負けない!","プロモーション","Leaf","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",105020,105021,"" -5,101556,"【SSR】三角 葵[私たちは、負けない!]",1002,"私たちは、負けない!","プロモーション","Aqua","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",105020,105021,"" -5,101557,"【SSR】皇城 セツナ[私たちは、負けない!]",1016,"私たちは、負けない!","プロモーション","Leaf","高校3年生",3,"60,257,280,295,307,317,0,0,0,322",105020,105021,"" -5,101558,"【SR】星咲 あかり[STARRED HEART]",1000,"STARRED HEART","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",125078,125079,"[O.N.G.E.K.I.]Special Card" -5,101559,"【SR】結城 莉玖[STARRED HEART]",1004,"STARRED HEART","奏坂学園","Fire","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",125078,125079,"[O.N.G.E.K.I.]Special Card" -5,101560,"【SR】九條 楓[STARRED HEART]",1008,"STARRED HEART","奏坂学園","Leaf","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",125078,125079,"[O.N.G.E.K.I.]Special Card" -5,101561,"【SR】珠洲島 有栖[STARRED HEART]",1012,"STARRED HEART","奏坂学園","Aqua","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",125078,125079,"[O.N.G.E.K.I.]Special Card" -5,101562,"【SR】東雲 つむぎ[STARRED HEART]",1015,"STARRED HEART","奏坂学園","Aqua","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",125078,125079,"[O.N.G.E.K.I.]Special Card" -5,101568,"【SSR】桃山 みらい[プリティーシリーズ 10th Anniversary]",45038,"プリティーシリーズ 10th Anniversary","プリティーシリーズ","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",100031,100072,"[O.N.G.E.K.I.]1.25-E-0097" -5,101578,"【SR】博麗 霊夢[TOUHOU MEGANE]",2000,"TOUHOU MEGANE","東方Project","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",105042,105043,"[O.N.G.E.K.I.]1.25-E-0130" -5,101579,"【SR】霧雨 魔理沙[TOUHOU MEGANE]",2001,"TOUHOU MEGANE","東方Project","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",105044,105045,"[O.N.G.E.K.I.]1.25-E-0131" -5,101580,"【SR】十六夜 咲夜[TOUHOU MEGANE]",2002,"TOUHOU MEGANE","東方Project","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.25-E-0133" -5,101581,"【SR】レミリア・スカーレット[TOUHOU MEGANE]",2003,"TOUHOU MEGANE","東方Project","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",115036,115037,"[O.N.G.E.K.I.]1.25-E-0135" -5,101582,"【SR】フランドール・スカーレット[TOUHOU MEGANE]",2004,"TOUHOU MEGANE","東方Project","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",105044,105045,"[O.N.G.E.K.I.]1.25-E-0136" -5,101583,"【SR】紅 美鈴[TOUHOU MEGANE]",2005,"TOUHOU MEGANE","東方Project","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.25-E-0137" -5,101584,"【SR】パチュリー・ノーレッジ[TOUHOU MEGANE]",2006,"TOUHOU MEGANE","東方Project","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.25-E-0139" -5,101585,"【SR】チルノ[TOUHOU MEGANE]",2007,"TOUHOU MEGANE","東方Project","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.25-E-0140" -5,101586,"【SSR】十六夜 咲夜[完全で瀟洒な眼鏡従者]",2002,"完全で瀟洒な眼鏡従者","東方Project","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",110120,110121,"[O.N.G.E.K.I.]1.25-E-0132" -5,101587,"【SSR】パチュリー・ノーレッジ[知識と日陰の眼鏡少女]",2006,"知識と日陰の眼鏡少女","東方Project","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",120094,120095,"[O.N.G.E.K.I.]1.25-E-0138" -5,101588,"【SSR】レミリア・スカーレット[眼鏡の紅い悪魔姉妹]",2003,"眼鏡の紅い悪魔姉妹","東方Project","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",125014,125015,"[O.N.G.E.K.I.]1.25-E-0134" -5,101343,"【SR+】高瀬 梨緒[ONGEKI Vocal Party 06]",1003,"ONGEKI Vocal Party 06","プロモーション","Aqua","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",115126,115127,"[O.N.G.E.K.I.]VP06-0001" -5,101344,"【SR+】藍原 椿[ONGEKI Vocal Party 06]",1005,"ONGEKI Vocal Party 06","プロモーション","Leaf","高校1年生",12,"53,240,263,278,290,300,0,0,0,300",125086,125087,"[O.N.G.E.K.I.]VP06-0002" -5,101345,"【SR+】桜井 春菜[ONGEKI Vocal Party 06]",1007,"ONGEKI Vocal Party 06","プロモーション","Fire","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",125088,125089,"[O.N.G.E.K.I.]VP06-0003" -6,100467,"【SSR】チルノ[凍符「パーフェクトフリーズ」]",2007,"凍符「パーフェクトフリーズ」","東方Project","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.30-E-0038" -6,101322,"【SSR】皇城 セツナ[デイドリーム・フェアリーズ]",1016,"デイドリーム・フェアリーズ","奏坂学園","Fire","高校3年生",3,"65,270,291,304,314,322,0,0,0,327",130014,130015,"[O.N.G.E.K.I.]1.30-0003" -6,101323,"【SSR】藍原 椿[デイドリーム・エンジェルズ]",1005,"デイドリーム・エンジェルズ","奏坂学園","Leaf","高校1年生",3,"70,300,313,323,330,335,0,0,0,340",130016,130017,"[O.N.G.E.K.I.]1.30-0023" -6,101325,"【SSR】藤沢 柚子[デイドリーム・エンジェルズ]",1001,"デイドリーム・エンジェルズ","奏坂学園","Leaf","高校2年生",3,"70,300,313,323,330,335,0,0,0,340",130020,130021,"[O.N.G.E.K.I.]1.30-0014" -6,101326,"【SSR】高瀬 梨緒[デイドリーム・エンジェルズ]",1003,"デイドリーム・エンジェルズ","奏坂学園","Aqua","高校2年生",3,"70,300,313,323,330,335,0,0,0,340",130022,130023,"[O.N.G.E.K.I.]1.30-0021" -6,101328,"【SSR】星咲 あかり[デイドリーム・エンジェルズ]",1000,"デイドリーム・エンジェルズ","奏坂学園","Fire","高校2年生",3,"70,300,313,323,330,335,0,0,0,340",130026,130027,"[O.N.G.E.K.I.]1.30-0013" -6,101329,"【SSR】三角 葵[デイドリーム・エンジェルズ]",1002,"デイドリーム・エンジェルズ","奏坂学園","Aqua","高校2年生",3,"70,300,313,323,330,335,0,0,0,340",130028,130029,"[O.N.G.E.K.I.]1.30-0015" -6,101330,"【SSR】結城 莉玖[デイドリーム・エンジェルズ]",1004,"デイドリーム・エンジェルズ","奏坂学園","Fire","高校1年生",3,"70,300,313,323,330,335,0,0,0,340",130030,130031,"[O.N.G.E.K.I.]1.30-0022" -6,101563,"【N】皇城 セツナ",1016,"","奏坂学園","Fire","高校3年生",0,"50,197,212,227,242,257,287,317,347,347",130092,130093,"[O.N.G.E.K.I.]1.30-0008" -6,101564,"【SSR】皇城 セツナ[ONGEKI Sound Collection 06]",1016,"ONGEKI Sound Collection 06","プロモーション","Fire","高校3年生",3,"65,262,285,300,312,322,0,0,0,327",130096,130097,"[O.N.G.E.K.I.]SC-0006" -6,101566,"【SR】皇城 セツナ[てきぱきリサーチ]",1016,"てきぱきリサーチ","奏坂学園","Fire","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",130054,130055,"[O.N.G.E.K.I.]1.30-0005" -6,101567,"【SSR】皇城 セツナ[漆黒の執行者]",1016,"漆黒の執行者","奏坂学園","Fire","高校3年生",3,"65,270,291,304,314,322,0,0,0,327",130052,130053,"[O.N.G.E.K.I.]1.30-0002" -6,101573,"【SR+】三角 葵[ここだけSmiling]",1002,"ここだけSmiling","奏坂学園","Aqua","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",130090,130091,"[O.N.G.E.K.I.]1.30-0025" -6,101574,"【SR+】柏木 咲姫[お届けハウスキーパー]",1009,"お届けハウスキーパー","奏坂学園","Aqua","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",110066,110067,"[O.N.G.E.K.I.]1.30-0024" -6,101575,"【SR+】逢坂 茜[罰ゲームはメイド服]",1011,"罰ゲームはメイド服","奏坂学園","Fire","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",125084,125085,"[O.N.G.E.K.I.]1.30-0016" -6,101576,"【SR+】日向 千夏[カミナリにご用心]",1014,"カミナリにご用心","奏坂学園","Leaf","中学2年生",12,"53,240,263,278,290,300,0,0,0,300",125084,125085,"[O.N.G.E.K.I.]1.30-0030" -6,101577,"【SR+】皇城 セツナ[薄暮バイオリニスト]",1016,"薄暮バイオリニスト","奏坂学園","Fire","高校3年生",12,"53,240,263,278,290,300,0,0,0,300",130098,130099,"[O.N.G.E.K.I.]1.30-0004" -6,101589,"【R】皇城 セツナ[シュータードレス]",1016,"シュータードレス","奏坂学園","Fire","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",130056,130057,"[O.N.G.E.K.I.]1.30-0006" -6,101592,"【SSR】星咲 あかり[憧れの先を目指して]",1000,"憧れの先を目指して","奏坂学園","Fire","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",130050,130051,"[O.N.G.E.K.I.]1.30-0001" -6,101593,"【SSR】日向 千夏[一緒に入ろ?]",1014,"一緒に入ろ?","奏坂学園","Leaf","中学2年生",3,"65,270,291,304,314,322,0,0,0,327",130060,130061,"[O.N.G.E.K.I.]1.30-0012" -6,101594,"【SSR】東雲 つむぎ[イルミネーション・ファンタジー]",1015,"イルミネーション・ファンタジー","奏坂学園","Aqua","中学2年生",3,"65,270,291,304,314,322,0,0,0,327",130066,130067,"[O.N.G.E.K.I.]1.30-0020" -6,101595,"【SSR】早乙女 彩華[bitter kiss, bitter girl]",1006,"bitter kiss, bitter girl","奏坂学園","Aqua","高校3年生",3,"65,270,291,304,314,322,0,0,0,327",130070,130071,"[O.N.G.E.K.I.]1.30-0027" -6,101601,"【SR】九條 楓[ほっこり湯上がり]",1008,"ほっこり湯上がり","奏坂学園","Leaf","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",125064,125065,"[O.N.G.E.K.I.]1.30-0010" -6,101602,"【SR】藤沢 柚子[ビッグなプレゼント希望]",1001,"ビッグなプレゼント希望","奏坂学園","Leaf","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",130094,130095,"[O.N.G.E.K.I.]1.30-0017" -6,101603,"【SR】結城 莉玖[よりどりチョコバイキング]",1004,"よりどりチョコバイキング","奏坂学園","Fire","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",120088,120089,"[O.N.G.E.K.I.]1.30-0026" -6,101609,"【SSR】三角 葵[湯けむり温泉旅情]",1002,"湯けむり温泉旅情","奏坂学園","Aqua","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",125046,125047,"[O.N.G.E.K.I.]1.30-0009" -6,101610,"【SSR】逢坂 茜[湯けむり温泉旅情]",1011,"湯けむり温泉旅情","奏坂学園","Fire","高校3年生",3,"65,270,291,304,314,322,0,0,0,327",130058,130059,"[O.N.G.E.K.I.]1.30-0011" -6,101611,"【SSR】柏木 咲姫[Christmas with You]",1009,"Christmas with You","奏坂学園","Aqua","高校3年生",3,"65,270,291,304,314,322,0,0,0,327",130062,130063,"[O.N.G.E.K.I.]1.30-0019" -6,101612,"【SSR】桜井 春菜[Christmas with You]",1007,"Christmas with You","奏坂学園","Fire","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",130064,130065,"[O.N.G.E.K.I.]1.30-0018" -6,101613,"【SSR】珠洲島 有栖[満点ショコラティエ]",1012,"満点ショコラティエ","奏坂学園","Aqua","高校1年生",3,"65,270,291,304,314,322,0,0,0,327",130070,130071,"[O.N.G.E.K.I.]1.30-0028" -6,101614,"【SSR】皇城 セツナ[満点ショコラティエ]",1016,"満点ショコラティエ","奏坂学園","Fire","高校3年生",3,"65,270,291,304,314,322,0,0,0,327",130068,130069,"[O.N.G.E.K.I.]1.30-0029" -6,101619,"【R】皇城 セツナ[私服]",1016,"私服","奏坂学園","Fire","高校3年生",1,"50,197,212,227,242,257,0,0,0,257",130056,130057,"[O.N.G.E.K.I.]1.30-0007" -6,101628,"【R】ティアラ[LiGHTs]",46049,"LiGHTs","ラピスリライツ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.30-E-0046" -6,101629,"【R】ロゼッタ[LiGHTs]",46050,"LiGHTs","ラピスリライツ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.30-E-0048" -6,101630,"【R】ラヴィ[LiGHTs]",46051,"LiGHTs","ラピスリライツ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.30-E-0050" -6,101631,"【R】アシュレイ[LiGHTs]",46052,"LiGHTs","ラピスリライツ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.30-E-0052" -6,101632,"【R】リネット[LiGHTs]",46053,"LiGHTs","ラピスリライツ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.30-E-0054" -6,101633,"【R】エミリア[IV KLORE]",46054,"IV KLORE","ラピスリライツ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.30-E-0056" -6,101634,"【R】あるふぁ[IV KLORE]",46055,"IV KLORE","ラピスリライツ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100003,100044,"[O.N.G.E.K.I.]1.30-E-0058" -6,101635,"【R】サルサ[IV KLORE]",46056,"IV KLORE","ラピスリライツ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.30-E-0060" -6,101636,"【R】ガーネット[IV KLORE]",46057,"IV KLORE","ラピスリライツ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.30-E-0062" -6,101637,"【R】ナデシコ[この花は乙女]",46058,"この花は乙女","ラピスリライツ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.30-E-0064" -6,101638,"【R】ツバキ[この花は乙女]",46059,"この花は乙女","ラピスリライツ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.30-E-0066" -6,101639,"【R】カエデ[この花は乙女]",46060,"この花は乙女","ラピスリライツ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.30-E-0068" -6,101640,"【R】ラトゥーラ[シュガーポケッツ]",46061,"シュガーポケッツ","ラピスリライツ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.30-E-0070" -6,101641,"【R】シャンペ[シュガーポケッツ]",46062,"シュガーポケッツ","ラピスリライツ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.30-E-0072" -6,101642,"【R】メアリーベリー[シュガーポケッツ]",46063,"シュガーポケッツ","ラピスリライツ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.30-E-0074" -6,101643,"【R】アンジェリカ[Sadistic★Candy]",46064,"Sadistic★Candy","ラピスリライツ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110000,110001,"[O.N.G.E.K.I.]1.30-E-0076" -6,101644,"【R】ルキフェル[Sadistic★Candy]",46065,"Sadistic★Candy","ラピスリライツ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",115148,115149,"[O.N.G.E.K.I.]1.30-E-0078" -6,101645,"【R】ユエ[supernova]",46066,"supernova","ラピスリライツ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.30-E-0080" -6,101646,"【R】ミルフィーユ[supernova]",46067,"supernova","ラピスリライツ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.30-E-0082" -6,101647,"【R】フィオナ[supernova]",46068,"supernova","ラピスリライツ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.30-E-0084" -6,101648,"【SR】ティアラ[私たちが、新しい「光」になる。]",46049,"私たちが、新しい「光」になる。","ラピスリライツ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.30-E-0045" -6,101649,"【SR】ロゼッタ[私たちが、新しい「光」になる。]",46050,"私たちが、新しい「光」になる。","ラピスリライツ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.30-E-0047" -6,101650,"【SR】ラヴィ[私たちが、新しい「光」になる。]",46051,"私たちが、新しい「光」になる。","ラピスリライツ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.30-E-0049" -6,101651,"【SR】アシュレイ[私たちが、新しい「光」になる。]",46052,"私たちが、新しい「光」になる。","ラピスリライツ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.30-E-0051" -6,101652,"【SR】リネット[私たちが、新しい「光」になる。]",46053,"私たちが、新しい「光」になる。","ラピスリライツ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100138,100139,"[O.N.G.E.K.I.]1.30-E-0053" -6,101653,"【SR】エミリア[奏でましょう、艶やかなる 都市伝説。]",46054,"奏でましょう、艶やかなる 都市伝説。","ラピスリライツ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.30-E-0055" -6,101654,"【SR】あるふぁ[奏でましょう、艶やかなる 都市伝説。]",46055,"奏でましょう、艶やかなる 都市伝説。","ラピスリライツ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",105008,105009,"[O.N.G.E.K.I.]1.30-E-0057" -6,101655,"【SR】サルサ[奏でましょう、艶やかなる 都市伝説。]",46056,"奏でましょう、艶やかなる 都市伝説。","ラピスリライツ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.30-E-0059" -6,101656,"【SR】ガーネット[奏でましょう、艶やかなる 都市伝説。]",46057,"奏でましょう、艶やかなる 都市伝説。","ラピスリライツ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110086,110087,"[O.N.G.E.K.I.]1.30-E-0061" -6,101657,"【SR】ナデシコ[さあ、「ろっく」に参りましょう。]",46058,"さあ、「ろっく」に参りましょう。","ラピスリライツ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110114,110115,"[O.N.G.E.K.I.]1.30-E-0063" -6,101658,"【SR】ツバキ[さあ、「ろっく」に参りましょう。]",46059,"さあ、「ろっく」に参りましょう。","ラピスリライツ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.30-E-0065" -6,101659,"【SR】カエデ[さあ、「ろっく」に参りましょう。]",46060,"さあ、「ろっく」に参りましょう。","ラピスリライツ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.30-E-0067" -6,101660,"【SR】ラトゥーラ[みんなのハートを盗んであげる!]",46061,"みんなのハートを盗んであげる!","ラピスリライツ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.30-E-0069" -6,101661,"【SR】シャンペ[みんなのハートを盗んであげる!]",46062,"みんなのハートを盗んであげる!","ラピスリライツ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.30-E-0071" -6,101662,"【SR】メアリーベリー[みんなのハートを盗んであげる!]",46063,"みんなのハートを盗んであげる!","ラピスリライツ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115028,115029,"[O.N.G.E.K.I.]1.30-E-0073" -6,101663,"【SR】アンジェリカ[きゃっるるーん♪ ハロハロー★]",46064,"きゃっるるーん♪ ハロハロー★","ラピスリライツ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100016,100057,"[O.N.G.E.K.I.]1.30-E-0075" -6,101664,"【SR】ルキフェル[きゃっるるーん♪ ハロハロー★]",46065,"きゃっるるーん♪ ハロハロー★","ラピスリライツ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",105024,105025,"[O.N.G.E.K.I.]1.30-E-0077" -6,101665,"【SR】ユエ[誰よりも、誰よりも「強く」輝きたい。]",46066,"誰よりも、誰よりも「強く」輝きたい。","ラピスリライツ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.30-E-0079" -6,101666,"【SR】ミルフィーユ[誰よりも、誰よりも「強く」輝きたい。]",46067,"誰よりも、誰よりも「強く」輝きたい。","ラピスリライツ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110098,110099,"[O.N.G.E.K.I.]1.30-E-0081" -6,101667,"【SR】フィオナ[誰よりも、誰よりも「強く」輝きたい。]",46068,"誰よりも、誰よりも「強く」輝きたい。","ラピスリライツ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",120074,120075,"[O.N.G.E.K.I.]1.30-E-0083" -6,101668,"【SSR】ティアラ[この世界のアイドルは魔法が使える]",46049,"この世界のアイドルは魔法が使える","ラピスリライツ","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",100138,100139,"[O.N.G.E.K.I.]1.30-E-0044" -6,101669,"【SR】赤座 あかり[わぁい、ちなつちゃんと一緒♪]",30003,"わぁい、ちなつちゃんと一緒♪","ゆるゆり♪♪","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.30-E-0002" -6,101670,"【SR】歳納 京子[てやんでぃ]",30001,"てやんでぃ","ゆるゆり♪♪","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",120018,120019,"[O.N.G.E.K.I.]1.30-E-0004" -6,101671,"【SR】船見 結衣[ラムレーズン食べていいよ]",30001,"ラムレーズン食べていいよ","ゆるゆり♪♪","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",125006,125007,"[O.N.G.E.K.I.]1.30-E-0005" -6,101672,"【SR】吉川 ちなつ[先輩大好きです~~っ]",30003,"先輩大好きです~~っ","ゆるゆり♪♪","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100031,100072,"[O.N.G.E.K.I.]1.30-E-0006" -6,101673,"【SR】杉浦 綾乃[余裕ありま温泉よ]",30002,"余裕ありま温泉よ","ゆるゆり♪♪","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",130082,130083,"[O.N.G.E.K.I.]1.30-E-0008" -6,101674,"【SR】池田 千歳[綾乃ちゃん告白せえへんの?]",30002,"綾乃ちゃん告白せえへんの?","ゆるゆり♪♪","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",130082,130083,"[O.N.G.E.K.I.]1.30-E-0009" -6,101675,"【SR】大室 櫻子[私だって負けないわよ!]",30002,"私だって負けないわよ!","ゆるゆり♪♪","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115028,115029,"[O.N.G.E.K.I.]1.30-E-0010" -6,101676,"【SR】古谷 向日葵[負けませんわよ!]",30002,"負けませんわよ!","ゆるゆり♪♪","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",105042,105043,"[O.N.G.E.K.I.]1.30-E-0011" -6,101677,"【SSR】歳納 京子[七森中 ごらく部!]",30001,"七森中 ごらく部!","ゆるゆり♪♪","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",105000,105001,"[O.N.G.E.K.I.]1.30-E-0003" -6,101678,"【SSR】杉浦 綾乃[七森中 生徒会!]",30002,"七森中 生徒会!","ゆるゆり♪♪","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",100031,100072,"[O.N.G.E.K.I.]1.30-E-0007" -6,101679,"【R】赤座 あかり[対戦相手専用]",30003,"対戦相手専用","ゆるゆり♪♪","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" -6,101680,"【SR】天海 春香[765プロダクション]",46001,"765プロダクション","「アイドルマスター」シリーズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.30-E-0013" -6,101681,"【SR】如月 千早[765プロダクション]",46002,"765プロダクション","「アイドルマスター」シリーズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",130000,130001,"[O.N.G.E.K.I.]1.30-E-0014" -6,101682,"【SR】星井 美希[765プロダクション]",46003,"765プロダクション","「アイドルマスター」シリーズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.30-E-0015" -6,101683,"【SR】島村 卯月[346プロダクション]",46004,"346プロダクション","「アイドルマスター」シリーズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.30-E-0017" -6,101684,"【SR】渋谷 凛[346プロダクション]",46005,"346プロダクション","「アイドルマスター」シリーズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.30-E-0018" -6,101685,"【SR】本田 未央[346プロダクション]",46006,"346プロダクション","「アイドルマスター」シリーズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",130000,130001,"[O.N.G.E.K.I.]1.30-E-0019" -6,101686,"【SR】春日 未来[765プロダクション]",46007,"765プロダクション","「アイドルマスター」シリーズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.30-E-0021" -6,101687,"【SR】最上 静香[765プロダクション]",46008,"765プロダクション","「アイドルマスター」シリーズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",125016,125017,"[O.N.G.E.K.I.]1.30-E-0022" -6,101688,"【SR】伊吹 翼[765プロダクション]",46009,"765プロダクション","「アイドルマスター」シリーズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.30-E-0023" -6,101689,"【SR】天道 輝[315プロダクション]",46010,"315プロダクション","「アイドルマスター」シリーズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100016,100057,"[O.N.G.E.K.I.]1.30-E-0025" -6,101690,"【SR】桜庭 薫[315プロダクション]",46011,"315プロダクション","「アイドルマスター」シリーズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100018,100059,"[O.N.G.E.K.I.]1.30-E-0026" -6,101691,"【SR】柏木 翼[315プロダクション]",46012,"315プロダクション","「アイドルマスター」シリーズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100017,100058,"[O.N.G.E.K.I.]1.30-E-0027" -6,101692,"【SR】櫻木 真乃[283プロダクション]",46013,"283プロダクション","「アイドルマスター」シリーズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",125016,125017,"[O.N.G.E.K.I.]1.30-E-0029" -6,101693,"【SR】風野 灯織[283プロダクション]",46014,"283プロダクション","「アイドルマスター」シリーズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.30-E-0030" -6,101694,"【SR】八宮 めぐる[283プロダクション]",46015,"283プロダクション","「アイドルマスター」シリーズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.30-E-0031" -6,101695,"【SSR】天海 春香[なんどでも笑おう]",46001,"なんどでも笑おう","「アイドルマスター」シリーズ","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",130002,130003,"[O.N.G.E.K.I.]1.30-E-0012" -6,101696,"【SSR】島村 卯月[なんどでも笑おう]",46004,"なんどでも笑おう","「アイドルマスター」シリーズ","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",130002,130003,"[O.N.G.E.K.I.]1.30-E-0016" -6,101697,"【SSR】春日 未来[なんどでも笑おう]",46007,"なんどでも笑おう","「アイドルマスター」シリーズ","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",130002,130003,"[O.N.G.E.K.I.]1.30-E-0020" -6,101698,"【SSR】天道 輝[なんどでも笑おう]",46010,"なんどでも笑おう","「アイドルマスター」シリーズ","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",130002,130003,"[O.N.G.E.K.I.]1.30-E-0024" -6,101699,"【SSR】櫻木 真乃[なんどでも笑おう]",46013,"なんどでも笑おう","「アイドルマスター」シリーズ","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",130002,130003,"[O.N.G.E.K.I.]1.30-E-0028" -6,101700,"【SR】芹沢 あさひ[ストレイライト]",46016,"ストレイライト","「アイドルマスター」シリーズ","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",115036,115037,"[O.N.G.E.K.I.]1.30-E-0033" -6,101701,"【SR】黛 冬優子[ストレイライト]",46017,"ストレイライト","「アイドルマスター」シリーズ","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115036,115037,"[O.N.G.E.K.I.]1.30-E-0035" -6,101702,"【SR】和泉 愛依[ストレイライト]",46018,"ストレイライト","「アイドルマスター」シリーズ","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",115036,115037,"[O.N.G.E.K.I.]1.30-E-0037" -6,101703,"【SSR】芹沢 あさひ[隠匿シンギュラリティ]",46016,"隠匿シンギュラリティ","「アイドルマスター」シリーズ","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.30-E-0032" -6,101704,"【SSR】黛 冬優子[隠匿シンギュラリティ]",46017,"隠匿シンギュラリティ","「アイドルマスター」シリーズ","Leaf","-",3,"60,265,286,299,309,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]1.30-E-0034" -6,101705,"【SSR】和泉 愛依[隠匿シンギュラリティ]",46018,"隠匿シンギュラリティ","「アイドルマスター」シリーズ","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",130004,130005,"[O.N.G.E.K.I.]1.30-E-0036" -6,101706,"【R】天海 春香[モデル表示用]",46001,"モデル表示用","「アイドルマスター」シリーズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" -6,101707,"【R】如月 千早[モデル表示用]",46002,"モデル表示用","「アイドルマスター」シリーズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" -6,101708,"【R】星井 美希[モデル表示用]",46003,"モデル表示用","「アイドルマスター」シリーズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" -6,101709,"【R】島村 卯月[モデル表示用]",46004,"モデル表示用","「アイドルマスター」シリーズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" -6,101710,"【R】渋谷 凛[モデル表示用]",46005,"モデル表示用","「アイドルマスター」シリーズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" -6,101711,"【R】本田 未央[モデル表示用]",46006,"モデル表示用","「アイドルマスター」シリーズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" -6,101712,"【R】春日 未来[モデル表示用]",46007,"モデル表示用","「アイドルマスター」シリーズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" -6,101713,"【R】最上 静香[モデル表示用]",46008,"モデル表示用","「アイドルマスター」シリーズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" -6,101714,"【R】伊吹 翼[モデル表示用]",46009,"モデル表示用","「アイドルマスター」シリーズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" -6,101715,"【R】天道 輝[モデル表示用]",46010,"モデル表示用","「アイドルマスター」シリーズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" -6,101716,"【R】桜庭 薫[モデル表示用]",46011,"モデル表示用","「アイドルマスター」シリーズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" -6,101717,"【R】柏木 翼[モデル表示用]",46012,"モデル表示用","「アイドルマスター」シリーズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" -6,101718,"【R】櫻木 真乃[モデル表示用]",46013,"モデル表示用","「アイドルマスター」シリーズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" -6,101719,"【R】風野 灯織[モデル表示用]",46014,"モデル表示用","「アイドルマスター」シリーズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" -6,101720,"【R】八宮 めぐる[モデル表示用]",46015,"モデル表示用","「アイドルマスター」シリーズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" -6,101721,"【R】芹沢 あさひ[モデル表示用]",46016,"モデル表示用","「アイドルマスター」シリーズ","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" -6,101722,"【R】黛 冬優子[モデル表示用]",46017,"モデル表示用","「アイドルマスター」シリーズ","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" -6,101723,"【R】和泉 愛依[モデル表示用]",46018,"モデル表示用","「アイドルマスター」シリーズ","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" -6,101744,"【R】リンカ[ナビゲーター]",46040,"ナビゲーター","グルーヴコースター","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.30-E-0120" -6,101745,"【R】ユメ[ナビゲーター]",46041,"ナビゲーター","グルーヴコースター","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.30-E-0123" -6,101746,"【R】セイネ[ナビゲーター]",46042,"ナビゲーター","グルーヴコースター","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.30-E-0126" -6,101747,"【SR】リンカ[ヘヴンリーフェスティバル]",46040,"ヘヴンリーフェスティバル","グルーヴコースター","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100017,100058,"[O.N.G.E.K.I.]1.30-E-0118" -6,101748,"【SR】ユメ[サンタ]",46041,"サンタ","グルーヴコースター","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100016,100057,"[O.N.G.E.K.I.]1.30-E-0121" -6,101749,"【SR】セイネ[サマー]",46042,"サマー","グルーヴコースター","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100018,100059,"[O.N.G.E.K.I.]1.30-E-0124" -6,101750,"【SR】リンカ[インフィニティハイウェイ]",46040,"インフィニティハイウェイ","グルーヴコースター","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.30-E-0119" -6,101751,"【SR】ユメ[ドリームパーティー]",46041,"ドリームパーティー","グルーヴコースター","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",130012,130013,"[O.N.G.E.K.I.]1.30-E-0122" -6,101752,"【SR】セイネ[スターライトロード]",46042,"スターライトロード","グルーヴコースター","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.30-E-0125" -6,101753,"【SSR】リンカ[ダイヤモンドギャラクシー]",46040,"ダイヤモンドギャラクシー","グルーヴコースター","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.30-E-0117" -6,101754,"【SSR】式宮 碧音[オンゲキ?]",32001,"オンゲキ?","Re:ステージ!プリズムステップ","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.30-E-0146" -6,101755,"【SSR】岬 珊瑚[オンゲキ?]",32001,"オンゲキ?","Re:ステージ!プリズムステップ","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",100033,100074,"[O.N.G.E.K.I.]1.30-E-0147" -6,101756,"【SSR】式宮 舞菜[Happy Birthday!!]",32001,"Happy Birthday!!","Re:ステージ!プリズムステップ","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.30-E-0145" -6,101757,"【R】為栗 メロ[でんこ]",46043,"でんこ","駅メモ!&アワメモ!","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.30-E-0129" -6,101758,"【R】新阪 ルナ[でんこ]",46044,"でんこ","駅メモ!&アワメモ!","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.30-E-0132" -6,101759,"【R】恋浜 みろく[でんこ]",46045,"でんこ","駅メモ!&アワメモ!","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.30-E-0135" -6,101760,"【R】天下 さや[でんこ]",46046,"でんこ","駅メモ!&アワメモ!","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.30-E-0138" -6,101761,"【R】根雨 つむぎ[でんこ]",46047,"でんこ","駅メモ!&アワメモ!","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110148,110149,"[O.N.G.E.K.I.]1.30-E-0141" -6,101762,"【R】観音町 ひめ[でんこ]",46048,"でんこ","駅メモ!&アワメモ!","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.30-E-0144" -6,101763,"【SR】為栗 メロ[電子ウサギな]",46043,"電子ウサギな","駅メモ!&アワメモ!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.30-E-0127" -6,101764,"【SR】新阪 ルナ[ハロウィンパーティな]",46044,"ハロウィンパーティな","駅メモ!&アワメモ!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",130000,130001,"[O.N.G.E.K.I.]1.30-E-0130" -6,101765,"【SR】恋浜 みろく[帝都の]",46045,"帝都の","駅メモ!&アワメモ!","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.30-E-0133" -6,101766,"【SR】天下 さや[スクールライフな]",46046,"スクールライフな","駅メモ!&アワメモ!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.30-E-0136" -6,101767,"【SR】根雨 つむぎ[花の妖精な]",46047,"花の妖精な","駅メモ!&アワメモ!","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",125016,125017,"[O.N.G.E.K.I.]1.30-E-0139" -6,101768,"【SR】観音町 ひめ[マジカルな]",46048,"マジカルな","駅メモ!&アワメモ!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.30-E-0142" -6,101769,"【SR】為栗 メロ[ASTERISMな]",46043,"ASTERISMな","駅メモ!&アワメモ!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115088,115089,"[O.N.G.E.K.I.]1.30-E-0128" -6,101770,"【SR】新阪 ルナ[ASTERISMな]",46044,"ASTERISMな","駅メモ!&アワメモ!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100023,100064,"[O.N.G.E.K.I.]1.30-E-0131" -6,101771,"【SR】恋浜 みろく[ASTERISMな]",46045,"ASTERISMな","駅メモ!&アワメモ!","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",115088,115089,"[O.N.G.E.K.I.]1.30-E-0134" -6,101772,"【SR】天下 さや[⊿TRiEDGEな]",46046,"⊿TRiEDGEな","駅メモ!&アワメモ!","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",130088,130089,"[O.N.G.E.K.I.]1.30-E-0137" -6,101773,"【SR】根雨 つむぎ[⊿TRiEDGEな]",46047,"⊿TRiEDGEな","駅メモ!&アワメモ!","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100024,100065,"[O.N.G.E.K.I.]1.30-E-0140" -6,101774,"【SR】観音町 ひめ[⊿TRiEDGEな]",46048,"⊿TRiEDGEな","駅メモ!&アワメモ!","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",130088,130089,"[O.N.G.E.K.I.]1.30-E-0143" -6,101793,"【R】小豆沢 こはね[プロジェクトセカイ]",46028,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100001,100042,"[O.N.G.E.K.I.]1.30-E-0089" -6,101794,"【R】白石 杏[プロジェクトセカイ]",46029,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.30-E-0091" -6,101795,"【R】東雲 彰人[プロジェクトセカイ]",46030,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.30-E-0093" -6,101796,"【R】青柳 冬弥[プロジェクトセカイ]",46031,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.30-E-0095" -6,101797,"【R】天馬 司[プロジェクトセカイ]",46032,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.30-E-0099" -6,101798,"【R】鳳 えむ[プロジェクトセカイ]",46033,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.30-E-0101" -6,101799,"【R】草薙 寧々[プロジェクトセカイ]",46034,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.30-E-0103" -6,101800,"【R】神代 類[プロジェクトセカイ]",46035,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",110142,110143,"[O.N.G.E.K.I.]1.30-E-0105" -6,101801,"【R】宵崎 奏[プロジェクトセカイ]",46036,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100004,100045,"[O.N.G.E.K.I.]1.30-E-0109" -6,101802,"【R】朝比奈 まふゆ[プロジェクトセカイ]",46037,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",105004,105005,"[O.N.G.E.K.I.]1.30-E-0111" -6,101803,"【R】東雲 絵名[プロジェクトセカイ]",46038,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",110092,110093,"[O.N.G.E.K.I.]1.30-E-0113" -6,101804,"【R】暁山 瑞希[プロジェクトセカイ]",46039,"プロジェクトセカイ","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100005,100046,"[O.N.G.E.K.I.]1.30-E-0115" -6,101805,"【SR】小豆沢 こはね[Vivid BAD SQUAD]",46028,"Vivid BAD SQUAD","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.30-E-0088" -6,101806,"【SR】白石 杏[Vivid BAD SQUAD]",46029,"Vivid BAD SQUAD","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100033,100074,"[O.N.G.E.K.I.]1.30-E-0090" -6,101807,"【SR】東雲 彰人[Vivid BAD SQUAD]",46030,"Vivid BAD SQUAD","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",120074,120075,"[O.N.G.E.K.I.]1.30-E-0092" -6,101808,"【SR】青柳 冬弥[Vivid BAD SQUAD]",46031,"Vivid BAD SQUAD","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",100025,100066,"[O.N.G.E.K.I.]1.30-E-0094" -6,101809,"【SR】天馬 司[ワンダーランズ×ショウタイム]",46032,"ワンダーランズ×ショウタイム","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115088,115089,"[O.N.G.E.K.I.]1.30-E-0098" -6,101810,"【SR】鳳 えむ[ワンダーランズ×ショウタイム]",46033,"ワンダーランズ×ショウタイム","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115088,115089,"[O.N.G.E.K.I.]1.30-E-0100" -6,101811,"【SR】草薙 寧々[ワンダーランズ×ショウタイム]",46034,"ワンダーランズ×ショウタイム","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.30-E-0102" -6,101812,"【SR】神代 類[ワンダーランズ×ショウタイム]",46035,"ワンダーランズ×ショウタイム","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",110094,110095,"[O.N.G.E.K.I.]1.30-E-0104" -6,101813,"【SR】宵崎 奏[25時、ナイトコードで。]",46036,"25時、ナイトコードで。","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",130010,130011,"[O.N.G.E.K.I.]1.30-E-0108" -6,101814,"【SR】朝比奈 まふゆ[25時、ナイトコードで。]",46037,"25時、ナイトコードで。","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",130010,130011,"[O.N.G.E.K.I.]1.30-E-0110" -6,101815,"【SR】東雲 絵名[25時、ナイトコードで。]",46038,"25時、ナイトコードで。","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",100031,100072,"[O.N.G.E.K.I.]1.30-E-0112" -6,101816,"【SR】暁山 瑞希[25時、ナイトコードで。]",46039,"25時、ナイトコードで。","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",110066,110067,"[O.N.G.E.K.I.]1.30-E-0114" -6,101817,"【SR】初音ミク[Vivid BAD SQUAD]",5000,"Vivid BAD SQUAD","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",110088,110089,"[O.N.G.E.K.I.]1.30-E-0096" -6,101818,"【SR】初音ミク[ワンダーランズ×ショウタイム]",5000,"ワンダーランズ×ショウタイム","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",2,"50,222,237,252,267,282,0,0,0,282",115028,115029,"[O.N.G.E.K.I.]1.30-E-0106" -6,101819,"【SR】初音ミク[25時、ナイトコードで。]",5000,"25時、ナイトコードで。","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",2,"50,222,237,252,267,282,0,0,0,282",115038,115039,"[O.N.G.E.K.I.]1.30-E-0116" -6,101820,"【SSR】星乃 一歌[見上げる先に]",43001,"見上げる先に","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",110098,110099,"[O.N.G.E.K.I.]1.30-E-0085" -6,101821,"【SSR】花里 みのり[ライブ前夜の決意]",43006,"ライブ前夜の決意","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.30-E-0086" -6,101822,"【SSR】小豆沢 こはね[セカイで一息]",46028,"セカイで一息","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",100144,100145,"[O.N.G.E.K.I.]1.30-E-0087" -6,101823,"【SSR】天馬 司[着ぐるみからこんにちは]",46032,"着ぐるみからこんにちは","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",110070,110071,"[O.N.G.E.K.I.]1.30-E-0097" -6,101824,"【SSR】宵崎 奏[画面の向こうに…]",46036,"画面の向こうに…","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",3,"60,257,280,295,307,317,0,0,0,322",105056,105057,"[O.N.G.E.K.I.]1.30-E-0107" -6,101842,"【R】名取さな[ば~ちゃるな~す]",46019,"ば~ちゃるな~す","名取さな","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",130006,130007,"[O.N.G.E.K.I.]1.30-E-0043" -6,101843,"【SR】名取さな[おはようございナース!]",46019,"おはようございナース!","名取さな","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",120018,120019,"[O.N.G.E.K.I.]1.30-E-0041" -6,101844,"【SR】名取さな[オンゲキに進撃…ってね]",46019,"オンゲキに進撃…ってね","名取さな","Fire","-",2,"50,222,237,252,267,282,0,0,0,282",115032,115033,"[O.N.G.E.K.I.]1.30-E-0042" -6,101845,"【SSR】名取さな[さなちゃんねる王のお言葉を聞きなさ~い!]",46019,"さなちゃんねる王のお言葉を聞きなさ~い!","名取さな","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",130086,130087,"[O.N.G.E.K.I.]1.30-E-0040" -6,101846,"【SSR】名取さな[さなのばくたん。]",46019,"さなのばくたん。","名取さな","Fire","-",3,"60,257,280,295,307,317,0,0,0,322",130008,130009,"[O.N.G.E.K.I.]1.30-E-0039" -6,101847,"【R】初音ミク[対戦相手専用]",5000,"対戦相手専用","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Fire","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" -6,101848,"【R】初音ミク[対戦相手専用]",5000,"対戦相手専用","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Leaf","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" -6,101849,"【R】初音ミク[対戦相手専用]",5000,"対戦相手専用","プロジェクトセカイ カラフルステージ! feat. 初音ミク","Aqua","-",1,"50,197,212,227,242,257,0,0,0,257",100000,100041,"" -6,101850,"【SSR】赤座 あかり[あかり、主人公だよね!?]",30003,"あかり、主人公だよね!?","ゆるゆり♪♪","Leaf","-",3,"60,257,280,295,307,317,0,0,0,322",130084,130085,"[O.N.G.E.K.I.]1.30-E-0001" -7,101339,"【SSR】皇城 セツナ[デイドリーム・エンジェルズ]",1016,"デイドリーム・エンジェルズ","奏坂学園","Fire","高校3年生",3,"70,300,313,323,330,335,0,0,0,340",130048,130049,"[O.N.G.E.K.I.]1.35-0032" -7,101569,"【SR+】星咲 あかり[Perfect Shining!!]",1000,"Perfect Shining!!","奏坂学園","Fire","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",135002,135003,"[O.N.G.E.K.I.]1.35-0038" -7,101571,"【SR+】桜井 春菜[My precious holiday]",1007,"My precious holiday","奏坂学園","Fire","高校2年生",12,"53,240,263,278,290,300,0,0,0,300",135006,135007,"[O.N.G.E.K.I.]1.35-0035" -7,101572,"【SR+】井之原 小星[GAME IS LIFE]",1010,"GAME IS LIFE","奏坂学園","Leaf","高校1年生",12,"53,240,263,278,290,300,0,0,0,300",135008,135009,"[O.N.G.E.K.I.]1.35-0039" -7,101599,"【SSR】柏木 美亜[レイニー・カラーズ]",1013,"レイニー・カラーズ","奏坂学園","Fire","中学2年生",3,"65,270,291,304,314,322,0,0,0,327",120006,120007,"[O.N.G.E.K.I.]1.35-0033" -7,101600,"【SSR】結城 莉玖[宙舞うブランコマスター]",1004,"宙舞うブランコマスター","奏坂学園","Fire","高校1年生",3,"65,270,291,304,314,322,0,0,0,327",135000,135001,"[O.N.G.E.K.I.]1.35-0036" -7,101607,"【SR】東雲 つむぎ[雨降りエモーショナル]",1015,"雨降りエモーショナル","奏坂学園","Aqua","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",115000,115001,"[O.N.G.E.K.I.]1.35-0034" -7,101608,"【SR】珠洲島 有栖[噂の敏腕トレーナー]",1012,"噂の敏腕トレーナー","奏坂学園","Aqua","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",125062,125063,"[O.N.G.E.K.I.]1.35-0037" -7,101627,"【SSR】星咲 あかり[4th Anniversary]",1000,"4th Anniversary","奏坂学園","Fire","高校2年生",3,"52,237,252,267,282,297,0,0,0,302",115030,115031,"[O.N.G.E.K.I.]Special Card" -7,101851,"【SSR】星咲 あかり[Winter Memory]",1000,"Winter Memory","奏坂学園","Fire","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",135072,135073,"Winter Memory" -7,101852,"【SR】星咲 あかり[Transcend Lights]",1000,"Transcend Lights","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" -7,101853,"【SR】藤沢 柚子[Transcend Lights]",1001,"Transcend Lights","奏坂学園","Leaf","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" -7,101854,"【SR】三角 葵[Transcend Lights]",1002,"Transcend Lights","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" -7,101855,"【SR】高瀬 梨緒[Transcend Lights]",1003,"Transcend Lights","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" -7,101856,"【SR】結城 莉玖[Transcend Lights]",1004,"Transcend Lights","奏坂学園","Fire","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" -7,101857,"【SR】藍原 椿[Transcend Lights]",1005,"Transcend Lights","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" -7,101858,"【SR】早乙女 彩華[Transcend Lights]",1006,"Transcend Lights","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" -7,101859,"【SR】桜井 春菜[Transcend Lights]",1007,"Transcend Lights","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" -7,101860,"【SR】九條 楓[Transcend Lights]",1008,"Transcend Lights","奏坂学園","Leaf","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" -7,101861,"【SR】柏木 咲姫[Transcend Lights]",1009,"Transcend Lights","奏坂学園","Aqua","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" -7,101862,"【SR】井之原 小星[Transcend Lights]",1010,"Transcend Lights","奏坂学園","Leaf","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" -7,101863,"【SR】逢坂 茜[Transcend Lights]",1011,"Transcend Lights","奏坂学園","Fire","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" -7,101864,"【SR】珠洲島 有栖[Transcend Lights]",1012,"Transcend Lights","奏坂学園","Aqua","高校1年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" -7,101865,"【SR】柏木 美亜[Transcend Lights]",1013,"Transcend Lights","奏坂学園","Fire","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" -7,101866,"【SR】日向 千夏[Transcend Lights]",1014,"Transcend Lights","奏坂学園","Leaf","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" -7,101867,"【SR】東雲 つむぎ[Transcend Lights]",1015,"Transcend Lights","奏坂学園","Aqua","中学2年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" -7,101868,"【SR】皇城 セツナ[Transcend Lights]",1016,"Transcend Lights","奏坂学園","Fire","高校3年生",2,"55,227,242,257,272,287,0,0,0,287",120076,120077,"Winter Memory" -7,101869,"【SSR】星咲 あかり[Make UP Future!]",1000,"Make UP Future!","奏坂学園","Fire","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",135014,135015,"Winter Memory" -7,101870,"【SSR】藤沢 柚子[Make UP Future!]",1001,"Make UP Future!","奏坂学園","Leaf","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",135016,135017,"Winter Memory" -7,101871,"【SSR】三角 葵[Make UP Future!]",1002,"Make UP Future!","奏坂学園","Aqua","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",135018,135019,"Winter Memory" -7,101872,"【SSR】高瀬 梨緒[Make UP Future!]",1003,"Make UP Future!","奏坂学園","Aqua","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",135026,135027,"Autumn Memory" -7,101873,"【SSR】結城 莉玖[Make UP Future!]",1004,"Make UP Future!","奏坂学園","Fire","高校1年生",3,"65,270,291,304,314,322,0,0,0,327",135028,135029,"Autumn Memory" -7,101874,"【SSR】藍原 椿[Make UP Future!]",1005,"Make UP Future!","奏坂学園","Leaf","高校1年生",3,"65,270,291,304,314,322,0,0,0,327",135030,135031,"Autumn Memory" -7,101885,"【SSR】皇城 セツナ[Make UP Future!]",1016,"Make UP Future!","奏坂学園","Fire","高校3年生",3,"65,270,291,304,314,322,0,0,0,327",135046,135047,"Autumn Memory" -7,101917,"【SSR】星咲 あかり[Autumn Memory]",1000,"Autumn Memory","奏坂学園","Fire","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",100034,100075,"Autumn Memory" -7,101956,"【SSR】星咲 あかり[O.N.G.E.K.I. R.E.D.]",1000,"O.N.G.E.K.I. R.E.D.","奏坂学園","Fire","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",115116,115117,"Autumn Memory" -7,101957,"【SSR】星咲 あかり[O.N.G.E.K.I. bright]",1000,"O.N.G.E.K.I. bright","奏坂学園","Fire","高校2年生",3,"65,270,291,304,314,322,0,0,0,327",135052,135053,"Winter Memory" -7,101958,"【SR】こんじきニャン[対戦相手専用]",6,"対戦相手専用","-","Fire","-",2,"55,227,242,257,272,287,0,0,0,287",100000,100041,"[O.N.G.E.K.I.]*.**-****" -7,101959,"【SR】高瀬 梨緒[Memory対戦相手専用]",1003,"Memory対戦相手専用","奏坂学園","Aqua","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100000,100041,"[O.N.G.E.K.I.]*.**-****" -7,101960,"【SR】星咲 あかり[Memory対戦相手専用]",1000,"Memory対戦相手専用","奏坂学園","Fire","高校2年生",2,"55,227,242,257,272,287,0,0,0,287",100000,100041,"[O.N.G.E.K.I.]*.**-****" -7,101965,"【SSR】星咲 あかり[Memories of O.N.G.E.K.I.]",1000,"Memories of O.N.G.E.K.I.","奏坂学園","Fire","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",100036,100077,"[O.N.G.E.K.I.]Special Card" -7,101966,"【SSR】星咲 あかり[ONGEKI Sound Collection 07]",1000,"ONGEKI Sound Collection 07","プロモーション","Fire","高校2年生",3,"60,257,280,295,307,317,0,0,0,322",115134,115135,"[O.N.G.E.K.I.]SC-007" diff --git a/titles/cm/cm_data/MU3/static_gacha_cards.csv b/titles/cm/cm_data/MU3/static_gacha_cards.csv index 303e71a..9588c6f 100644 --- a/titles/cm/cm_data/MU3/static_gacha_cards.csv +++ b/titles/cm/cm_data/MU3/static_gacha_cards.csv @@ -334,3 +334,168 @@ 1135,101602,3,1,0,0 1135,101603,3,1,0,0 1135,101619,2,1,0,0 +1156,101604,3,1,0,0 +1156,101605,3,1,0,0 +1156,101607,3,1,0,0 +1156,101608,3,1,0,0 +1156,101596,4,1,0,0 +1156,101597,4,1,0,0 +1156,101599,4,1,0,0 +1156,101600,4,1,0,0 +1149,100003,2,1,0,0 +1149,100004,2,1,0,0 +1149,100012,2,1,0,0 +1149,100013,2,1,0,0 +1149,100021,2,1,0,0 +1149,100022,2,1,0,0 +1149,100173,2,1,0,0 +1149,100174,2,1,0,0 +1149,100175,2,1,0,0 +1149,100339,2,1,0,0 +1149,100340,2,1,0,0 +1149,100341,2,1,0,0 +1149,100223,2,1,0,0 +1149,100224,2,1,0,0 +1149,100225,2,1,0,0 +1149,100692,2,1,0,0 +1149,100693,2,1,0,0 +1149,100694,2,1,0,0 +1149,101020,2,1,0,0 +1149,101025,2,1,0,0 +1149,100418,3,1,0,0 +1149,101005,3,1,0,0 +1149,100785,3,1,0,0 +1149,100786,3,1,0,0 +1149,101602,3,1,0,0 +1149,101604,3,1,0,0 +1149,100760,4,1,0,0 +1149,100780,4,1,0,0 +1149,100987,4,1,0,0 +1149,101295,4,1,0,0 +1149,101296,4,1,0,0 +1149,101592,4,1,0,0 +1163,100008,4,1,0,1 +1163,100017,4,1,0,1 +1163,100026,4,1,0,1 +1163,100034,4,1,0,1 +1163,100041,4,1,0,1 +1163,100048,4,1,0,1 +1163,100054,4,1,0,1 +1163,100060,4,1,0,1 +1163,100066,4,1,0,1 +1163,100078,4,1,0,1 +1163,100072,4,1,0,1 +1163,100084,4,1,0,1 +1163,100090,4,1,0,1 +1163,100282,4,1,0,1 +1163,100285,4,1,0,1 +1163,100284,4,1,0,1 +1163,100286,4,1,0,1 +1163,100280,4,1,0,1 +1163,100276,4,1,0,1 +1163,100277,4,1,0,1 +1163,100275,4,1,0,1 +1163,100278,4,1,0,1 +1163,100431,4,1,0,1 +1163,100407,4,1,0,1 +1163,100432,4,1,0,1 +1163,100433,4,1,0,1 +1163,100434,4,1,0,1 +1163,100435,4,1,0,1 +1163,100436,4,1,0,1 +1163,100437,4,1,0,1 +1163,100438,4,1,0,1 +1163,100439,4,1,0,1 +1163,100760,4,1,0,1 +1163,100761,4,1,0,1 +1163,100779,4,1,0,1 +1163,100767,4,1,0,1 +1163,100780,4,1,0,1 +1163,100784,4,1,0,1 +1163,100768,4,1,0,1 +1163,100725,4,1,0,1 +1163,100726,4,1,0,1 +1163,100984,4,1,0,1 +1163,100985,4,1,0,1 +1163,100987,4,1,0,1 +1163,100988,4,1,0,1 +1163,100986,4,1,0,1 +1163,100989,4,1,0,1 +1163,100982,4,1,0,1 +1163,100983,4,1,0,1 +1163,100787,4,1,0,1 +1163,101293,4,1,0,1 +1163,101294,4,1,0,1 +1163,101295,4,1,0,1 +1163,101296,4,1,0,1 +1163,101297,4,1,0,1 +1163,101320,4,1,0,1 +1163,101567,4,1,0,1 +1164,100008,4,1,0,1 +1164,100017,4,1,0,1 +1164,100026,4,1,0,1 +1164,100034,4,1,0,1 +1164,100041,4,1,0,1 +1164,100048,4,1,0,1 +1164,100054,4,1,0,1 +1164,100060,4,1,0,1 +1164,100066,4,1,0,1 +1164,100078,4,1,0,1 +1164,100072,4,1,0,1 +1164,100084,4,1,0,1 +1164,100090,4,1,0,1 +1164,100282,4,1,0,1 +1164,100285,4,1,0,1 +1164,100284,4,1,0,1 +1164,100286,4,1,0,1 +1164,100280,4,1,0,1 +1164,100276,4,1,0,1 +1164,100277,4,1,0,1 +1164,100275,4,1,0,1 +1164,100278,4,1,0,1 +1164,100431,4,1,0,1 +1164,100407,4,1,0,1 +1164,100432,4,1,0,1 +1164,100433,4,1,0,1 +1164,100434,4,1,0,1 +1164,100435,4,1,0,1 +1164,100436,4,1,0,1 +1164,100437,4,1,0,1 +1164,100438,4,1,0,1 +1164,100439,4,1,0,1 +1164,100760,4,1,0,1 +1164,100761,4,1,0,1 +1164,100779,4,1,0,1 +1164,100767,4,1,0,1 +1164,100780,4,1,0,1 +1164,100784,4,1,0,1 +1164,100768,4,1,0,1 +1164,100725,4,1,0,1 +1164,100726,4,1,0,1 +1164,100984,4,1,0,1 +1164,100985,4,1,0,1 +1164,100987,4,1,0,1 +1164,100988,4,1,0,1 +1164,100986,4,1,0,1 +1164,100989,4,1,0,1 +1164,100982,4,1,0,1 +1164,100983,4,1,0,1 +1164,100787,4,1,0,1 +1164,101293,4,1,0,1 +1164,101294,4,1,0,1 +1164,101295,4,1,0,1 +1164,101296,4,1,0,1 +1164,101297,4,1,0,1 +1164,101320,4,1,0,1 +1164,101567,4,1,0,1 +1164,101592,4,1,0,1 +1164,101593,4,1,0,1 +1164,101594,4,1,0,1 +1164,101595,4,1,0,1 +1164,101598,4,1,0,1 +1164,101596,4,1,0,1 +1164,101597,4,1,0,1 +1164,101599,4,1,0,1 +1164,101600,4,1,0,1 +1141,101600,4,1,0,1 +1141,101608,3,1,0,1 diff --git a/titles/cm/cm_data/MU3/static_gachas.csv b/titles/cm/cm_data/MU3/static_gachas.csv index 2061574..5554cdb 100644 --- a/titles/cm/cm_data/MU3/static_gachas.csv +++ b/titles/cm/cm_data/MU3/static_gachas.csv @@ -1,104 +1,69 @@ -"version","gachaId","gachaName","type","kind","isCeiling","maxSelectPoint","ceilingCnt","changeRateCnt1","changeRateCnt2" -6,1011,"無料ガチャ",0,3,0,0,10,0,0 -6,1012,"無料ガチャ(SR確定)",0,3,0,0,10,0,0 -6,1043,"レギュラーガチャ",0,0,0,0,10,0,0 +"version","gachaId","gachaName","type","kind","isCeiling","maxSelectPoint" +6,1011,"無料ガチャ",0,3,0,0 +6,1012,"無料ガチャ(SR確定)",0,3,0,0 +6,1043,"レギュラーガチャ",0,0,0,0 6,1067,"例えるなら大人のパッションフルーツ -リゾートプールガチャ",0,1,0,0,10,0,0 +リゾートプールガチャ",0,1,0,0 6,1068,"柏木 咲姫 -ピックアップガチャ",0,2,0,0,10,0,0 +ピックアップガチャ",0,2,0,0 6,1069,"井之原 小星 -ピックアップガチャ",0,2,0,0,10,0,0 +ピックアップガチャ",0,2,0,0 6,1070,"目指すは優勝! -炎の体育祭リミテッドガチャ",0,1,1,110,10,0,0 +炎の体育祭リミテッドガチャ",0,1,1,110 6,1071,"星咲 あかり -ピックアップガチャ",0,2,0,0,10,0,0 +ピックアップガチャ",0,2,0,0 6,1072,"藤沢 柚子 -ピックアップガチャ",0,2,0,0,10,0,0 +ピックアップガチャ",0,2,0,0 6,1073,"三角 葵 -ピックアップガチャ",0,2,0,0,10,0,0 +ピックアップガチャ",0,2,0,0 6,1074,"おくれてきた -Halloweenガチャ",0,1,0,0,10,0,0 +Halloweenガチャ",0,1,0,0 6,1075,"早乙女 彩華 -ピックアップガチャ",0,2,0,0,10,0,0 +ピックアップガチャ",0,2,0,0 6,1076,"桜井 春菜 -ピックアップガチャ",0,2,0,0,10,0,0 +ピックアップガチャ",0,2,0,0 6,1077,"ふわふわすぺーす -お仕事体験リミテッドガチャ",0,1,1,110,10,0,0 +お仕事体験リミテッドガチャ",0,1,1,110 6,1078,"高瀬 梨緒 -ピックアップガチャ",0,2,0,0,10,0,0 +ピックアップガチャ",0,2,0,0 6,1079,"結城 莉玖 -ピックアップガチャ",0,2,0,0,10,0,0 +ピックアップガチャ",0,2,0,0 6,1080,"藍原 椿 -ピックアップガチャ",0,2,0,0,10,0,0 +ピックアップガチャ",0,2,0,0 6,1081,"今夜はおうちでパーティ☆ -メリクリガチャ",0,1,0,0,10,0,0 +メリクリガチャ",0,1,0,0 6,1082,"日向 千夏 -ピックアップガチャ",0,2,0,0,10,0,0 +ピックアップガチャ",0,2,0,0 6,1083,"柏木 美亜 -ピックアップガチャ",0,2,0,0,10,0,0 +ピックアップガチャ",0,2,0,0 6,1084,"東雲 つむぎ -ピックアップガチャ",0,2,0,0,10,0,0 +ピックアップガチャ",0,2,0,0 6,1085,"謹賀新年 -福袋ガチャ",0,0,1,33,10,0,0 +福袋ガチャ",0,0,1,33 6,1086,"逢坂 茜 -ピックアップガチャ",0,2,0,0,10,0,0 +ピックアップガチャ",0,2,0,0 6,1087,"珠洲島 有栖 -ピックアップガチャ",0,2,0,0,10,0,0 +ピックアップガチャ",0,2,0,0 6,1088,"九條 楓 -ピックアップガチャ",0,2,0,0,10,0,0 +ピックアップガチャ",0,2,0,0 6,1089,"冬の魔法 -スーパーウルトラウィンターガチャ",0,1,0,0,10,0,0 -6,1093,"高瀬 梨緒ピックアップガチャ",0,2,0,0,10,0,0 -6,1094,"結城 莉玖ピックアップガチャ",0,2,0,0,10,0,0 -6,1095,"藍原 椿ピックアップガチャ",0,2,0,0,10,0,0 -6,1096,"早乙女 彩華ピックアップガチャ",0,2,0,0,10,0,0 -6,1097,"桜井 春菜ピックアップガチャ",0,2,0,0,10,0,0 -6,1098,"逢坂 茜ピックアップガチャ",0,2,0,0,10,0,0 -6,1099,"九條 楓ピックアップガチャ",0,2,0,0,10,0,0 -6,1100,"珠洲島 有栖ピックアップガチャ",0,2,0,0,10,0,0 -6,1101,"LEAF属性オンリーガチャ",0,2,0,0,10,0,0 -6,1102,"AQUA属性オンリーガチャ",0,2,0,0,10,0,0 -6,1103,"FIRE属性オンリーガチャ",0,2,0,0,10,0,0 -6,1104,"夜明け前の双星ガチャ",0,1,0,0,10,0,0 -6,1105,"謎の洞窟 黄金は実在した!!ガチャ",0,1,0,0,10,0,0 -6,1106,"スウィートブライダルリミテッドガチャ",0,1,0,0,10,0,0 -6,1107,"忘れられない、愛(ピュア)とロックがここにある。ガチャ",0,1,0,0,10,0,0 -6,1108,"メルティ夜ふかしガチャ",0,1,0,0,10,0,0 -6,1109,"絵本の国のシューターズガチャ",0,1,0,0,10,0,0 -6,1110,"オンゲキ R.E.D. PLUS 大感謝祭ガチャ",0,1,0,0,10,0,0 -6,1111,"オンゲキ 3rd Anniversaryガチャ",0,1,1,33,10,0,0 -6,1113,"柏木 咲姫ピックアップガチャ",0,2,0,0,10,0,0 -6,1114,"井之原 小星ピックアップガチャ",0,2,0,0,10,0,0 -6,1115,"星咲 あかりピックアップガチャ",0,2,0,0,10,0,0 -6,1116,"藤沢 柚子ピックアップガチャ",0,2,0,0,10,0,0 -6,1117,"三角 葵ピックアップガチャ",0,2,0,0,10,0,0 -6,1118,"日向 千夏ピックアップガチャ",0,2,0,0,10,0,0 -6,1119,"柏木 美亜ピックアップガチャ",0,2,0,0,10,0,0 -6,1120,"東雲 つむぎピックアップガチャ",0,2,0,0,10,0,0 -6,1121,"LEAF属性オンリーガチャ",0,2,0,0,10,0,0 -6,1122,"FIRE属性オンリーガチャ",0,2,0,0,10,0,0 -6,1123,"AQUA属性オンリーガチャ",0,2,0,0,10,0,0 -6,1125,"Let`s SHOOT!ガチャ",0,1,0,0,10,0,0 -6,1126,"ぽかぽか""温""ゲキ!いい湯だな リミテッドガチャ",0,1,0,0,10,0,0 -6,1127,"聖夜に煌めく イルミネーションガチャ",0,1,0,0,10,0,0 -6,1128,"bitter chocolate kiss ガチャ",0,1,0,0,10,0,0 -6,1134,"謹賀新年福袋ガチャ",0,1,0,0,10,0,0 -6,1135,"オンゲキ bright 大感謝祭ガチャ",0,1,0,0,10,0,0 -7,1140,"カラフルアンブレラガチャ",0,0,0,0,10,0,0 -7,1141,"It's Showtime!ワンダフルサーカスガチャ",0,0,0,0,10,0,0 -7,1147,"R.B.P. ピックアップガチャ",0,0,0,0,10,0,0 -7,1148,"皇城 セツナ ピックアップガチャ",0,0,0,0,10,0,0 -7,1149,"ASTERISM ピックアップガチャ",0,0,0,0,10,0,0 -7,1153,"Memories of O.N.G.E.K.I.打ち上げガチャ",0,0,0,0,10,0,0 -7,1156,"bright memory振り返りガチャ",0,0,0,0,10,0,0 -7,1158,"レギュラーガチャ",0,0,0,100,0,0,0 -7,1159,"オンゲキ&オンゲキ PLUS ピックアップガチャ",0,2,0,100,0,0,0 -7,1160,"SUMMER & SUMMER PLUS ピックアップガチャ",0,2,0,100,0,0,0 -7,1161,"R.E.D. & R.E.D. PLUS ピックアップガチャ",0,2,0,100,0,0,0 -7,1162,"bright & bright MEMORY ピックアップガチャ",0,2,0,100,0,0,0 -7,1163,"4周年記念!! 4rd Anniversaryセレクトガチャ",0,1,0,100,0,0,0 -7,1164,"2023謹賀新年福袋ガチャ",0,1,0,100,0,0,0 -7,1165,"5周年記念!! 5rd Anniversaryセレクトガチャ",0,1,0,100,0,0,0 -7,1166,"2024謹賀新年福袋ガチャ",0,1,0,100,0,0,0 -7,1167,"6周年記念!! 6rd Anniversaryセレクトガチャ",0,1,0,100,0,0,0 -7,1168,"2025謹賀新年福袋ガチャ",0,1,0,100,0,0,0 +スーパーウルトラウィンターガチャ",0,1,0,0 +6,1093,"高瀬 梨緒ピックアップガチャ",0,2,0,0 +6,1094,"結城 莉玖ピックアップガチャ",0,2,0,0 +6,1095,"藍原 椿ピックアップガチャ",0,2,0,0 +6,1096,"早乙女 彩華ピックアップガチャ",0,2,0,0 +6,1097,"桜井 春菜ピックアップガチャ",0,2,0,0 +6,1098,"逢坂 茜ピックアップガチャ",0,2,0,0 +6,1099,"九條 楓ピックアップガチャ",0,2,0,0 +6,1100,"珠洲島 有栖ピックアップガチャ",0,2,0,0 +6,1101,"LEAF属性オンリーガチャ",0,2,0,0 +6,1102,"AQUA属性オンリーガチャ",0,2,0,0 +6,1103,"FIRE属性オンリーガチャ",0,2,0,0 +6,1104,"夜明け前の双星ガチャ",0,1,0,0 +6,1105,"謎の洞窟 黄金は実在した!!ガチャ",0,1,0,0 +6,1106,"スウィートブライダルリミテッドガチャ",0,1,0,0 +6,1107,"忘れられない、愛(ピュア)とロックがここにある。ガチャ",0,1,0,0 +6,1108,"メルティ夜ふかしガチャ",0,1,0,0 +6,1109,"絵本の国のシューターズガチャ",0,1,0,0 +6,1110,"オンゲキ R.E.D. PLUS 大感謝祭ガチャ",0,1,0,0 +6,1111,"オンゲキ 3rd Anniversaryガチャ",0,1,1,33 \ No newline at end of file diff --git a/titles/cm/const.py b/titles/cm/const.py index f59af5d..54cc009 100644 --- a/titles/cm/const.py +++ b/titles/cm/const.py @@ -2,8 +2,9 @@ class CardMakerConstants(): GAME_CODE = "SDED" VER_CARD_MAKER = 0 + VER_CARD_MAKER_136 = 1 - VERSION_NAMES = ["Card Maker 1.34"] + VERSION_NAMES = ("Card Maker 1.34", "Card Maker 1.36") @classmethod def game_ver_to_string(cls, ver: int): diff --git a/titles/cm/index.py b/titles/cm/index.py index 6ba7382..34fafb0 100644 --- a/titles/cm/index.py +++ b/titles/cm/index.py @@ -12,6 +12,7 @@ from core.config import CoreConfig from titles.cm.config import CardMakerConfig from titles.cm.const import CardMakerConstants from titles.cm.base import CardMakerBase +from titles.cm.cm136 import CardMaker136 class CardMakerServlet(): @@ -21,14 +22,15 @@ class CardMakerServlet(): self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/cardmaker.yaml"))) self.versions = [ - CardMakerBase(core_cfg, self.game_cfg) + CardMakerBase(core_cfg, self.game_cfg), + CardMaker136(core_cfg, self.game_cfg) ] self.logger = logging.getLogger("cardmaker") log_fmt_str = "[%(asctime)s] Card Maker | %(levelname)s | %(message)s" log_fmt = logging.Formatter(log_fmt_str) fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.core_cfg.server.log_dir, "cardmaker"), encoding='utf8', - when="d", backupCount=10) + when="d", backupCount=10) fileHandler.setFormatter(log_fmt) @@ -39,7 +41,8 @@ class CardMakerServlet(): self.logger.addHandler(consoleHandler) self.logger.setLevel(self.game_cfg.server.loglevel) - coloredlogs.install(level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str) + coloredlogs.install(level=self.game_cfg.server.loglevel, + logger=self.logger, fmt=log_fmt_str) def render_POST(self, request: Request, version: int, url_path: str) -> bytes: req_raw = request.content.getvalue() @@ -51,18 +54,21 @@ class CardMakerServlet(): if version >= 130 and version < 135: # Card Maker internal_ver = CardMakerConstants.VER_CARD_MAKER + elif version >= 135 and version < 140: # Card Maker + internal_ver = CardMakerConstants.VER_CARD_MAKER_136 if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32: - # If we get a 32 character long hex string, it's a hash and we're - # doing encrypted. The likelyhood of false positives is low but + # If we get a 32 character long hex string, it's a hash and we're + # doing encrypted. The likelyhood of false positives is low but # technically not 0 self.logger.error("Encryption not supported at this time") - try: + try: unzip = zlib.decompress(req_raw) except zlib.error as e: - self.logger.error(f"Failed to decompress v{version} {endpoint} request -> {e}") + self.logger.error( + f"Failed to decompress v{version} {endpoint} request -> {e}") return zlib.compress("{\"stat\": \"0\"}".encode("utf-8")) req_data = json.loads(unzip) @@ -76,11 +82,13 @@ class CardMakerServlet(): resp = handler(req_data) except AttributeError as e: - self.logger.warning(f"Unhandled v{version} request {endpoint} - {e}") + self.logger.warning( + f"Unhandled v{version} request {endpoint} - {e}") return zlib.compress("{\"stat\": \"0\"}".encode("utf-8")) except Exception as e: - self.logger.error(f"Error handling v{version} method {endpoint} - {e}") + self.logger.error( + f"Error handling v{version} method {endpoint} - {e}") return zlib.compress("{\"stat\": \"0\"}".encode("utf-8")) if resp is None: diff --git a/titles/cm/read.py b/titles/cm/read.py index b9cb674..57d9279 100644 --- a/titles/cm/read.py +++ b/titles/cm/read.py @@ -24,16 +24,17 @@ class CardMakerReader(BaseReader): self.logger.info( f"Start importer for {CardMakerConstants.game_ver_to_string(version)}") except IndexError: - self.logger.error(f"Invalid ongeki version {version}") + self.logger.error(f"Invalid Card Maker version {version}") exit(1) def read(self) -> None: static_datas = { - "static_cards.csv": "read_ongeki_card_csv", "static_gachas.csv": "read_ongeki_gacha_csv", "static_gacha_cards.csv": "read_ongeki_gacha_card_csv" } + data_dirs = [] + if self.bin_dir is not None: for file, func in static_datas.items(): if os.path.exists(f"{self.bin_dir}/MU3/{file}"): @@ -43,36 +44,12 @@ class CardMakerReader(BaseReader): self.logger.warn(f"Couldn't find {file} file in {self.bin_dir}, skipping") if self.opt_dir is not None: - dir = self.get_data_directories(self.opt_dir) + data_dirs += self.get_data_directories(self.opt_dir) # ONGEKI (MU3) cnnot easily access the bin data(A000.pac) # so only opt_dir will work for now - self.read_gacha(f"{dir}/MU3/gacha") - self.read_card(f"{dir}/MU3/card") - - def read_ongeki_card_csv(self, file_path: str) -> None: - self.logger.info(f"Reading cards from {file_path}...") - - with open(file_path, encoding="utf-8") as f: - reader = csv.DictReader(f) - for row in reader: - self.ongeki_data.static.put_card( - row["version"], - row["cardId"], - name=row["name"], - charaId=row["charaId"], - nickName=row["nickName"] if row["nickName"] != "" else None, - school=row["school"], - attribute=row["attribute"], - gakunen=row["gakunen"], - rarity=row["rarity"], - levelParam=row["levelParam"], - skillId=row["skillId"], - choKaikaSkillId=row["choKaikaSkillId"], - cardNumber=row["cardNumber"] if row["cardNumber"] != "" else None - ) - - self.logger.info(f"Added card {row['cardId']}") + for dir in data_dirs: + self.read_ongeki_gacha(f"{dir}/MU3/gacha") def read_ongeki_gacha_csv(self, file_path: str) -> None: self.logger.info(f"Reading gachas from {file_path}...") @@ -87,10 +64,7 @@ class CardMakerReader(BaseReader): row["kind"], type=row["type"], isCeiling=True if row["isCeiling"] == "1" else False, - maxSelectPoint=row["maxSelectPoint"], - ceilingCnt=row["ceilingCnt"], - changeRateCnt1=row["changeRateCnt1"], - changeRateCnt2=row["changeRateCnt2"] + maxSelectPoint=row["maxSelectPoint"] ) self.logger.info(f"Added gacha {row['gachaId']}") @@ -112,64 +86,6 @@ class CardMakerReader(BaseReader): self.logger.info(f"Added card {row['cardId']} to gacha") - def read_ongeki_card(self, base_dir: str) -> None: - self.logger.info(f"Reading cards from {base_dir}...") - - version_ids = { - '1000': OngekiConstants.VER_ONGEKI, - '1005': OngekiConstants.VER_ONGEKI_PLUS, - '1010': OngekiConstants.VER_ONGEKI_SUMMER, - '1015': OngekiConstants.VER_ONGEKI_SUMMER_PLUS, - '1020': OngekiConstants.VER_ONGEKI_RED, - '1025': OngekiConstants.VER_ONGEKI_RED_PLUS, - '1030': OngekiConstants.VER_ONGEKI_BRIGHT, - '1035': OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY - } - - for root, dirs, files in os.walk(base_dir): - for dir in dirs: - if os.path.exists(f"{root}/{dir}/Card.xml"): - with open(f"{root}/{dir}/Card.xml", "r", encoding="utf-8") as f: - troot = ET.fromstring(f.read()) - - card_id = int(troot.find('Name').find('id').text) - name = troot.find('Name').find('str').text - chara_id = int(troot.find('CharaID').find('id').text) - nick_name = troot.find('NickName').text - school = troot.find('School').find('str').text - attribute = troot.find('Attribute').text - gakunen = troot.find('Gakunen').find('str').text - rarity = OngekiConstants.RARITY_TYPES[ - troot.find('Rarity').text].value - - level_param = [] - for lvl in troot.find('LevelParam').findall('int'): - level_param.append(lvl.text) - - skill_id = int(troot.find('SkillID').find('id').text) - cho_kai_ka_skill_id = int(troot.find('ChoKaikaSkillID').find('id').text) - - version = version_ids[ - troot.find('VersionID').find('id').text] - card_number = troot.find('CardNumberString').text - - self.ongeki_data.static.put_card( - version, - card_id, - name=name, - charaId=chara_id, - nickName=nick_name, - school=school, - attribute=attribute, - gakunen=gakunen, - rarity=rarity, - levelParam=','.join(level_param), - skillId=skill_id, - choKaikaSkillId=cho_kai_ka_skill_id, - cardNumber=card_number - ) - self.logger.info(f"Added card {card_id}") - def read_ongeki_gacha(self, base_dir: str) -> None: self.logger.info(f"Reading gachas from {base_dir}...") @@ -189,11 +105,34 @@ class CardMakerReader(BaseReader): troot = ET.fromstring(f.read()) name = troot.find('Name').find('str').text - id = int(troot.find('Name').find('id').text) + gacha_id = int(troot.find('Name').find('id').text) + + # skip already existing gachas + if self.ongeki_data.static.get_gacha( + OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY, gacha_id) is not None: + self.logger.info(f"Gacha {gacha_id} already added, skipping") + continue + + # 1140 is the first bright memory gacha + if gacha_id < 1140: + version = OngekiConstants.VER_ONGEKI_BRIGHT + else: + version = OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY gacha_kind = OngekiConstants.CM_GACHA_KINDS[ type_to_kind[troot.find('Type').text]].value + # hardcode which gachas get "Select Gacha" with 33 points + is_ceiling, max_select_point = 0, 0 + if gacha_id in {1163, 1164, 1165, 1166, 1167, 1168}: + is_ceiling = 1 + max_select_point = 33 + self.ongeki_data.static.put_gacha( - self.version, id, name, gacha_kind) - self.logger.info(f"Added gacha {id}") + version, + gacha_id, + name, + gacha_kind, + isCeiling=is_ceiling, + maxSelectPoint=max_select_point) + self.logger.info(f"Added gacha {gacha_id}") diff --git a/titles/ongeki/__init__.py b/titles/ongeki/__init__.py index 26e107c..8ca6862 100644 --- a/titles/ongeki/__init__.py +++ b/titles/ongeki/__init__.py @@ -15,4 +15,4 @@ trailing_slash = True use_default_host = False host = "" -current_schema_version = 2 \ No newline at end of file +current_schema_version = 2 diff --git a/titles/ongeki/bright.py b/titles/ongeki/bright.py index 57660b5..8f66f93 100644 --- a/titles/ongeki/bright.py +++ b/titles/ongeki/bright.py @@ -22,10 +22,8 @@ class OngekiBright(OngekiBase): return ret def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: - # first check for a bright memory profile after that check for a - # bright profile - p = (self.data.profile.get_profile_data(data["userId"], self.version+1) - or self.data.profile.get_profile_data(data["userId"], self.version)) + # check for a bright profile + p = self.data.profile.get_profile_data(data["userId"], self.version) if p is None: return {} @@ -50,6 +48,8 @@ class OngekiBright(OngekiBase): user_data["accessCode"] = cards[0]["access_code"] # hardcode Card Maker version for now + # Card Maker 1.34.00 = 1.30.01 + # Card Maker 1.36.00 = 1.35.04 user_data["compatibleCmVersion"] = "1.30.01" return {"userId": data["userId"], "userData": user_data} @@ -195,7 +195,9 @@ class OngekiBright(OngekiBase): # make sure to only show gachas for the current version # so only up to bright, 1140 is the first bright memory gacha - if tmp["gachaId"] < 1140: + if self.version == OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY: + game_gacha_list.append(tmp) + elif self.version == OngekiConstants.VER_ONGEKI_BRIGHT and tmp["gachaId"] < 1140: game_gacha_list.append(tmp) return { @@ -379,11 +381,11 @@ class OngekiBright(OngekiBase): if "userData" in upsert and len(upsert["userData"]) > 0: # check if the profile is a bright memory profile - p = self.data.profile.get_profile_data(data["userId"], self.version+1) + p = self.data.profile.get_profile_data(data["userId"], self.version) if p is not None: # save the bright memory profile self.data.profile.put_profile_data( - user_id, self.version+1, upsert["userData"][0]) + user_id, self.version, upsert["userData"][0]) else: # save the bright profile self.data.profile.put_profile_data( @@ -413,11 +415,11 @@ class OngekiBright(OngekiBase): if "userData" in upsert and len(upsert["userData"]) > 0: # check if the profile is a bright memory profile - p = self.data.profile.get_profile_data(data["userId"], self.version+1) + p = self.data.profile.get_profile_data(data["userId"], self.version) if p is not None: # save the bright memory profile self.data.profile.put_profile_data( - user_id, self.version+1, upsert["userData"][0]) + user_id, self.version, upsert["userData"][0]) else: # save the bright profile self.data.profile.put_profile_data( @@ -601,11 +603,11 @@ class OngekiBright(OngekiBase): if "userData" in upsert and len(upsert["userData"]) > 0: # check if the profile is a bright memory profile - p = self.data.profile.get_profile_data(data["userId"], self.version+1) + p = self.data.profile.get_profile_data(data["userId"], self.version) if p is not None: # save the bright memory profile self.data.profile.put_profile_data( - user_id, self.version+1, upsert["userData"][0]) + user_id, self.version, upsert["userData"][0]) else: # save the bright profile self.data.profile.put_profile_data( diff --git a/titles/ongeki/brightmemory.py b/titles/ongeki/brightmemory.py index a99f806..c3e4ef1 100644 --- a/titles/ongeki/brightmemory.py +++ b/titles/ongeki/brightmemory.py @@ -5,11 +5,12 @@ import json from core.config import CoreConfig from titles.ongeki.base import OngekiBase +from titles.ongeki.bright import OngekiBright from titles.ongeki.const import OngekiConstants from titles.ongeki.config import OngekiConfig -class OngekiBrightMemory(OngekiBase): +class OngekiBrightMemory(OngekiBright): def __init__(self, core_cfg: CoreConfig, game_cfg: OngekiConfig) -> None: super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY @@ -28,7 +29,7 @@ class OngekiBrightMemory(OngekiBase): def handle_get_user_memory_chapter_api_request(self, data: Dict) -> Dict: memories = self.data.item.get_memorychapters(data["userId"]) - if not memories: + if not memories: return {"userId": data["userId"], "length":6, "userMemoryChapterList":[ {"gaugeId":0, "isClear": False, "gaugeNum": 0, "chapterId": 70001, "jewelCount": 0, "isBossWatched": False, "isStoryWatched": False, "isDialogWatched": False, "isEndingWatched": False, "lastPlayMusicId": 0, "lastPlayMusicLevel": 0, "lastPlayMusicCategory": 0}, {"gaugeId":0, "isClear": False, "gaugeNum": 0, "chapterId": 70002, "jewelCount": 0, "isBossWatched": False, "isStoryWatched": False, "isDialogWatched": False, "isEndingWatched": False, "lastPlayMusicId": 0, "lastPlayMusicLevel": 0, "lastPlayMusicCategory": 0}, @@ -37,17 +38,17 @@ class OngekiBrightMemory(OngekiBase): {"gaugeId":0, "isClear": False, "gaugeNum": 0, "chapterId": 70005, "jewelCount": 0, "isBossWatched": False, "isStoryWatched": False, "isDialogWatched": False, "isEndingWatched": False, "lastPlayMusicId": 0, "lastPlayMusicLevel": 0, "lastPlayMusicCategory": 0}, {"gaugeId":0, "isClear": False, "gaugeNum": 0, "chapterId": 70099, "jewelCount": 0, "isBossWatched": False, "isStoryWatched": False, "isDialogWatched": False, "isEndingWatched": False, "lastPlayMusicId": 0, "lastPlayMusicLevel": 0, "lastPlayMusicCategory": 0} ]} - + memory_chp = [] for chp in memories: tmp = chp._asdict() tmp.pop("id") tmp.pop("user") memory_chp.append(tmp) - + return { - "userId": data["userId"], - "length": len(memory_chp), + "userId": data["userId"], + "length": len(memory_chp), "userMemoryChapterList": memory_chp } @@ -55,4 +56,15 @@ class OngekiBrightMemory(OngekiBase): return { "techScore": 0, "cardNum": 0 - } \ No newline at end of file + } + + def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: + # check for a bright memory profile + user_data = super().handle_cm_get_user_data_api_request(data) + + # hardcode Card Maker version for now + # Card Maker 1.34.00 = 1.30.01 + # Card Maker 1.36.00 = 1.35.04 + user_data["userData"]["compatibleCmVersion"] = "1.35.04" + + return user_data diff --git a/titles/ongeki/read.py b/titles/ongeki/read.py index 8b1be3d..397fa8c 100644 --- a/titles/ongeki/read.py +++ b/titles/ongeki/read.py @@ -11,29 +11,99 @@ from titles.ongeki.database import OngekiData from titles.ongeki.const import OngekiConstants from titles.ongeki.config import OngekiConfig + class OngekiReader(BaseReader): - def __init__(self, config: CoreConfig, version: int, bin_dir: Optional[str], opt_dir: Optional[str], extra: Optional[str]) -> None: + def __init__(self, config: CoreConfig, version: int, bin_dir: Optional[str], + opt_dir: Optional[str], extra: Optional[str]) -> None: super().__init__(config, version, bin_dir, opt_dir, extra) self.data = OngekiData(config) try: - self.logger.info(f"Start importer for {OngekiConstants.game_ver_to_string(version)}") + self.logger.info( + f"Start importer for {OngekiConstants.game_ver_to_string(version)}") except IndexError: self.logger.error(f"Invalid ongeki version {version}") exit(1) - + def read(self) -> None: data_dirs = [] if self.bin_dir is not None: data_dirs += self.get_data_directories(self.bin_dir) - + if self.opt_dir is not None: data_dirs += self.get_data_directories(self.opt_dir) - + for dir in data_dirs: self.read_events(f"{dir}/event") self.read_music(f"{dir}/music") - + self.read_card(f"{dir}/card") + + def read_card(self, base_dir: str) -> None: + self.logger.info(f"Reading cards from {base_dir}...") + + version_ids = { + '1000': OngekiConstants.VER_ONGEKI, + '1005': OngekiConstants.VER_ONGEKI_PLUS, + '1010': OngekiConstants.VER_ONGEKI_SUMMER, + '1015': OngekiConstants.VER_ONGEKI_SUMMER_PLUS, + '1020': OngekiConstants.VER_ONGEKI_RED, + '1025': OngekiConstants.VER_ONGEKI_RED_PLUS, + '1030': OngekiConstants.VER_ONGEKI_BRIGHT, + '1035': OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY + } + + for root, dirs, files in os.walk(base_dir): + for dir in dirs: + if os.path.exists(f"{root}/{dir}/Card.xml"): + with open(f"{root}/{dir}/Card.xml", "r", encoding="utf-8") as f: + troot = ET.fromstring(f.read()) + + card_id = int(troot.find('Name').find('id').text) + + # skip already existing cards + if self.data.static.get_card( + OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY, card_id) is not None: + + self.logger.info(f"Card {card_id} already added, skipping") + continue + + name = troot.find('Name').find('str').text + chara_id = int(troot.find('CharaID').find('id').text) + nick_name = troot.find('NickName').text + school = troot.find('School').find('str').text + attribute = troot.find('Attribute').text + gakunen = troot.find('Gakunen').find('str').text + rarity = OngekiConstants.RARITY_TYPES[ + troot.find('Rarity').text].value + + level_param = [] + for lvl in troot.find('LevelParam').findall('int'): + level_param.append(lvl.text) + + skill_id = int(troot.find('SkillID').find('id').text) + cho_kai_ka_skill_id = int(troot.find('ChoKaikaSkillID').find('id').text) + + version = version_ids[ + troot.find('VersionID').find('id').text] + card_number = troot.find('CardNumberString').text + + self.data.static.put_card( + version, + card_id, + name=name, + charaId=chara_id, + nickName=nick_name, + school=school, + attribute=attribute, + gakunen=gakunen, + rarity=rarity, + levelParam=','.join(level_param), + skillId=skill_id, + choKaikaSkillId=cho_kai_ka_skill_id, + cardNumber=card_number + ) + self.logger.info(f"Added card {card_id}") + def read_events(self, base_dir: str) -> None: self.logger.info(f"Reading events from {base_dir}...") @@ -45,12 +115,13 @@ class OngekiReader(BaseReader): name = troot.find('Name').find('str').text id = int(troot.find('Name').find('id').text) - event_type = OngekiConstants.EVT_TYPES[troot.find('EventType').text].value - + event_type = OngekiConstants.EVT_TYPES[troot.find( + 'EventType').text].value - 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}") - + def read_music(self, base_dir: str) -> None: self.logger.info(f"Reading music from {base_dir}...") @@ -72,9 +143,9 @@ class OngekiReader(BaseReader): title = name.find('str').text artist = troot.find('ArtistName').find('str').text genre = troot.find('Genre').find('str').text - + fumens = troot.find("FumenData") - for fumens_data in fumens.findall('FumenData'): + for fumens_data in fumens.findall('FumenData'): path = fumens_data.find('FumenFile').find('path').text if path is None or not os.path.exists(f"{root}/{dir}/{path}"): continue @@ -82,8 +153,9 @@ class OngekiReader(BaseReader): chart_id = int(path.split(".")[0].split("_")[1]) level = float( f"{fumens_data.find('FumenConstIntegerPart').text}.{fumens_data.find('FumenConstFractionalPart').text}" - ) - - self.data.static.put_chart(self.version, song_id, chart_id, title, artist, genre, level) - self.logger.info(f"Added song {song_id} chart {chart_id}") + ) + 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}") diff --git a/titles/ongeki/schema/profile.py b/titles/ongeki/schema/profile.py index d7514c6..ce07490 100644 --- a/titles/ongeki/schema/profile.py +++ b/titles/ongeki/schema/profile.py @@ -180,7 +180,7 @@ region = Table( mysql_charset='utf8mb4' ) -training_room = Table ( +training_room = Table( "ongeki_profile_training_room", metadata, Column("id", Integer, primary_key=True, nullable=False), @@ -193,7 +193,7 @@ training_room = Table ( mysql_charset='utf8mb4' ) -kop = Table ( +kop = Table( "ongeki_profile_kop", metadata, Column("id", Integer, primary_key=True, nullable=False), @@ -219,6 +219,7 @@ rival = Table( mysql_charset='utf8mb4' ) + class OngekiProfileData(BaseData): def __init__(self, cfg: CoreConfig, conn: Connection) -> None: super().__init__(cfg, conn) @@ -287,14 +288,14 @@ class OngekiProfileData(BaseData): result = self.execute(sql) if result is None: return None return result.fetchall() - + def get_kop(self, aime_id: int) -> Optional[List[Row]]: sql = select(kop).where(kop.c.user == aime_id) result = self.execute(sql) if result is None: return None return result.fetchall() - + def get_rivals(self, aime_id: int) -> Optional[List[Row]]: sql = select(rival.c.rivalUserId).where(rival.c.user == aime_id) diff --git a/titles/ongeki/schema/static.py b/titles/ongeki/schema/static.py index 2e1bc51..7c5ed68 100644 --- a/titles/ongeki/schema/static.py +++ b/titles/ongeki/schema/static.py @@ -192,7 +192,7 @@ class OngekiStaticData(BaseData): def get_gacha(self, version: int, gacha_id: int) -> Optional[Dict]: sql = gachas.select(and_( - gachas.c.version == version, + gachas.c.version <= version, gachas.c.gachaId == gacha_id )) From 6609732546f96cc6745a137ccfbebd519021c043 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Mon, 6 Mar 2023 16:06:17 +0100 Subject: [PATCH 21/63] cm: added get_allnet_info --- titles/cm/__init__.py | 8 +------- titles/cm/const.py | 2 ++ titles/cm/index.py | 22 ++++++++++++++++++++-- titles/ongeki/__init__.py | 2 +- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/titles/cm/__init__.py b/titles/cm/__init__.py index 7c03605..ae4e9f0 100644 --- a/titles/cm/__init__.py +++ b/titles/cm/__init__.py @@ -5,12 +5,6 @@ from titles.cm.read import CardMakerReader index = CardMakerServlet reader = CardMakerReader -use_default_title = True -include_protocol = True -title_secure = False game_codes = [CardMakerConstants.GAME_CODE] -trailing_slash = True -use_default_host = False -host = "" -current_schema_version = 1 \ No newline at end of file +current_schema_version = 1 diff --git a/titles/cm/const.py b/titles/cm/const.py index 54cc009..3dec4fe 100644 --- a/titles/cm/const.py +++ b/titles/cm/const.py @@ -1,6 +1,8 @@ class CardMakerConstants(): GAME_CODE = "SDED" + CONFIG_NAME = "cardmaker.yaml" + VER_CARD_MAKER = 0 VER_CARD_MAKER_136 = 1 diff --git a/titles/cm/index.py b/titles/cm/index.py index 34fafb0..b2751b0 100644 --- a/titles/cm/index.py +++ b/titles/cm/index.py @@ -1,4 +1,3 @@ -from twisted.web.http import Request import json import inflection import yaml @@ -6,6 +5,10 @@ import string import logging import coloredlogs import zlib + +from os import path +from typing import Tuple +from twisted.web.http import Request from logging.handlers import TimedRotatingFileHandler from core.config import CoreConfig @@ -19,7 +22,8 @@ class CardMakerServlet(): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: self.core_cfg = core_cfg self.game_cfg = CardMakerConfig() - self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/cardmaker.yaml"))) + if path.exists(f"{cfg_dir}/{CardMakerConstants.CONFIG_NAME}"): + self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{CardMakerConstants.CONFIG_NAME}"))) self.versions = [ CardMakerBase(core_cfg, self.game_cfg), @@ -44,6 +48,20 @@ class CardMakerServlet(): coloredlogs.install(level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str) + @classmethod + def get_allnet_info(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str, str]: + game_cfg = CardMakerConfig() + if path.exists(f"{cfg_dir}/{CardMakerConstants.CONFIG_NAME}"): + game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{CardMakerConstants.CONFIG_NAME}"))) + + if not game_cfg.server.enable: + return (False, "", "") + + if core_cfg.server.is_develop: + return (True, f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/", "") + + return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", "") + def render_POST(self, request: Request, version: int, url_path: str) -> bytes: req_raw = request.content.getvalue() url_split = url_path.split("/") diff --git a/titles/ongeki/__init__.py b/titles/ongeki/__init__.py index 8ca6862..26e107c 100644 --- a/titles/ongeki/__init__.py +++ b/titles/ongeki/__init__.py @@ -15,4 +15,4 @@ trailing_slash = True use_default_host = False host = "" -current_schema_version = 2 +current_schema_version = 2 \ No newline at end of file From fb6a026b84929e92a384957e22830e7c848f8ae3 Mon Sep 17 00:00:00 2001 From: Midorica Date: Thu, 9 Mar 2023 09:10:36 -0500 Subject: [PATCH 22/63] Adding the music list for crossbeats again --- titles/cxb/data/Export.csv | 474 +++++++++++++++++++++++++++++++++++++ 1 file changed, 474 insertions(+) create mode 100644 titles/cxb/data/Export.csv diff --git a/titles/cxb/data/Export.csv b/titles/cxb/data/Export.csv new file mode 100644 index 0000000..ee913dd --- /dev/null +++ b/titles/cxb/data/Export.csv @@ -0,0 +1,474 @@ +index,mcode,name,artist,category,easy,standard,hard,master,unlimited, +100000,tutori2,Tutorial,Tutorial,Unknown,Easy N/A,Standard N/A,Hard N/A,Master N/A,Unlimited N/A, +100000,tutori3,Tutorial,Tutorial,Unknown,Easy N/A,Standard N/A,Hard N/A,Master N/A,Unlimited N/A, +100000,tutori4,Tutorial,Tutorial,Pick-Up J-Pop (New),Easy N/A,Standard N/A,Hard N/A,Master N/A,Unlimited N/A, +100000,tutori6,Tutorial,Tutorial,Pick-Up J-Pop (New),Easy N/A,Standard N/A,Hard N/A,Master N/A,Unlimited N/A, +100000,tutori8,白鳥の湖 (Short Remix),,Original,Easy N/A,Standard 3,Hard 15,Master 35,Unlimited N/A, +100300,sateli,Satellite System ft.Diana Chiaki,GRATEC MOUR,Original,Easy 17,Standard 28,Hard 49,Master 77,Unlimited 82, +100301,nature,Human Nature,Z pinkpong,Original,Easy 5,Standard 14,Hard 24,Master 53,Unlimited 75, +100307,purple,DEEP PURPLE,NAOKI,Original,Easy 14,Standard 22,Hard 54,Master 64,Unlimited 73, +100308,hearts,Heartstrings,Nhato,Original,Easy 8,Standard 18,Hard 38,Master 68,Unlimited 77, +100310,phasea,Phase Angel,OCOT,Original,Easy 9,Standard 16,Hard 38,Master 65,Unlimited 75, +100311,planet,Planet Calling,Nyolfen,Original,Easy 10,Standard 17,Hard 36,Master 49,Unlimited 71, +100314,firefo,Firefox,Go-qualia,Original,Easy 7,Standard 13,Hard 36,Master 57,Unlimited 83, +100315,kounen,光年(konen),小野秀幸,Original,Easy 10,Standard 21,Hard 40,Master 66,Unlimited 78, +100316,essenc,Another Essence,RAM,Original,Easy 11,Standard 25,Hard 50,Master 70,Unlimited 76, +100317,summer,Summer End Anthem,Personative,Original,Easy 13,Standard 23,Hard 57,Master 79,Unlimited 89, +100319,tanosi,たのしいことだけ,Yamajet,Original,Easy 16,Standard 25,Hard 45,Master 70,Unlimited 80, +100320,picora,ピコラセテ,TORIENA,Original,Easy 8,Standard 15,Hard 38,Master 66,Unlimited 75, +100323,devils,Devil's Classic,Tatsh,Original,Easy 15,Standard 27,Hard 40,Master 80,Unlimited N/A, +100328,techno,Techno Highway,SIMON,Original,Easy 9,Standard 16,Hard 38,Master 51,Unlimited 74, +100335,glowww,GLOW,Shoichiro Hirata feat. Ellie,Original,Easy 8,Standard 17,Hard 28,Master 42,Unlimited 60, +100336,powerr,Power,Dubscribe,Original,Easy 12,Standard 19,Hard 38,Master 69,Unlimited 79, +100340,amater,Amateras,Sakuzyo,Original,Easy 13,Standard 21,Hard 48,Master 65,Unlimited 79, +100349,advers,Adverse Effect,Rin,Original,Easy 9,Standard 15,Hard 48,Master 71,Unlimited 83, +100353,venera,Venerated,Tosh,Original,Easy 8,Standard 15,Hard 43,Master 68,Unlimited 75, +100357,dazaii,堕罪,HAKKYOU-KUN feat.せつな,Original,Easy 12,Standard 21,Hard 43,Master 73,Unlimited 77, +100365,thesig,The Signs Of The Last Day,SLAKE,Original,Easy 10,Standard 21,Hard 38,Master 56,Unlimited 73, +100344,hosita,星達のメロディ,ゆいこんぬ,Original,Easy 10,Standard 16,Hard 36,Master 48,Unlimited 65, +100372,bluede,Blue Destiny Blue,NAOKI feat. Florence McNair,Original,Easy 12,Standard 22,Hard 41,Master 58,Unlimited 70, +100373,emerao,EMERALD♡KISS ~Original Side~,jun with Aimee,Original,Easy 19,Standard 30,Hard 53,Master 85,Unlimited N/A, +100129,megaro,MEGALOMAN[i]A,TITANZ,Original,Easy 0,Standard 55,Hard 80,Master 93,Unlimited 98, +100330,angeli,angelik-vice,void,Original,Easy 22,Standard 33,Hard 56,Master 82,Unlimited 90, +100342,moonli,月鳴 -moonlit urge-,AZURE FACTORY,Original,Easy 8,Standard 14,Hard 43,Master 61,Unlimited 73, +100369,yumemi,ユメミル船,yozuca*,Original,Easy 6,Standard 12,Hard 35,Master 59,Unlimited 69, +100348,pinkym,Pinky Magic,Junk,Original,Easy 16,Standard 24,Hard 44,Master 74,Unlimited 81, +100370,dynami2,DYNAMITE SENSATION REV.,NAOKI feat. Hyphen,Original,Easy 8,Standard 18,Hard 51,Master 78,Unlimited 80, +100306,reseed3,Reseed (Another Edit),quick master,Original,Easy 10,Standard 20,Hard 55,Master 76,Unlimited 80, +100002,toucho,Touch Of Gold,Togo Project feat. Frances Maya,Original,Easy 5,Standard 9,Hard 28,Master 44,Unlimited 65, +100003,ameoto,雨の音が虹を呼ぶ,Barbarian On The Groove feat.霜月はるか,Original,Easy 6,Standard 12,Hard 26,Master 47,Unlimited 63, +100004,kimito,キミとMUSIC,CooRie,Original,Easy 7,Standard 10,Hard 26,Master 49,Unlimited 66, +100021,giantk,Giant Killing,R-Lab,Original,Easy 11,Standard 25,Hard 53,Master 71,Unlimited 78, +100015,breakd,Break down,GARNiDELiA,Original,Easy 11,Standard 23,Hard 34,Master 57,Unlimited 74, +100028,dazzlj,DAZZLING♡SEASON (Japanese Side),jun,Original,Easy 16,Standard 35,Hard 60,Master 80,Unlimited 90, +100093,ididid,I.D.,Tatsh feat. 彩音,Original,Easy 16,Standard 29,Hard 46,Master 72,Unlimited 81, +100042,sundro,Sundrop,Yamajet,Original,Easy 14,Standard 24,Hard 47,Master 75,Unlimited 83, +100063,auflcb,some day (instrumental),NAOKI,Original,Easy 8,Standard 13,Hard 43,Master 81,Unlimited N/A, +100045,dennou,電脳少女は歌姫の夢を見るか?,デスおはぎ feat.蛮,Original,Easy 15,Standard 29,Hard 60,Master 76,Unlimited 87, +100068,hokoro,ホコロビシロガールズ,むかしばなし,Original,Easy 14,Standard 29,Hard 57,Master 71,Unlimited 81, +100005,landin,Landing on the moon,SIMON,Original,Easy 13,Standard 26,Hard 33,Master 49,Unlimited 67, +100362,tomorr,Tomorrow,桜井零士,Original,Easy 8,Standard 15,Hard 24,Master 44,Unlimited 62, +100363,daybyd,day by day,海辺,Original,Easy 6,Standard 13,Hard 26,Master 38,Unlimited 59, +100309,syoujo,生々世々,SADA,Original,Easy 8,Standard 19,Hard 35,Master 53,Unlimited 78, +100352,destru,Destrudo,D-Fener,Original,Easy 10,Standard 19,Hard 41,Master 62,Unlimited 72, +100041,gingat,Re:Milky way,イトヲカシ,Original,Easy 5,Standard 13,Hard 29,Master 46,Unlimited 61, +100066,daisak,大殺界がらくたシンパシー,まふまふ,Original,Easy 12,Standard 28,Hard 36,Master 60,Unlimited 75, +100376,paradi,Paradise Regained,LC:AZE feat.chakk,Original,Easy 10,Standard 16,Hard 28,Master 53,Unlimited 64, +100377,pigooo,PIG-O,NNNNNNNNNN,Original,Easy 13,Standard 19,Hard 34,Master 59,Unlimited 84, +100386,season,The Four Seasons -SPRING- (Remix Ver.),,Variety,Easy 8,Standard 15,Hard 28,Master 44,Unlimited 65, +100387,canonn,カノン (Remix Ver.),,Variety,Easy 7,Standard 17,Hard 28,Master 50,Unlimited 83, +100388,rhapso,Rhapsody in Blue (Remix Ver.),,Variety,Easy 6,Standard 18,Hard 34,Master 62,Unlimited 74, +100389,turkis,トルコ行進曲 (Remix Ver.),,Variety,Easy 11,Standard 19,Hard 39,Master 64,Unlimited 84, +100390,biohaz,code_,umbrella Cores,Variety,Easy 6,Standard 15,Hard 30,Master 51,Unlimited 64, +100391,monhan,英雄の証 ~ 4Version,カプコンサウンドチーム,Variety,Easy 5,Standard 10,Hard 26,Master 36,Unlimited 54, +100392,gyakut2,追求 ~最終プロモーションバージョン (crossbeats REV.アレンジ),岩垂 徳行,Variety,Easy 5,Standard 13,Hard 35,Master 43,Unlimited 56, +100393,street,Theme of Ryu -SFIV Arrange-,Capcom Sound Team / Hideyuki Fukasawa,Variety,Easy 7,Standard 13,Hard 34,Master 47,Unlimited 66, +100394,rockma2,Dr. WILY STAGE 1 -OMEGAMAN MIX-,ROCK-MEN,Variety,Easy 14,Standard 21,Hard 34,Master 49,Unlimited 76, +100374,auflcb3,SOMEDAY -00.prologue-,TЁЯRA,Original,Easy 6,Standard 16,Hard 36,Master 66,Unlimited 86, +100325,irohaa,Iroha,Ryunosuke Kudo,Original,Easy 12,Standard 19,Hard 41,Master 55,Unlimited 76, +100326,ibelie,I Believe Someday,SPARKER,Original,Easy 14,Standard 27,Hard 47,Master 78,Unlimited 82, +100409,monhan2,灼熱の刃 ~ ディノバルド,カプコンサウンドチーム,Variety,Easy 6,Standard 12,Hard 24,Master 43,Unlimited 68, +100410,monhan3,古代の息吹き,カプコンサウンドチーム,Variety,Easy 8,Standard 18,Hard 28,Master 45,Unlimited 73, +100418,yejiii,YEJI,ginkiha,Original,Easy 10,Standard 22,Hard 36,Master 63,Unlimited 79, +100419,histor,HISTORIA,Cranky,Original,Easy 11,Standard 20,Hard 36,Master 56,Unlimited 82, +100338,chaset,Chase the WAVE,Tatsh feat. AKINO with bless4,Original,Easy 8,Standard 15,Hard 31,Master 58,Unlimited 76, +100412,metall,Metallical parade,Vice Principal,Original,Easy 8,Standard 16,Hard 28,Master 57,Unlimited 77, +100327,letmeg,Let Me Give You My Heart,brinq,Original,Easy 12,Standard 18,Hard 32,Master 50,Unlimited 72, +100010,hontno,ホントのワタシ,mao,Original,Easy 9,Standard 12,Hard 26,Master 53,Unlimited 66, +100024,azitat,Azitate,void,Original,Easy 14,Standard 24,Hard 55,Master 70,Unlimited 83, +100360,hellom,Hello Mr.crosbie,民安★ROCK,Original,Easy 7,Standard 18,Hard 37,Master 58,Unlimited 72, +100337,laught,Perfect laughter,ぽんず loved by yksb,Original,Easy 7,Standard 20,Hard 35,Master 51,Unlimited 71, +100426,bluede2,Blue Destiny Blue ETERNAL,NAOKI feat. Florence McNair,Original,Easy 9,Standard 16,Hard 36,Master 56,Unlimited 81, +100423,street2,Ultra Street Fighter IV,Hideyuki Fukasawa,Variety,Easy 14,Standard 21,Hard 37,Master 55,Unlimited 73, +100424,street3,Theme of Chun-Li -SFIV Arrange-,Capcom Sound Team / Hideyuki Fukasawa,Variety,Easy 13,Standard 24,Hard 38,Master 57,Unlimited 74, +100425,street4,Street Fighter V,Masahiro Aoki,Variety,Easy 11,Standard 17,Hard 32,Master 51,Unlimited 78, +100421,silbur,Silbury Sign,カヒーナムジカ,Original,Easy 9,Standard 20,Hard 35,Master 54,Unlimited 75, +100422,spicaa,Spica,Endorfin.,Original,Easy 10,Standard 23,Hard 36,Master 56,Unlimited 78, +100438,tricko,Trick Or Treat,SLAKE,Original,Easy 11,Standard 23,Hard 34,Master 56,Unlimited 75, +100435,thisis,THIS IS HDM,Relect,Original,Easy 12,Standard 23,Hard 37,Master 60,Unlimited 74, +100436,rising,Rising Day ft. Satan,GRATEC MOUR,Original,Easy 14,Standard 23,Hard 38,Master 66,Unlimited 85, +100411,orbita,Orbital velocity,Vice Principal,Original,Easy 12,Standard 20,Hard 38,Master 62,Unlimited 74, +100433,dddddd,D,六弦アリス,Original,Easy 9,Standard 13,Hard 33,Master 53,Unlimited 69, +100427,pyroma,Pyromania,KO3,Original,Easy 8,Standard 22,Hard 42,Master 70,Unlimited 84, +100312,touchn,Touch n Go,Paisley Parks,Original,Easy 15,Standard 27,Hard 46,Master 68,Unlimited 86, +100359,onlyll,only L,emon,Original,Easy 13,Standard 21,Hard 32,Master 56,Unlimited 69, +100313,upside,Upside Down,Nave ft.Mayu Wakisaka,Original,Easy 8,Standard 18,Hard 27,Master 48,Unlimited 67, +100322,istanb,İstanbul,REVen-G,Original,Easy 23,Standard 41,Hard 49,Master 90,Unlimited 98, +100371,memori,Memoria ~終焉を司る荊姫の静粛なる宴~,Astilbe × arendsii,Original,Easy 13,Standard 26,Hard 38,Master 65,Unlimited 84, +100350,straye,Strayer,Taishi,Original,Easy 14,Standard 25,Hard 36,Master 61,Unlimited 73, +100358,rearhy,Rearhythm,CooRie,Original,Easy 7,Standard 17,Hard 32,Master 52,Unlimited 69, +100432,hereco,Here comes the sun ~For you~,Z pinkpong,Original,Easy 11,Standard 16,Hard 34,Master 51,Unlimited 68, +100441,thesun,THE SUN,Tatsh,Original,Easy 13,Standard 25,Hard 40,Master 72,Unlimited 87, +100343,sayona,さよなら最終列車,むかしばなし,Original,Easy 10,Standard 20,Hard 34,Master 53,Unlimited 71, +100380,flameu,Flame Up,Inu Machine,Original,Easy 10,Standard 18,Hard 36,Master 57,Unlimited 65, +100434,raidon,RAiD on Mars,sky_delta,Original,Easy 13,Standard 30,Hard 38,Master 58,Unlimited 87, +100437,riseup,Rise Up,Dubscribe,Original,Easy 7,Standard 18,Hard 41,Master 67,Unlimited 77, +100431,sunglo,Sunglow,Yamajet feat. ひうらまさこ,Original,Easy 10,Standard 18,Hard 39,Master 59,Unlimited 72, +100439,kinbos,金星(kinboshi),Hideyuki Ono,Original,Easy 12,Standard 22,Hard 38,Master 64,Unlimited 77, +100430,densho,電脳少女と機械仕掛けの神,Chimera music.,Original,Easy 17,Standard 28,Hard 42,Master 74,Unlimited 90, +100471,aiohoo,愛をほおばりたいッ!~Like a Monkey!~,新堂敦士,J-Pop,Easy 10,Standard 18,Hard 29,Master 42,Unlimited 66, +100472,entert,エンターテイナー (Remix ver.),,Variety,Easy 9,Standard 15,Hard 33,Master 54,Unlimited 87, +100457,takeit,Take It Back,Daniel Seven,Original,Easy 12,Standard 20,Hard 38,Master 65,Unlimited 83, +100449,harmon,Harmony,ピクセルビー,Original,Easy 12,Standard 20,Hard 36,Master 49,Unlimited 65, +100428,avemar,アヴェ・マリア (Remix ver.),,Variety,Easy 8,Standard 16,Hard 31,Master 53,Unlimited 75, +100429,mateki,復讐の炎は地獄のように我が心に燃え (Remix ver.),,Variety,Easy 10,Standard 18,Hard 35,Master 54,Unlimited 79, +100445,lovech,LOVE CHASE,大島はるな,Original,Easy 8,Standard 16,Hard 30,Master 46,Unlimited 68, +100473,akaihe,赤いヘッドホン,新堂敦士,J-Pop,Easy 8,Standard 21,Hard 32,Master 50,Unlimited 69, +100474,juicys,Juicy! ~幸せスパイラル~,新堂敦士,J-Pop,Easy 10,Standard 18,Hard 26,Master 45,Unlimited 83, +100468,codena,CODENAMEはEQ,TORIENA,Original,Easy 9,Standard 18,Hard 35,Master 48,Unlimited 68, +100475,groove,LINK LINK FEVER!!!(グルーヴコースター 3 リンクフィーバーより),リンカ (CV:豊田萌絵),Variety,Easy 10,Standard 22,Hard 40,Master 52,Unlimited 73, +100450,kansho,観賞用マーメイド,ヤマイ,Original,Easy 7,Standard 13,Hard 27,Master 55,Unlimited 74, +100486,overcl2,Over Clock ~前兆~,NAOKI feat. un∞limited,Original,Easy 12,Standard 23,Hard 42,Master 58,Unlimited 74, +100483,taikoo,SAKURA EXHAUST,RIO HAMAMOTO(BNSI)「太鼓の達人」より,Variety,Easy 6,Standard 13,Hard 39,Master 50,Unlimited 75, +100480,groove2,QLWA(グルーヴコースター 3 リンクフィーバーより),t+pazolite,Variety,Easy 9,Standard 15,Hard 40,Master 58,Unlimited 85, +100487,overcl,Over Clock ~開放~,NAOKI feat. un∞limited,Original,Easy 8,Standard 12,Hard 35,Master 57,Unlimited 86, +100466,notoss,Notos,ginkiha,Original,Easy 8,Standard 12,Hard 42,Master 65,Unlimited 91, +100447,machup,マチュ☆ピチュ,コツキミヤ,Original,Easy 9,Standard 17,Hard 32,Master 41,Unlimited 71, +100488,groove3,カリソメ(グルーヴコースター 3 リンクフィーバーより),コンプ(豚乙女) × ichigo(岸田教団 & THE明星ロケッツ),Touhou + Variety,Easy 10,Standard 18,Hard 34,Master 64,Unlimited 85, +100489,groove4,そして誰もいなくなった(グルーヴコースター 3 リンクフィーバーより),コバヤシユウヤ(IOSYS) × あにー(TaNaBaTa),Touhou + Variety,Easy 12,Standard 22,Hard 35,Master 50,Unlimited 75, +100482,everyt,EVERYTHING,Tatsh feat.小田ユウ,Original,Easy 13,Standard 22,Hard 30,Master 74,Unlimited N/A, +100465,lespri,L'esprit,Cosine,Original,Easy 13,Standard 25,Hard 57,Master 80,Unlimited N/A, +100491,groove5,グルーヴ・ザ・ハート(グルーヴコースター 3 リンクフィーバーより),ビートまりお+あまね,Variety,Easy 14,Standard 24,Hard 37,Master 67,Unlimited N/A, +100490,honeyo,HONEY♡SUNRiSE ~Original Side~,jun with Aimee,Original,Easy 24,Standard 32,Hard 63,Master 88,Unlimited 93, +100494,groove6,Got hive of Ra(グルーヴコースター 3 リンクフィーバーより),E.G.G.,Variety,Easy 22,Standard 30,Hard 64,Master 79,Unlimited N/A, +100495,sunglo2,Sunglow (Happy Hardcore Style),Yamajet feat. ひうらまさこ,Original,Easy 11,Standard 21,Hard 36,Master 67,Unlimited 81, +100498,fourte,14th Clock,INNOCENT NOIZE,Original,Easy 14,Standard 24,Hard 50,Master 74,Unlimited 80, +100496,monhan4,英雄の証/MHF-G 2015 Version,若林タカツグ,Variety,Easy 5,Standard 12,Hard 40,Master 51,Unlimited 62, +100497,monhan5,異ヲ辿リシモノ -対峙-,若林タカツグ,Variety,Easy 10,Standard 12,Hard 35,Master 42,Unlimited 65, +100504,darkpa,Dark Parashu,INNOCENT NOIZE,Original,Easy 16,Standard 26,Hard 39,Master 70,Unlimited 84, +100505,hervor,Hervor,INNOCENT NOIZE,Original,Easy 18,Standard 28,Hard 39,Master 73,Unlimited 81, +100499,cirnon,チルノのパーフェクトさんすう教室,ARM+夕野ヨシミ (IOSYS) feat. miko,Touhou,Easy 17,Standard 24,Hard 40,Master 60,Unlimited 79, +100500,marisa,魔理沙は大変なものを盗んでいきました,ARM+夕野ヨシミ (IOSYS) feat. 藤咲かりん,Touhou,Easy 18,Standard 25,Hard 41,Master 62,Unlimited 85, +100501,yakini,究極焼肉レストラン!お燐の地獄亭!,ARM+夕野ヨシミ (IOSYS) feat. 藤枝あかね,Touhou,Easy 13,Standard 25,Hard 34,Master 58,Unlimited 82, +100502,justic,ジャスティス・オブ・ザ・界隈 ~ALL IS FAIR IN LOVE AND ALIMARI~,void (IOSYS) feat.山本椛,Touhou,Easy 14,Standard 19,Hard 36,Master 57,Unlimited 83, +100503,sintyo,進捗どうですか?,sumijun feat.ななひら,Touhou,Easy 16,Standard 25,Hard 46,Master 70,Unlimited 83, +100347,ascand,Ascendanz,void,Original,Easy 18,Standard 32,Hard 54,Master 80,Unlimited 90, +100506,blackl,Black Lotus,Maozon,Original,Easy 12,Standard 19,Hard 41,Master 73,Unlimited 84, +100043,childr,チルドレン・オートマトン~ある歌声の亡霊~,あさまっく,Original,Easy 14,Standard 24,Hard 39,Master 56,Unlimited 62, +100044,tsukai,ツカイステ・デッドワールド,コゲ犬×ゆちゃ,Original,Easy 13,Standard 19,Hard 44,Master 72,Unlimited 76, +100067,rideon,RIDE ON NOW!,さつき が てんこもり feat.un:c,Original,Easy 16,Standard 29,Hard 41,Master 60,Unlimited 80, +100507,minest,Minestrone,orangentle,Original,Easy 13,Standard 21,Hard 39,Master 62,Unlimited 76, +100508,ordine,Ordine,orangentle,Original,Easy 19,Standard 25,Hard 43,Master 73,Unlimited 82, +100509,dreamw,DReamWorKer,LC:AZE,Original,Easy 16,Standard 26,Hard 37,Master 62,Unlimited 75, +100510,minerv,Minerva,xi,Original,Easy 25,Standard 32,Hard 61,Master 90,Unlimited N/A, +100001,wannab,Wanna Be Your Special,Shoichiro Hirata feat. SUIMI,Original,Easy 5,Standard 9,Hard 23,Master 40,Unlimited 65, +100511,sekain,世界の果て,Yamajet,Original,Easy 16,Standard 26,Hard 39,Master 69,Unlimited 78, +100512,farawa,Faraway,ミフメイ,Original,Easy 18,Standard 23,Hard 36,Master 60,Unlimited 76, +100100,crissc,Crisscrosser,void,Original,Easy 17,Standard 37,Hard 63,Master 86,Unlimited 91, +100324,speedy,Awake Speedy,DJ MURASAME,Original,Easy 11,Standard 22,Hard 55,Master 77,Unlimited N/A, +100513,xxxrev,XXX-revolt,void feat. KOTOKO,Original,Easy 15,Standard 21,Hard 34,Master 56,Unlimited 73, +100016,higame,Hi,Go-qualia,Original,Easy 13,Standard 20,Hard 30,Master 58,Unlimited 71, +100022,theepi,The Epic,Cranky,Original,Easy 14,Standard 19,Hard 40,Master 61,Unlimited 75, +100023,anomie,Anomie,D-Fener,Original,Easy 15,Standard 22,Hard 38,Master 61,Unlimited 77, +100524,crocus,Crocus,村瀬悠太,Original,Easy 15,Standard 26,Hard 37,Master 60,Unlimited 72, +100546,lavien,La vie en Fleurs,VILA,Original,Easy 18,Standard 27,Hard 41,Master 71,Unlimited 80, +100361,megaro2,MEGALOMAN[i]A -2nd IMPACT-,NEO-G,Original,Easy N/A,Standard N/A,Hard N/A,Master 99,Unlimited 100, +100541,chipnn,Chip Notch Educ@tion,yaseta feat. chip_Notch,Original,Easy 16,Standard 27,Hard 34,Master 61,Unlimited 79, +100007,yiyoyi,Wanyo Wanyo,MC Natsack,Original,Easy 7,Standard 14,Hard 33,Master 56,Unlimited 70, +100014,binary,Binary Overdrive,フラット3rd,Original,Easy 14,Standard 17,Hard 35,Master 64,Unlimited 89, +100054,makaim,魔界村 (平地BGM),Remixed by ARM (IOSYS),Original + Variety,Easy 23,Standard 30,Hard 50,Master 77,Unlimited N/A, +100055,gyakut,逆転裁判 (綾里真宵 ~逆転姉妹のテーマ),Remixed by OSTER project,Original + Variety,Easy 6,Standard 15,Hard 21,Master 46,Unlimited 64, +100056,basara,戦国BASARA (SENGOKU BASARA),Remixed by SOUND HOLIC,Original + Variety,Easy 14,Standard 19,Hard 37,Master 64,Unlimited 73, +100514,daybre,DAYBREAK FRONTLINE,Orangestar,Vocaloid,Easy 9,Standard 16,Hard 32,Master 53,Unlimited 72, +100515,umiyur,ウミユリ海底譚,n-buna,Vocaloid,Easy 8,Standard 14,Hard 28,Master 46,Unlimited 64, +100516,chalur,シャルル,バルーン,Vocaloid,Easy 14,Standard 18,Hard 40,Master 60,Unlimited 72, +100517,melanc,メランコリック,Junky,Vocaloid,Easy 10,Standard 15,Hard 30,Master 50,Unlimited 63, +100518,konofu,このふざけた素晴らしき世界は、僕の為にある,n.k,Vocaloid,Easy 11,Standard 23,Hard 35,Master 62,Unlimited 81, +100526,bladem,The Blade Master,mikashu,Original,Easy 17,Standard 28,Hard 38,Master 63,Unlimited 75, +100536,southw,South wind,moimoi,Original,Easy 12,Standard 18,Hard 27,Master 57,Unlimited 68, +100537,ryuuse,流星デモクラシー,kamejack,Original,Easy 13,Standard 21,Hard 32,Master 59,Unlimited N/A, +100519,redhea,ROCK'N'ROLL☆FLYING REDHEAD,暁Records,Touhou,Easy 10,Standard 27,Hard 39,Master 59,Unlimited 72, +100520,warnin,WARNING×WARNING×WARNING,暁Records,Touhou,Easy 12,Standard 25,Hard 36,Master 61,Unlimited 74, +100521,topsec,TOP SECRET -My Red World-,暁Records,Touhou,Easy 13,Standard 24,Hard 34,Master 51,Unlimited 64, +100522,dddoll,DOWN DOWN DOLL,暁Records,Touhou,Easy 14,Standard 26,Hard 38,Master 55,Unlimited 63, +100548,tracee,トレイス・エゴイズム,暁Records,Touhou,Easy 9,Standard 19,Hard 31,Master 49,Unlimited 65, +100111,drivin,Driving story,Duca,Original,Easy 8,Standard 23,Hard 40,Master 66,Unlimited 76, +100118,genzit,現実幻覚スピードスター,yozuca*,Original,Easy 12,Standard 18,Hard 46,Master 73,Unlimited 82, +100039,aerial,エアリアル,カヒーナムジカ,Original,Easy 5,Standard 11,Hard 28,Master 56,Unlimited 71, +100532,einher,Einherjar,閣下,Original,Easy 16,Standard 29,Hard 40,Master 74,Unlimited 80, +100540,ariell,Ariel,nanobii,Original,Easy 15,Standard 19,Hard 32,Master 64,Unlimited 73, +100542,firstl,First Love,UFO,Original,Easy 17,Standard 25,Hard 36,Master 65,Unlimited 77, +100550,heartl,Heartland,Bernis,Original,Easy 11,Standard 23,Hard 30,Master 64,Unlimited N/A, +100551,erasee,ERASE,MozSound,Original,Easy 12,Standard 22,Hard 35,Master 58,Unlimited 68, +100530,regene,Regeneration ray,Tsukasa,Original,Easy 13,Standard 20,Hard 30,Master 56,Unlimited 70, +100549,allelu,アレルヤ,HAKKYOU-KUN feat.玉置成実,Original,Easy 16,Standard 28,Hard 35,Master 64,Unlimited 75, +100543,lighto,Light of my Life,S3RL,Original,Easy 12,Standard 25,Hard 33,Master 60,Unlimited 74, +100552,termin,Terminus a quo,ginkiha,Original,Easy 13,Standard 24,Hard 34,Master 63,Unlimited 79, +100556,ryuuse2,流星でもくらちー☆,kamejack,Original,Easy 13,Standard 20,Hard 36,Master 62,Unlimited 75, +100547,prizmm,PRIZM,ミフメイ,Original,Easy 12,Standard 21,Hard 30,Master 54,Unlimited N/A, +100098,samalv,サマ★ラブ,コツキミヤ,Original,Easy 13,Standard 19,Hard 39,Master 58,Unlimited 77, +100544,palpit,Palpitation,Zekk,Original,Easy 18,Standard 29,Hard 55,Master 84,Unlimited 92, +100558,gainen,Break the Wall!! ~ロンリガイネン,暁Records,Original,Easy 15,Standard 26,Hard 37,Master 63,Unlimited N/A, +100525,moonsh,Moon Shard,satella,Original,Easy 10,Standard 23,Hard 36,Master 62,Unlimited N/A, +100559,moonki,MoonLightKiss,effe,Original,Easy 13,Standard 25,Hard 39,Master 64,Unlimited N/A, +100560,moonri,Moonrise,Relect,Original,Easy 14,Standard 21,Hard 38,Master 58,Unlimited 85, +100561,goaway,Go Away,Cranky,Original,Easy 10,Standard 23,Hard 45,Master 59,Unlimited 70, +100567,itback,Bring it back now,siromaru,Original,Easy 12,Standard 23,Hard 38,Master 71,Unlimited N/A, +100569,redhhh,Red Heart,Yooh vs. siromaru,Original,Easy 13,Standard 24,Hard 39,Master 77,Unlimited N/A, +100568,actual,Actual Reverse,siromaru,Original,Easy 14,Standard 25,Hard 38,Master 80,Unlimited N/A, +100367,zonzon,Bi-Zon Zon Zombi,MC Natsack,Original,Easy 5,Standard 16,Hard 33,Master 63,Unlimited 67, +100565,memorm,Memorim,Avans,Original,Easy 15,Standard 26,Hard 37,Master 73,Unlimited N/A, +100554,kokoro,ココロメソッド,Endorfin.,Original,Easy 12,Standard 20,Hard 43,Master 65,Unlimited 69, +100563,poweri,Power is Power,KO3,Original,Easy 13,Standard 26,Hard 49,Master 75,Unlimited 91, +100555,nisenn,2020,Ω,Original,Easy N/A,Standard N/A,Hard N/A,Master 76,Unlimited N/A, +100096,yukiya,Vespero,Monotone Rhythm feat.綾川雪弥,Original,Easy 11,Standard 19,Hard 40,Master 61,Unlimited N/A, +100124,zankyo,残響のアカーシャ,Astilbe × arendsii,Original,Easy 10,Standard 18,Hard 38,Master 57,Unlimited 74, +100119,overlp,オーバーラップ,millie loved by yksb,Original,Easy 9,Standard 17,Hard 30,Master 51,Unlimited N/A, +100529,fracta,Fractalize,Sakuzyo,Original,Easy 19,Standard 31,Hard 52,Master 83,Unlimited N/A, +100455,cantst,Can't Stop,KaSa,Original,Easy 11,Standard 23,Hard 42,Master 65,Unlimited N/A, +100527,primaa,Prima,Kiryu(桐生),Original,Easy 12,Standard 18,Hard 35,Master 54,Unlimited 75, +100448,cyberg,CYBER GANG,ヒゲドライVAN,Original,Easy 12,Standard 23,Hard 35,Master 60,Unlimited N/A, +100018,freakw,Freak With Me,SLAKE,Original,Easy 13,Standard 22,Hard 42,Master 65,Unlimited 66, +100006,aquali,Aqualight,MAYA AKAI,Original,Easy 11,Standard 16,Hard 34,Master 58,Unlimited N/A, +100572,takesc,Music Takes Control,Fierce Chain,Original,Easy 10,Standard 27,Hard 37,Master 69,Unlimited N/A, +100531,cthugh,Cthugha,MozSound,Original,Easy 14,Standard 25,Hard 48,Master 73,Unlimited N/A, +100571,thetaa,θ (theta) ,effe,Original,Easy 11,Standard 21,Hard 34,Master 62,Unlimited N/A, +100493,nekofu,ネコふんじゃった☆ (クローニャSTYLE),,Variety,Easy 10,Standard 22,Hard 34,Master 57,Unlimited 80, +100057,howtru,How True Is Your Love,brinq,Original,Easy 8,Standard 12,Hard 25,Master 53,Unlimited 74, +100047,romanc,ロマンシングゲーム,まふ×ティン,Original,Easy 10,Standard 28,Hard 55,Master 78,Unlimited N/A, +100573,kotobu,KOTOBUKI,REVen-G,Original,Easy 25,Standard 32,Hard 71,Master 90,Unlimited N/A, +100417,xmasss,ジングルベル (NM REMIX),,Variety,Easy 8,Standard 18,Hard 38,Master 56,Unlimited 77, +100600,galaxy,GALAXY,キュウソネコカミ,J-Pop,Easy 10,Standard 16,Hard 32,Master 43,Unlimited 67, +100601,rebell,Rebellion,NAOKI underground,Original,Easy N/A,Standard 49,Hard 63,Master 91,Unlimited N/A, +100602,anothe,Another Chance,Luci,Original,Easy N/A,Standard 27,Hard 37,Master 73,Unlimited 76, +100603,addict,Addicted,luz×アリエP,Original,Easy N/A,Standard 20,Hard 34,Master 52,Unlimited 62, +100604,dirtyy,Dirty Mouth,Asletics,Original,Easy N/A,Standard 15,Hard 28,Master 59,Unlimited 74, +100605,levelf,LEVEL5-Judgelight-,fripSide,J-Pop,Easy 5,Standard 11,Hard 28,Master 45,Unlimited 63, +100606,omnive,Omniverse,Atomic,Original,Easy N/A,Standard 34,Hard 52,Master 83,Unlimited 86, +100607,kakuse,覚醒 ∞ awake!,PwD,Original,Easy N/A,Standard 17,Hard 55,Master 75,Unlimited N/A, +100608,unbeli,アンビリーバーズ,米津玄師,J-Pop,Easy 7,Standard 13,Hard 26,Master 38,Unlimited 62, +100609,sonzai,ソンザイキョウドウタイ,ジギル,Original,Easy N/A,Standard 26,Hard 40,Master 59,Unlimited 66, +100610,okonik,OKONIKUKOD,SHU OKUYAMA,Original,Easy N/A,Standard 26,Hard 45,Master 67,Unlimited N/A, +100611,crssho,CrossShooter,Tatsh,Original,Easy 10,Standard 35,Hard 60,Master 85,Unlimited N/A, +100612,reanim,Reanimation,DC feat.S!N,Original,Easy N/A,Standard 28,Hard 44,Master 70,Unlimited 80, +100613,kamino,kaminoko,HAKKYOU-KUN,Original,Easy 15,Standard 40,Hard 62,Master 78,Unlimited N/A, +100614,fiveee,Five,ANOTHER STORY,J-Pop,Easy 10,Standard 18,Hard 37,Master 61,Unlimited 71, +100615,granda,Grand Arc,Tosh,Original,Easy N/A,Standard 21,Hard 38,Master 79,Unlimited N/A, +100616,fronti2,NEXT FRONTIER -TRUE RISE-,NAOKI,Original,Easy 9,Standard 46,Hard 69,Master 89,Unlimited N/A, +100617,saigon,最後の1ページ,桜井零士,Original,Easy N/A,Standard 19,Hard 31,Master 57,Unlimited N/A, +100618,replay,REPLAY,VAMPS,J-Pop,Easy 8,Standard 18,Hard 44,Master 63,Unlimited 70, +100619,mousou,妄想全開,志麻×ふぉP,Original,Easy N/A,Standard 16,Hard 26,Master 54,Unlimited N/A, +100620,aheadd,AHEAD,VAMPS,J-Pop,Easy 7,Standard 13,Hard 25,Master 35,Unlimited 58, +100621,musicr1,All You Need Is Beat(s) -musicるTV・ミリオン連発音楽作家塾第7弾-,CLONE,Original,Easy 12,Standard 22,Hard 33,Master 58,Unlimited 74, +100622,getthe,Get the glory,中ノ森文子,J-Pop,Easy 6,Standard 17,Hard 37,Master 49,Unlimited 66, +100623,design,Designed World,Alinut,Original,Easy N/A,Standard 15,Hard 39,Master 68,Unlimited 69, +100624,garnet,GARNET HOWL,フラット3rd,Original,Easy N/A,Standard 26,Hard 46,Master 70,Unlimited 94, +100625,hopesb,Hopes Bright,WHITE ASH,J-Pop,Easy 7,Standard 10,Hard 25,Master 44,Unlimited 61, +100626,shooti,Shooting Star feat.HISASHI (GLAY),96猫,J-Pop,Easy 7,Standard 15,Hard 37,Master 49,Unlimited 69, +100627,dangan,弾丸と星空,HAKKYOU-KUN,Original,Easy N/A,Standard 28,Hard 58,Master 81,Unlimited N/A, +100628,impact,Impact,Tatsh,Original,Easy 20,Standard 24,Hard 60,Master 72,Unlimited 90, +100629,lightm,Light My Fire,KOTOKO,J-Pop,Easy 11,Standard 26,Hard 33,Master 54,Unlimited 71, +100630,miiroo,海色,AKINO from bless4,J-Pop,Easy 11,Standard 22,Hard 39,Master 58,Unlimited 68, +100631,voiceo,Voice Of House,DOT96,Original,Easy N/A,Standard 18,Hard 34,Master 58,Unlimited 59, +100632,cosmol,Cosmology,RIC,Original,Easy 25,Standard 36,Hard 64,Master 87,Unlimited N/A, +100633,vividd,ViViD,May'n,J-Pop,Easy 9,Standard 16,Hard 35,Master 55,Unlimited 65, +100634,splash,SPLASH,MAYA AKAI,Original,Easy N/A,Standard 26,Hard 50,Master 71,Unlimited N/A, +100635,donuth,ドーナツホール,ハチ,Vocaloid,Easy 11,Standard 22,Hard 40,Master 54,Unlimited 80, +100636,senbon,千本桜,和楽器バンド,Vocaloid,Easy 12,Standard 20,Hard 28,Master 54,Unlimited 74, +100637,kmtyju,君と野獣,バンドハラスメント,J-Pop,Easy 12,Standard 24,Hard 31,Master 57,Unlimited 74, +100638,fronti,NEXT FRONTIER,NAOKI,Original,Easy 13,Standard 48,Hard 65,Master 82,Unlimited N/A, +100639,nueraa,Nu Era,SPARKER,Original,Easy N/A,Standard 22,Hard 43,Master 75,Unlimited 53, +100640,childe,CHiLD -error-,MY FIRST STORY,J-Pop,Easy 4,Standard 9,Hard 24,Master 34,Unlimited 56, +100641,dazzli2,DAZZLING♡SEASON (Darwin Remix),jun,Original,Easy 19,Standard 35,Hard 60,Master 82,Unlimited N/A, +100642,perfec,Perfectionism,高橋渉 feat.2d6,Original,Easy N/A,Standard 39,Hard 64,Master 78,Unlimited N/A, +100643,flower,Flowerwall,米津玄師,J-Pop,Easy 6,Standard 7,Hard 20,Master 40,Unlimited 65, +100644,frgmnt,Frgmnts,Nyolfen,Original,Easy 10,Standard 33,Hard 63,Master 74,Unlimited 65, +100645,headph,HEADPHONE PARTY,A-One,Original,Easy N/A,Standard 24,Hard 32,Master 52,Unlimited N/A, +100646,crsang,Cross+Angel,Tatsh feat. 彩音,Original,Easy 13,Standard 27,Hard 53,Master 67,Unlimited N/A, +100647,musicr4,Accept,sushi feat.とよだま,Original,Easy 12,Standard 19,Hard 32,Master 58,Unlimited N/A, +100648,imaxim,A×E×U×G -act.1-,190Cb,Original,Easy N/A,Standard 44,Hard 69,Master 90,Unlimited 87, +100649,azitat2,Azitate (Prologue Edition),void,Original,Easy 8,Standard 23,Hard 52,Master 66,Unlimited N/A, +100650,dynami,DYNAMITE SENSATION,NAOKI,Original,Easy 11,Standard 26,Hard 54,Master 68,Unlimited N/A, +100651,incave,Into the Cave,Jerico,Original,Easy N/A,Standard 22,Hard 44,Master 76,Unlimited 78, +100652,aktuki,AKATSUKI,NAOKI underground,Original,Easy 10,Standard 26,Hard 58,Master 84,Unlimited N/A, +100653,kindof,Wonderful,Fraz,Original,Easy N/A,Standard 14,Hard 29,Master 48,Unlimited N/A, +100654,mikaku,未確認XX生命体,民安★ROCK,Original,Easy N/A,Standard 19,Hard 31,Master 54,Unlimited N/A, +100655,strang,ストレンジ・ディーヴァ,麹町養蚕館,Original,Easy N/A,Standard 12,Hard 28,Master 55,Unlimited N/A, +100656,hesper,Hesperides,xi,Original,Easy N/A,Standard 36,Hard 61,Master 92,Unlimited 93, +100657,breaka,Break a spell,川田まみ,J-Pop,Easy 7,Standard 15,Hard 31,Master 45,Unlimited 68, +100658,myname,When You Call My Name,Beat Envy,Original,Easy N/A,Standard 6,Hard 14,Master 30,Unlimited 57, +100659,amaiko,甘い言葉,Kenichi Chiba feat. EVO+,Original,Easy N/A,Standard 15,Hard 37,Master 60,Unlimited N/A, +100660,reseed2,Reseed,quick master,Original,Easy N/A,Standard 22,Hard 47,Master 63,Unlimited N/A, +100661,kingst,KING STUN,JUPITRIS,Original,Easy 12,Standard 38,Hard 63,Master 74,Unlimited N/A, +100662,ramram,Break Your World,RAM,Original,Easy N/A,Standard 23,Hard 34,Master 67,Unlimited N/A, +100663,murasa,Murasame,Ryunosuke Kudo,Original,Easy N/A,Standard 28,Hard 41,Master 76,Unlimited N/A, +100664,happyd,Happy Deathday,ANOTHER STORY,Original,Easy 18,Standard 22,Hard 41,Master 73,Unlimited 79, +100665,izimed,イジメ、ダメ、ゼッタイ,BABYMETAL,J-Pop,Easy 9,Standard 19,Hard 39,Master 69,Unlimited 77, +100666,wastel,Wasteland,James Taplin,Original,Easy N/A,Standard 4,Hard 12,Master 23,Unlimited 40, +100667,assign,Assign,MASAYASU,Original,Easy N/A,Standard 26,Hard 43,Master 61,Unlimited 62, +100668,jahaci,Jahacid,DJ SODEYAMA,Original,Easy N/A,Standard 17,Hard 29,Master 59,Unlimited N/A, +100669,hisuii,Hisui,stereoberry,Original,Easy N/A,Standard 22,Hard 47,Master 70,Unlimited N/A, +100670,godkno,God knows...,涼宮ハルヒ(C.V.平野綾),J-Pop,Easy 6,Standard 10,Hard 26,Master 45,Unlimited 64, +100671,roadof,Road of Resistance,BABYMETAL,J-Pop,Easy 7,Standard 15,Hard 36,Master 50,Unlimited 75, +100672,rokuch,六兆年と一夜物語,和楽器バンド,J-Pop + Vocaloid,Easy 11,Standard 21,Hard 35,Master 62,Unlimited 81, +100673,valent,いつか王子様が (Remix Ver.),,Original,Easy 10,Standard 27,Hard 33,Master 59,Unlimited 77, +100674,unfini,→unfinished→,KOTOKO,J-Pop,Easy 8,Standard 16,Hard 32,Master 50,Unlimited 71, +100675,auflcb2,some day -see you again-,NAOKI,Original,Easy 10,Standard 22,Hard 37,Master 75,Unlimited N/A, +100676,burnin,Burning Inside,Nhato,Original,Easy 15,Standard 18,Hard 28,Master 60,Unlimited 85, +100677,sphere,Hypersphere,Dubscribe,Original,Easy N/A,Standard 20,Hard 38,Master 73,Unlimited N/A, +100678,dropou,D.O.B.,野水いおり,J-Pop,Easy 14,Standard 17,Hard 31,Master 46,Unlimited 69, +100679,xencou,X-encounter,黒崎真音,J-Pop,Easy 8,Standard 20,Hard 32,Master 52,Unlimited 60, +100680,killyk,killy killy JOKER,分島花音,J-Pop,Easy 6,Standard 13,Hard 42,Master 63,Unlimited 76, +100681,missil,the Last Missile Man,adHoc World,Original,Easy N/A,Standard 16,Hard 38,Master 59,Unlimited N/A, +100682,burstt,Burst The Gravity,ALTIMA,J-Pop,Easy 7,Standard 12,Hard 25,Master 46,Unlimited 63, +100683,musicr2,My Recklessness,Kagerou,Original,Easy 12,Standard 22,Hard 33,Master 58,Unlimited N/A, +100684,isingl,Isinglass,Voltex,Original,Easy 12,Standard 25,Hard 44,Master 80,Unlimited N/A, +100685,lvless,Loveless,YOSA,Original,Easy N/A,Standard 23,Hard 38,Master 60,Unlimited N/A, +100686,sapphi,Sapphire,voltex,Original,Easy N/A,Standard 29,Hard 44,Master 81,Unlimited N/A, +100687,musicr3,Climaxxx Party -musicるTV・ミリオン連発音楽作家塾第7弾-,Kyota. feat.とよだま&れい,Original,Easy 12,Standard 19,Hard 32,Master 58,Unlimited 72, +100688,deeout,Deep Outside,Seiho,Original,Easy N/A,Standard 18,Hard 34,Master 63,Unlimited 81, +100689,sugars,シュガーソングとビターステップ,UNISON SQUARE GARDEN,J-Pop,Easy 6,Standard 17,Hard 30,Master 42,Unlimited 66, +100690,mercur,MERCURY ,E.Z.M,Original,Easy N/A,Standard 14,Hard 35,Master 66,Unlimited N/A, +100691,zizizi,Z[i],Cybermiso,Original,Easy N/A,Standard 30,Hard 57,Master 88,Unlimited 96, +100692,wegooo,WE GO,BREAKERZ,J-Pop,Easy 10,Standard 18,Hard 34,Master 54,Unlimited 68, +100693,alonee,ALONE,MY FIRST STORY,J-Pop,Easy 5,Standard 11,Hard 21,Master 36,Unlimited 48, +100694,nuheat,Nu Heat,Paisley Parks,Original,Easy N/A,Standard 29,Hard 44,Master 65,Unlimited 85, +100695,granro,メモリーズ,GRANRODEO,J-Pop,Easy 8,Standard 15,Hard 28,Master 43,Unlimited 60, +100696,sister,sister's noise,fripSide,J-Pop,Easy 7,Standard 10,Hard 27,Master 46,Unlimited 63, +100697,lotusl,Lotus Love,Maozon,Original,Easy N/A,Standard 20,Hard 36,Master 64,Unlimited N/A, +100698,yukari,YUKARI,Ocelot,Original,Easy N/A,Standard 31,Hard 50,Master 76,Unlimited 84, +100699,flawli,フローライト,米津玄師,J-Pop,Easy 8,Standard 17,Hard 30,Master 40,Unlimited 59, +100700,nightf,NIGHT FEELIN',マセラティ渚,Original,Easy N/A,Standard 15,Hard 28,Master 46,Unlimited 71, +100701,random,シャッフルセレクト,シャッフルセレクト,Original,Easy N/A,Standard N/A,Hard N/A,Master N/A,Unlimited N/A, +100702,wiwwtw,What Is Wrong With The World,SADA,Original,Easy N/A,Standard 26,Hard 38,Master 62,Unlimited N/A, +100703,inneru,Inner Urge,上坂すみれ,Original,Easy 9,Standard 22,Hard 36,Master 48,Unlimited 67, +100704,taishi,Otherside,Taishi,Original,Easy N/A,Standard 19,Hard 35,Master 58,Unlimited N/A, +100705,daysss,Days,Kent Alexander,Original,Easy N/A,Standard 38,Hard 59,Master 81,Unlimited 81, +100706,bokuwa,僕は君のアジテーターじゃない feat.Neru,焚吐,J-Pop,Easy 16,Standard 23,Hard 34,Master 55,Unlimited 69, +100707,showww,掌 -show-,喜多村英梨,Original,Easy 15,Standard 18,Hard 35,Master 51,Unlimited 79, +100708,nevers,Never Sleep Again,PassCode,J-Pop,Easy 15,Standard 26,Hard 32,Master 65,Unlimited 75, +100709,bleeze,BLEEZE,GLAY,J-Pop,Easy 9,Standard 16,Hard 31,Master 47,Unlimited 62, +100710,dreami,DREAMIN' OF YOU feat.コッテル,Arts Of Collective,Original,Easy N/A,Standard 14,Hard 37,Master 65,Unlimited N/A, +100711,allune,All U Need,MesoPhunk,Pick-Up (New + Revival),Easy N/A,Standard 14,Hard 35,Master 71,Unlimited N/A, +100712,always,Always Thinking Of You,Sketchout,Pick-Up (New + Revival),Easy N/A,Standard 13,Hard 27,Master 49,Unlimited N/A, +100713,anomie2,Anomie (Axiom Style),D-Fener,Pick-Up (New + Revival),Easy N/A,Standard 16,Hard 43,Master 84,Unlimited N/A, +100714,aquali2,Aqualight (Remix Ver.),MAYA AKAI,Pick-Up (New + Revival),Easy N/A,Standard 22,Hard 43,Master 60,Unlimited 81, +100715,astaro,ASTAROTH,JUPITRIS,Pick-Up (New + Revival),Easy N/A,Standard 23,Hard 40,Master 74,Unlimited N/A, +100716,bassan,BASS ANTICS,Mitomoro,Pick-Up (New + Revival),Easy N/A,Standard 20,Hard 32,Master 66,Unlimited N/A, +100717,zonzon2,Bi-Zon Zon Zombi (More Zombies Ver.),MC Natsack,Pick-Up (New + Revival),Easy N/A,Standard 13,Hard 27,Master 68,Unlimited 75, +100718,bouled,boule de berlin,JTTR,Pick-Up (New + Revival),Easy N/A,Standard 19,Hard 30,Master 57,Unlimited N/A, +100719,brandn,BRAND NEW,Headphone-Tokyo(star)(star) feat.カヒーナ,Pick-Up (New + Revival),Easy N/A,Standard 9,Hard 39,Master 66,Unlimited 72, +100720,bravee,BRAVE,Ryuno,Pick-Up (New + Revival),Easy N/A,Standard 35,Hard 60,Master 82,Unlimited N/A, +100721,breakd2,Break down (2nd Edition),GARNiDELiA,Pick-Up (New + Revival),Easy N/A,Standard 34,Hard 64,Master 74,Unlimited N/A, +100722,buffet,Buffet survivor,Yamajet feat. Cathy & TEA,Pick-Up (New + Revival),Easy N/A,Standard 38,Hard 55,Master 68,Unlimited N/A, +100723,buzzke,BUZZ Ketos,フラット3rd,Pick-Up (New + Revival),Easy N/A,Standard 18,Hard 33,Master 58,Unlimited 77, +100724,cashhh,Cash!,Nor,Pick-Up (New + Revival),Easy N/A,Standard 19,Hard 25,Master 64,Unlimited N/A, +100725,cloudb,Cloudburst,Relect,Pick-Up (New + Revival),Easy N/A,Standard 37,Hard 66,Master 74,Unlimited N/A, +100726,clouds,cloudstepping,Ryuno,Pick-Up (New + Revival),Easy N/A,Standard 13,Hard 25,Master 47,Unlimited N/A, +100727,codepa,Code Paradiso,Himmel,Pick-Up (New + Revival),Easy N/A,Standard 29,Hard 55,Master 70,Unlimited N/A, +100728,comear,Come Around,MesoPhunk,Pick-Up (New + Revival),Easy N/A,Standard 38,Hard 56,Master 83,Unlimited N/A, +100729,crysta,Crystal Ribbon,Cosine,Pick-Up (New + Revival),Easy N/A,Standard 37,Hard 56,Master 81,Unlimited N/A, +100730,curseo,Curse of Doll,KO3,Pick-Up (New + Revival),Easy N/A,Standard 22,Hard 36,Master 74,Unlimited N/A, +100731,datami,data mining,voia,Pick-Up (New + Revival),Easy N/A,Standard 18,Hard 36,Master 66,Unlimited N/A, +100732,defaul,default affinity,JTTR,Pick-Up (New + Revival),Easy N/A,Standard 21,Hard 33,Master 48,Unlimited N/A, +100733,design2,Designed World (Remix ver.),Alinut,Pick-Up (New + Revival),Easy N/A,Standard 25,Hard 43,Master 68,Unlimited N/A, +100734,diamon,DIAMOND SKIN,GLAY,Pick-Up (New + Revival),Easy N/A,Standard 10,Hard 26,Master 33,Unlimited N/A, +100735,dispel,dispel,Endorfin.,Pick-Up (New + Revival),Easy N/A,Standard 28,Hard 48,Master 80,Unlimited N/A, +100736,distan,Distantmemory,村瀬悠太,Pick-Up (New + Revival),Easy N/A,Standard 20,Hard 30,Master 68,Unlimited N/A, +100737,dokibl,Doki Blaster,VOIA,Pick-Up (New + Revival),Easy N/A,Standard 15,Hard 23,Master 67,Unlimited N/A, +100738,dontwa,Don't Walk Away,Sarah-Jane,Pick-Up (New + Revival),Easy N/A,Standard 13,Hard 34,Master 69,Unlimited N/A, +100739,drgirl,Dreaming Girl,Nor,Pick-Up (New + Revival),Easy N/A,Standard 19,Hard 35,Master 54,Unlimited 73, +100740,eterna,Eternally,GLAY,Pick-Up (New + Revival),Easy N/A,Standard 12,Hard 21,Master 39,Unlimited N/A, +100741,everkr,everKrack,GLAY,Pick-Up (New + Revival),Easy N/A,Standard 18,Hard 29,Master 41,Unlimited N/A, +100742,everwh,EverWhite,satella,Pick-Up (New + Revival),Easy N/A,Standard 20,Hard 31,Master 58,Unlimited N/A, +100743,farthe,FarthestEnd,Sakuzyo,Pick-Up (New + Revival),Easy N/A,Standard 30,Hard 56,Master 78,Unlimited 87, +100744,filame,Filament Flow,Endorfin.,Pick-Up (New + Revival),Easy N/A,Standard 23,Hard 38,Master 63,Unlimited N/A, +100745,flameu2,Flame Up (Remix Ver.),Inu Machine,Pick-Up (New + Revival),Easy N/A,Standard 17,Hard 24,Master 59,Unlimited N/A, +100746,freeee,Free,千π,Pick-Up (New + Revival),Easy N/A,Standard 19,Hard 39,Master 69,Unlimited N/A, +100747,funkyb2,FUNKYBABY EVOLUTION,Yamajet,Pick-Up (New + Revival),Easy N/A,Standard 21,Hard 34,Master 56,Unlimited N/A, +100748,granda2,Grand Arc (Club Remix),Tosh,Pick-Up (New + Revival),Easy N/A,Standard 24,Hard 41,Master 73,Unlimited 83, +100749,hsphsp,H.S.P (Hard Style Party),Ravine & Tom Budin,Pick-Up (New + Revival),Easy N/A,Standard 12,Hard 25,Master 69,Unlimited N/A, +100750,halluc,Hallucination XXX,t+pazolite,Pick-Up (New + Revival),Easy N/A,Standard 40,Hard 52,Master 87,Unlimited N/A, +100751,indigo,Indigo Isle,Syntax Error,Pick-Up (New + Revival),Easy N/A,Standard 17,Hard 33,Master 50,Unlimited 75, +100752,inters,Interstellar Plazma,KO3,Pick-Up (New + Revival),Easy N/A,Standard 25,Hard 42,Master 77,Unlimited N/A, +100753,incave2,Into the Cave (Another Edit),Jerico,Pick-Up (New + Revival),Easy N/A,Standard 31,Hard 57,Master 88,Unlimited N/A, +100754,ioniza,IONIZATION,llliiillliiilll,Pick-Up (New + Revival),Easy N/A,Standard 17,Hard 34,Master 70,Unlimited 85, +100755,guilty,JUSTICE [from] GUILTY,GLAY,Pick-Up (New + Revival),Easy N/A,Standard 15,Hard 28,Master 50,Unlimited N/A, +100756,keraun,Keraunos,Xiphoid Sphere (xi + siromaru),Pick-Up (New + Revival),Easy N/A,Standard 25,Hard 52,Master 79,Unlimited N/A, +100757,landin2,Landing on the moon (Instrumental Version),SIMON,Pick-Up (New + Revival),Easy N/A,Standard 20,Hard 34,Master 59,Unlimited 66, +100758,videog,Life In A Video Game,Bentobox,Pick-Up (New + Revival),Easy N/A,Standard 21,Hard 37,Master 62,Unlimited N/A, +100759,loseyo,Lose Your Mind,Vau Boy,Pick-Up (New + Revival),Easy N/A,Standard 20,Hard 30,Master 71,Unlimited N/A, +100760,machin,Machine,Sparky,Pick-Up (New + Revival),Easy N/A,Standard 12,Hard 28,Master 72,Unlimited N/A, +100761,makeit,Make It Fresh EDM ver.,HighLux,Pick-Up (New + Revival),Easy N/A,Standard 11,Hard 24,Master 48,Unlimited N/A, +100762,daydre,Mechanized Daydream,s-don,Pick-Up (New + Revival),Easy N/A,Standard 19,Hard 36,Master 80,Unlimited N/A, +100763,metron,Metro Night,ginkiha,Pick-Up (New + Revival),Easy N/A,Standard 20,Hard 44,Master 71,Unlimited N/A, +100764,milkyw,Milky Way Trip,Nor,Pick-Up (New + Revival),Easy N/A,Standard 22,Hard 31,Master 60,Unlimited N/A, +100766,nayuta,nayuta,happy machine,Pick-Up (New + Revival),Easy N/A,Standard 17,Hard 37,Master 68,Unlimited N/A, +100767,nightm,nightmares,Seeds of the Upcoming Infection,Pick-Up (New + Revival),Easy N/A,Standard 20,Hard 49,Master 73,Unlimited N/A, +100768,otherw,Other World,XIO,Pick-Up (New + Revival),Easy N/A,Standard 23,Hard 41,Master 76,Unlimited N/A, +100769,overth,Over The Blue (Breaking Through),Fracus & Darwin Feat. Jenna,Pick-Up (New + Revival),Easy N/A,Standard 33,Hard 57,Master 82,Unlimited N/A, +100770,uuuuuu,Phoenix,U,Pick-Up (New + Revival),Easy N/A,Standard 23,Hard 37,Master 74,Unlimited N/A, +100771,rainin,Raining Again feat. Bea Aria,Sanxion,Pick-Up (New + Revival),Easy N/A,Standard 16,Hard 41,Master 69,Unlimited N/A, +100772,raisey,Raise Your Handz!,KO3 & Relect,Pick-Up (New + Revival),Easy N/A,Standard 23,Hard 55,Master 75,Unlimited N/A, +100773,resona,Resonance,RAM,Pick-Up (New + Revival),Easy N/A,Standard 17,Hard 32,Master 64,Unlimited N/A, +100774,reuniv,Reuniverse,Headphone-Tokyo(star)(star) feat.カヒーナ,Pick-Up (New + Revival),Easy N/A,Standard 14,Hard 23,Master 41,Unlimited N/A, +100775,rhythm,RHYTHM GAME MACHINE,ginkiha,Pick-Up (New + Revival),Easy N/A,Standard 37,Hard 56,Master 78,Unlimited N/A, +100776,rushhh,Rush,TANUKI,Pick-Up (New + Revival),Easy N/A,Standard 25,Hard 37,Master 75,Unlimited N/A, +100777,steeee,S.T.E,Tatsh,Pick-Up (New + Revival),Easy N/A,Standard 30,Hard 58,Master 87,Unlimited N/A, +100778,sangey,Sangeyasya,NNNNNNNNNN,Pick-Up (New + Revival),Easy N/A,Standard 27,Hard 47,Master 85,Unlimited N/A, +100779,senpai,Senpai Slam,千π,Pick-Up (New + Revival),Easy N/A,Standard 38,Hard 54,Master 77,Unlimited N/A, +100780,sestea,Sestea,Feryquitous,Pick-Up (New + Revival),Easy N/A,Standard 27,Hard 47,Master 76,Unlimited N/A, +100781,silver,Silverd,Feryquitous,Pick-Up (New + Revival),Easy N/A,Standard 28,Hard 40,Master 69,Unlimited N/A, +100782,sodama,Soda Machine,Syntax Error,Pick-Up (New + Revival),Easy N/A,Standard 20,Hard 40,Master 65,Unlimited N/A, +100783,stardu,STARDUST (game edit),MINIKOMA★,Pick-Up (New + Revival),Easy N/A,Standard 19,Hard 33,Master 64,Unlimited N/A, +100784,starti,starting station,happy machine,Pick-Up (New + Revival),Easy N/A,Standard 17,Hard 31,Master 54,Unlimited 70, +100785,sunday,SUNDAY リベンジ,HAPPY SUNDAY,Pick-Up (New + Revival),Easy N/A,Standard 18,Hard 29,Master 46,Unlimited 67, +100786,sundro2,Sundrop (Remix ver.),Yamajet,Pick-Up (New + Revival),Easy N/A,Standard 30,Hard 48,Master 79,Unlimited 82, +100787,sunnyd,Sunny day,センラ×蒼炎P,Pick-Up (New + Revival),Easy N/A,Standard 23,Hard 38,Master 59,Unlimited N/A, +100788,superl,SuperLuminalGirl Rebirth,Yamajet feat. 小宮真央,Pick-Up (New + Revival),Easy N/A,Standard 15,Hard 32,Master 59,Unlimited N/A, +100789,switch,SW!TCH,千π & MesoPhunk,Pick-Up (New + Revival),Easy N/A,Standard 16,Hard 35,Master 69,Unlimited N/A, +100790,theepi2,The Epic -Introduction-,Cranky,Pick-Up (New + Revival),Easy N/A,Standard 22,Hard 37,Master 65,Unlimited N/A, +100791,epipha,The Epiphany of Hardcore,SOTUI,Pick-Up (New + Revival),Easy N/A,Standard 15,Hard 30,Master 70,Unlimited N/A, +100792,thekin,The King of Pirates,RiraN,Pick-Up (New + Revival),Easy N/A,Standard 22,Hard 52,Master 75,Unlimited N/A, +100793,timele,Timeless encode,Vice Principal,Pick-Up (New + Revival),Easy N/A,Standard 16,Hard 33,Master 72,Unlimited N/A, +100794,tokyoo,tokyo,Headphone-Tokyo(star)(star) feat.nayuta,Pick-Up (New + Revival),Easy N/A,Standard 15,Hard 33,Master 71,Unlimited N/A, +100795,toooma,Tooo Many,S3RL,Pick-Up (New + Revival),Easy N/A,Standard 30,Hard 51,Master 77,Unlimited N/A, +100796,toucho2,Touch Of Gold (Bongo Mango Remix),Togo Project feat. Frances Maya,Pick-Up (New + Revival),Easy N/A,Standard 17,Hard 32,Master 52,Unlimited 78, +100797,tayuta,tΔyutΔi,ミフメイ,Pick-Up (New + Revival),Easy N/A,Standard 26,Hard 35,Master 72,Unlimited N/A, +100798,ultrix,ULTRiX,sky_delta,Pick-Up (New + Revival),Easy N/A,Standard 27,Hard 45,Master 76,Unlimited N/A, +100799,underw,Underworld,ANOTHER STORY,Pick-Up (New + Revival),Easy N/A,Standard 29,Hard 46,Master 69,Unlimited 86, +100800,virtua,Virtual Reality Controller,フラット3rd,Pick-Up (New + Revival),Easy N/A,Standard 15,Hard 35,Master 63,Unlimited N/A, +100801,voiceo2,VOICE OF HOUSE (96TH RETROMAN REMIX),DOT96,Pick-Up (New + Revival),Easy N/A,Standard 14,Hard 38,Master 67,Unlimited N/A, +100802,wannab2,Wanna Be Your Special (Remix ver.),Shoichiro Hirata feat. SUIMI,Pick-Up (New + Revival),Easy N/A,Standard 26,Hard 41,Master 69,Unlimited N/A, +100803,wiwwtw2,What Is Wrong With The World (Cross Edit),SADA,Pick-Up (New + Revival),Easy N/A,Standard 20,Hard 43,Master 67,Unlimited 72, +100804,wingso,Wings of Twilight,sky_delta,Pick-Up (New + Revival),Easy N/A,Standard 20,Hard 53,Master 71,Unlimited N/A, +100805,winter,Winter again,GLAY,Pick-Up (New + Revival),Easy N/A,Standard 14,Hard 24,Master 41,Unlimited N/A, +100806,iineee,いいね!,BABYMETAL,Pick-Up (New + Revival),Easy N/A,Standard 21,Hard 40,Master 81,Unlimited N/A, +100807,illumi,イルミナレガロ,Headphone-Tokyo(star)(star) feat.MiLO,Pick-Up (New + Revival),Easy N/A,Standard 10,Hard 25,Master 46,Unlimited 63, +100808,yellll,エール,FullMooN,Pick-Up (New + Revival),Easy N/A,Standard 8,Hard 17,Master 52,Unlimited N/A, +100809,eschat,エスカトロジィ,MozSound,Pick-Up (New + Revival),Easy N/A,Standard 36,Hard 57,Master 77,Unlimited N/A, +100810,counte,カウンターストップ,フラット3rd,Pick-Up (New + Revival),Easy N/A,Standard 29,Hard 34,Master 71,Unlimited N/A, +100811,gimcho,ギミチョコ!!,BABYMETAL,Pick-Up (New + Revival),Easy N/A,Standard 18,Hard 39,Master 70,Unlimited N/A, +100812,surviv,サバイバル,GLAY,Pick-Up (New + Revival),Easy N/A,Standard 24,Hard 40,Master 65,Unlimited N/A, +100814,turkis3,トルコ行進曲 (Short Remix),,Pick-Up (New + Revival),Easy N/A,Standard 6,Hard 20,Master 48,Unlimited N/A, +100815,picora2,ピコラセテ (Instrumental Ver.),TORIENA,Pick-Up (New + Revival),Easy N/A,Standard 28,Hard 53,Master 80,Unlimited N/A, +100816,fortis,フォルテシモ,らむだーじゃん,Pick-Up (New + Revival),Easy N/A,Standard 20,Hard 37,Master 53,Unlimited N/A, +100817,hedban,ヘドバンギャー!!,BABYMETAL,Pick-Up (New + Revival),Easy N/A,Standard 16,Hard 43,Master 66,Unlimited N/A, +100818,megitu,メギツネ,BABYMETAL,Pick-Up (New + Revival),Easy N/A,Standard 15,Hard 30,Master 49,Unlimited N/A, +100819,rockma,ロックマン (CUTMAN STAGE),Remixed by てつ×ねこ,Pick-Up (New + Revival),Easy N/A,Standard 27,Hard 48,Master 73,Unlimited N/A, +100820,kounen2,光年(konen)-Remix Ver.-,小野秀幸,Pick-Up (New + Revival),Easy N/A,Standard 21,Hard 43,Master 73,Unlimited N/A, +100821,saisyu,最終回STORY,MY FIRST STORY,Pick-Up (New + Revival),Easy N/A,Standard 18,Hard 36,Master 56,Unlimited N/A, +100822,yuukan,勇敢 i tout,kamejack,Pick-Up (New + Revival),Easy N/A,Standard 22,Hard 33,Master 78,Unlimited N/A, +100823,modern,彼女の“Modern…” CROSS×BEATS Remix,GLAY,Pick-Up (New + Revival),Easy N/A,Standard 20,Hard 32,Master 56,Unlimited N/A, +100824,miraie,未来へのプレリュード,カヒーナムジカ,Pick-Up (New + Revival),Easy N/A,Standard 21,Hard 35,Master 66,Unlimited N/A, +100825,ranfes,狂乱セレブレーション,Yamajet,Pick-Up (New + Revival),Easy N/A,Standard 20,Hard 42,Master 65,Unlimited N/A, +100826,nemure,眠れない歌,iru,Pick-Up (New + Revival),Easy N/A,Standard 15,Hard 38,Master 67,Unlimited 76, +100827,yuwaku,誘惑,GLAY,Pick-Up (New + Revival),Easy N/A,Standard 15,Hard 26,Master 43,Unlimited N/A, +100828,dontst,Don't Stop The Music feat.森高千里,tofubeats,Pick-Up (New + Revival),Easy N/A,Standard 15,Hard 32,Master 56,Unlimited 70, +100829,mottai,もったいないとらんど,きゃりーぱみゅぱみゅ,Pick-Up (New + Revival),Easy N/A,Standard 10,Hard 26,Master 36,Unlimited N/A, +100830,slysly,SLY,RIP SLYME,Pick-Up (New + Revival),Easy N/A,Standard 10,Hard 33,Master 58,Unlimited N/A, +100831,lookam,(Where's)THE SILENT MAJORITY?,高橋優,Pick-Up (New + Revival),Easy N/A,Standard 17,Hard 34,Master 67,Unlimited N/A, +100832,feverr,フィーバー,パスピエ,Pick-Up (New + Revival),Easy N/A,Standard 28,Hard 48,Master 68,Unlimited N/A, +100833,fashio,ファッションモンスター,きゃりーぱみゅぱみゅ,Pick-Up (New + Revival),Easy N/A,Standard 8,Hard 24,Master 39,Unlimited N/A, +100834,hagito,「ハギとこ!」のテーマ,ハギー,Pick-Up (New + Revival),Easy N/A,Standard 12,Hard 26,Master 50,Unlimited N/A, +100835,invade,インベーダーインベーダー,きゃりーぱみゅぱみゅ,Pick-Up (New + Revival),Easy N/A,Standard 10,Hard 28,Master 47,Unlimited N/A, +100836,ainoch,愛の地球祭,チームしゃちほこ,Pick-Up (New + Revival),Easy N/A,Standard 17,Hard 40,Master 59,Unlimited N/A, +100837,nakama,仲間を探したい,神聖かまってちゃん,Pick-Up (New + Revival),Easy N/A,Standard 14,Hard 32,Master 53,Unlimited N/A, +100838,ninjar,にんじゃりばんばん,きゃりーぱみゅぱみゅ,Pick-Up (New + Revival),Easy N/A,Standard 8,Hard 23,Master 41,Unlimited 65, +100839,parall,パラレルスペック,ゲスの極み乙女。,Pick-Up (New + Revival),Easy N/A,Standard 14,Hard 35,Master 61,Unlimited N/A, +100840,yukifu,雪降る夜にキスして,バンドじゃないもん!,Pick-Up (New + Revival),Easy N/A,Standard 13,Hard 29,Master 51,Unlimited N/A, +100841,furiso,ふりそでーしょん,きゃりーぱみゅぱみゅ,Pick-Up (New + Revival),Easy N/A,Standard 12,Hard 24,Master 44,Unlimited 74, +100842,honeyj,HONEY♡SUNRiSE ~jun Side~,jun with Aimee,Original,Easy 24,Standard 32,Hard 63,Master 88,Unlimited 93, +100843,emeraj,EMERALD♡KISS ~jun Side~,jun with Aimee,Original,Easy 19,Standard 30,Hard 53,Master 85,Unlimited N/A, +100844,dazzlo,DAZZLING♡SEASON (Original Side),jun,Original,Easy 16,Standard 35,Hard 60,Master 80,Unlimited 90, +100844,shares,SHARE SONG,SHARE SONG,Original,Easy N/A,Standard N/A,Hard N/A,Master N/A,Unlimited N/A, From 2f1728b64db5ca6c0d4d2bb9973368e003dd03b4 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Thu, 9 Mar 2023 10:35:58 -0500 Subject: [PATCH 23/63] cardmaker: simplify handle_get_game_connect_api_request, add develop mode check --- titles/cm/base.py | 5 ++++- titles/cm/cm136.py | 32 +++++++++----------------------- 2 files changed, 13 insertions(+), 24 deletions(-) diff --git a/titles/cm/base.py b/titles/cm/base.py index c22fa41..636e0ee 100644 --- a/titles/cm/base.py +++ b/titles/cm/base.py @@ -22,7 +22,10 @@ class CardMakerBase(): self.version = CardMakerConstants.VER_CARD_MAKER def handle_get_game_connect_api_request(self, data: Dict) -> Dict: - uri = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}" + if self.core_cfg.server.is_develop: + uri = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}" + else: + f"http://{self.core_cfg.title.hostname}" # CHUNITHM = 0, maimai = 1, ONGEKI = 2 return { diff --git a/titles/cm/cm136.py b/titles/cm/cm136.py index 2859c24..2298dc8 100644 --- a/titles/cm/cm136.py +++ b/titles/cm/cm136.py @@ -17,29 +17,15 @@ class CardMaker136(CardMakerBase): self.version = CardMakerConstants.VER_CARD_MAKER_136 def handle_get_game_connect_api_request(self, data: Dict) -> Dict: - uri = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}" - - # CHUNITHM = 0, maimai = 1, ONGEKI = 2 - return { - "length": 3, - "gameConnectList": [ - { - "modelKind": 0, - "type": 1, - "titleUri": f"{uri}/SDHD/205/" - }, - { - "modelKind": 1, - "type": 1, - "titleUri": f"{uri}/SDEZ/125/" - }, - { - "modelKind": 2, - "type": 1, - "titleUri": f"{uri}/SDDT/135/" - } - ] - } + ret = super().handle_get_game_connect_api_request(data) + if self.core_cfg.server.is_develop: + uri = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}" + else: + uri = f"http://{self.core_cfg.title.hostname}" + + ret["gameConnectList"][0]["titleUri"] = f"{uri}/SDHD/205/" + ret["gameConnectList"][1]["titleUri"] = f"{uri}/SDEZ/125/" + ret["gameConnectList"][2]["titleUri"] = f"{uri}/SDDT/135/" def handle_get_game_setting_api_request(self, data: Dict) -> Dict: ret = super().handle_get_game_setting_api_request(data) From 6dcd7b67ef9369b6f9672a8233ca7b1dea838ec6 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Thu, 9 Mar 2023 10:37:29 -0500 Subject: [PATCH 24/63] cm: hotfix for handle_get_game_connect_api_request --- titles/cm/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/cm/base.py b/titles/cm/base.py index 636e0ee..b5d65b7 100644 --- a/titles/cm/base.py +++ b/titles/cm/base.py @@ -25,7 +25,7 @@ class CardMakerBase(): if self.core_cfg.server.is_develop: uri = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}" else: - f"http://{self.core_cfg.title.hostname}" + uri = f"http://{self.core_cfg.title.hostname}" # CHUNITHM = 0, maimai = 1, ONGEKI = 2 return { From c8d4bc6109e3263bddc4c8e4af59c0c382935532 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Thu, 9 Mar 2023 10:52:49 -0500 Subject: [PATCH 25/63] add special-case ping handlers to mai2, ongeki and chuni --- titles/chuni/base.py | 3 --- titles/chuni/index.py | 3 +++ titles/mai2/base.py | 3 --- titles/mai2/index.py | 3 +++ titles/ongeki/index.py | 3 +++ 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/titles/chuni/base.py b/titles/chuni/base.py index d557583..65d991d 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -21,9 +21,6 @@ class ChuniBase(): self.game = ChuniConstants.GAME_CODE self.version = ChuniConstants.VER_CHUNITHM - def handle_ping_request(self, data: Dict) -> Dict: - return {"returnCode": 1} - def handle_game_login_api_request(self, data: Dict) -> Dict: #self.data.base.log_event("chuni", "login", logging.INFO, {"version": self.version, "user": data["userId"]}) return { "returnCode": 1 } diff --git a/titles/chuni/index.py b/titles/chuni/index.py index 866e9d9..66e8c65 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -86,6 +86,9 @@ class ChuniServlet(): return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", "") def render_POST(self, request: Request, version: int, url_path: str) -> bytes: + if url_path == "/ping": + return zlib.compress(json.dumps({'returnCode': 1}, ensure_ascii=False).encode("utf-8")) + req_raw = request.content.getvalue() url_split = url_path.split("/") encrtped = False diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 3817890..ea98681 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -16,9 +16,6 @@ class Mai2Base(): self.data = Mai2Data(cfg) self.logger = logging.getLogger("mai2") - def handle_ping_request(self, data: Dict) -> Dict: - return {"returnCode": 1} - def handle_get_game_setting_api_request(self, data: Dict): reboot_start = date.strftime(datetime.now() + timedelta(hours=3), Mai2Constants.DATE_TIME_FORMAT) reboot_end = date.strftime(datetime.now() + timedelta(hours=4), Mai2Constants.DATE_TIME_FORMAT) diff --git a/titles/mai2/index.py b/titles/mai2/index.py index 64a38a6..051cba0 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -69,6 +69,9 @@ class Mai2Servlet(): return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", f"{core_cfg.title.hostname}/") def render_POST(self, request: Request, version: int, url_path: str) -> bytes: + if url_path == "/ping": + return zlib.compress(json.dumps({'returnCode': 1}, ensure_ascii=False).encode("utf-8")) + req_raw = request.content.getvalue() url = request.uri.decode() url_split = url_path.split("/") diff --git a/titles/ongeki/index.py b/titles/ongeki/index.py index c5e78f6..c3d4b26 100644 --- a/titles/ongeki/index.py +++ b/titles/ongeki/index.py @@ -72,6 +72,9 @@ class OngekiServlet(): return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", f"{core_cfg.title.hostname}/") def render_POST(self, request: Request, version: int, url_path: str) -> bytes: + if url_path == "/ping": + return zlib.compress(json.dumps({'returnCode': 1}, ensure_ascii=False).encode("utf-8")) + req_raw = request.content.getvalue() url_split = url_path.split("/") internal_ver = 0 From 6761915a3fc2484da06c7d66e2c2d23d3285e062 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Thu, 9 Mar 2023 10:56:30 -0500 Subject: [PATCH 26/63] add .lower() to ping requsts --- titles/chuni/index.py | 2 +- titles/mai2/index.py | 2 +- titles/ongeki/index.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/titles/chuni/index.py b/titles/chuni/index.py index 66e8c65..ce6bda9 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -86,7 +86,7 @@ class ChuniServlet(): return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", "") def render_POST(self, request: Request, version: int, url_path: str) -> bytes: - if url_path == "/ping": + if url_path.lower() == "/ping": return zlib.compress(json.dumps({'returnCode': 1}, ensure_ascii=False).encode("utf-8")) req_raw = request.content.getvalue() diff --git a/titles/mai2/index.py b/titles/mai2/index.py index 051cba0..80b3ac8 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -69,7 +69,7 @@ class Mai2Servlet(): return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", f"{core_cfg.title.hostname}/") def render_POST(self, request: Request, version: int, url_path: str) -> bytes: - if url_path == "/ping": + if url_path.lower() == "/ping": return zlib.compress(json.dumps({'returnCode': 1}, ensure_ascii=False).encode("utf-8")) req_raw = request.content.getvalue() diff --git a/titles/ongeki/index.py b/titles/ongeki/index.py index c3d4b26..2bbdb46 100644 --- a/titles/ongeki/index.py +++ b/titles/ongeki/index.py @@ -72,7 +72,7 @@ class OngekiServlet(): return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", f"{core_cfg.title.hostname}/") def render_POST(self, request: Request, version: int, url_path: str) -> bytes: - if url_path == "/ping": + if url_path.lower() == "/ping": return zlib.compress(json.dumps({'returnCode': 1}, ensure_ascii=False).encode("utf-8")) req_raw = request.content.getvalue() From fa7206848cc1165ee1da1ab846be5e1c9a40dc21 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Thu, 9 Mar 2023 11:29:36 -0500 Subject: [PATCH 27/63] general code cleanup for multiple games --- titles/chuni/index.py | 28 +++++++++++++++------------- titles/cm/index.py | 13 ++++++------- titles/mai2/index.py | 14 +++++++------- titles/ongeki/index.py | 15 ++++++++------- titles/wacca/index.py | 22 +++++++++++----------- 5 files changed, 47 insertions(+), 45 deletions(-) diff --git a/titles/chuni/index.py b/titles/chuni/index.py index ce6bda9..5ee03a4 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -87,7 +87,7 @@ class ChuniServlet(): def render_POST(self, request: Request, version: int, url_path: str) -> bytes: if url_path.lower() == "/ping": - return zlib.compress(json.dumps({'returnCode': 1}, ensure_ascii=False).encode("utf-8")) + return zlib.compress(b'{"returnCode": "1"}') req_raw = request.content.getvalue() url_split = url_path.split("/") @@ -138,13 +138,13 @@ class ChuniServlet(): except: self.logger.error(f"Failed to decrypt v{version} request to {endpoint} -> {req_raw}") - return zlib.compress("{\"stat\": \"0\"}".encode("utf-8")) + return zlib.compress(b'{"stat": "0"}') encrtped = True if not encrtped and self.game_cfg.crypto.encrypted_only: self.logger.error(f"Unencrypted v{version} {endpoint} request, but config is set to encrypted only: {req_raw}") - return zlib.compress("{\"stat\": \"0\"}".encode("utf-8")) + return zlib.compress(b'{"stat": "0"}') try: unzip = zlib.decompress(req_raw) @@ -160,17 +160,19 @@ class ChuniServlet(): func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" - try: - handler = getattr(self.versions[internal_ver], func_to_find) - resp = handler(req_data) - - except AttributeError as e: - self.logger.warning(f"Unhandled v{version} request {endpoint} - {e}") - return zlib.compress("{\"stat\": \"0\"}".encode("utf-8")) + if not hasattr(self.versions[internal_ver], func_to_find): + self.logger.warning(f"Unhandled v{version} request {endpoint}") + resp = {"returnCode": 1} + + else: - except Exception as e: - self.logger.error(f"Error handling v{version} method {endpoint} - {e}") - return zlib.compress("{\"stat\": \"0\"}".encode("utf-8")) + try: + handler = getattr(self.versions[internal_ver], func_to_find) + resp = handler(req_data) + + except Exception as e: + self.logger.error(f"Error handling v{version} method {endpoint} - {e}") + return zlib.compress(b'{"stat": "0"}') if resp == None: resp = {'returnCode': 1} diff --git a/titles/cm/index.py b/titles/cm/index.py index b2751b0..ff9ea1f 100644 --- a/titles/cm/index.py +++ b/titles/cm/index.py @@ -87,7 +87,7 @@ class CardMakerServlet(): except zlib.error as e: self.logger.error( f"Failed to decompress v{version} {endpoint} request -> {e}") - return zlib.compress("{\"stat\": \"0\"}".encode("utf-8")) + return zlib.compress(b'{"stat": "0"}') req_data = json.loads(unzip) @@ -95,19 +95,18 @@ class CardMakerServlet(): func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" + if not hasattr(self.versions[internal_ver], func_to_find): + self.logger.warning(f"Unhandled v{version} request {endpoint}") + return zlib.compress(b'{"returnCode": 1}') + try: handler = getattr(self.versions[internal_ver], func_to_find) resp = handler(req_data) - except AttributeError as e: - self.logger.warning( - f"Unhandled v{version} request {endpoint} - {e}") - return zlib.compress("{\"stat\": \"0\"}".encode("utf-8")) - except Exception as e: self.logger.error( f"Error handling v{version} method {endpoint} - {e}") - return zlib.compress("{\"stat\": \"0\"}".encode("utf-8")) + return zlib.compress(b'{"stat": "0"}') if resp is None: resp = {'returnCode': 1} diff --git a/titles/mai2/index.py b/titles/mai2/index.py index 80b3ac8..305e389 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -70,7 +70,7 @@ class Mai2Servlet(): def render_POST(self, request: Request, version: int, url_path: str) -> bytes: if url_path.lower() == "/ping": - return zlib.compress(json.dumps({'returnCode': 1}, ensure_ascii=False).encode("utf-8")) + return zlib.compress(b'{"returnCode": "1"}') req_raw = request.content.getvalue() url = request.uri.decode() @@ -102,7 +102,7 @@ class Mai2Servlet(): except zlib.error as e: self.logger.error(f"Failed to decompress v{version} {endpoint} request -> {e}") - return zlib.compress("{\"stat\": \"0\"}".encode("utf-8")) + return zlib.compress(b'{"stat": "0"}') req_data = json.loads(unzip) @@ -110,17 +110,17 @@ class Mai2Servlet(): func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" + if not hasattr(self.versions[internal_ver], func_to_find): + self.logger.warning(f"Unhandled v{version} request {endpoint}") + return zlib.compress(b'{"returnCode": 1}') + try: handler = getattr(self.versions[internal_ver], func_to_find) resp = handler(req_data) - - except AttributeError as e: - self.logger.warning(f"Unhandled v{version} request {endpoint} - {e}") - return zlib.compress("{\"stat\": \"0\"}".encode("utf-8")) except Exception as e: self.logger.error(f"Error handling v{version} method {endpoint} - {e}") - return zlib.compress("{\"stat\": \"0\"}".encode("utf-8")) + return zlib.compress(b'{"stat": "0"}') if resp == None: resp = {'returnCode': 1} diff --git a/titles/ongeki/index.py b/titles/ongeki/index.py index 2bbdb46..09ab18c 100644 --- a/titles/ongeki/index.py +++ b/titles/ongeki/index.py @@ -73,7 +73,7 @@ class OngekiServlet(): def render_POST(self, request: Request, version: int, url_path: str) -> bytes: if url_path.lower() == "/ping": - return zlib.compress(json.dumps({'returnCode': 1}, ensure_ascii=False).encode("utf-8")) + return zlib.compress(b'{"returnCode": 1}') req_raw = request.content.getvalue() url_split = url_path.split("/") @@ -102,13 +102,14 @@ class OngekiServlet(): # doing encrypted. The likelyhood of false positives is low but # technically not 0 self.logger.error("Encryption not supported at this time") + return b"" try: unzip = zlib.decompress(req_raw) except zlib.error as e: self.logger.error(f"Failed to decompress v{version} {endpoint} request -> {e}") - return zlib.compress("{\"stat\": \"0\"}".encode("utf-8")) + return zlib.compress(b'{"stat": "0"}') req_data = json.loads(unzip) @@ -116,17 +117,17 @@ class OngekiServlet(): func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" + if not hasattr(self.versions[internal_ver], func_to_find): + self.logger.warning(f"Unhandled v{version} request {endpoint}") + return zlib.compress(b'{"returnCode": 1}') + try: handler = getattr(self.versions[internal_ver], func_to_find) resp = handler(req_data) - - except AttributeError as e: - self.logger.warning(f"Unhandled v{version} request {endpoint} - {e}") - return zlib.compress("{\"stat\": \"0\"}".encode("utf-8")) except Exception as e: self.logger.error(f"Error handling v{version} method {endpoint} - {e}") - return zlib.compress("{\"stat\": \"0\"}".encode("utf-8")) + return zlib.compress(b'{"stat": "0"}') if resp == None: resp = {'returnCode': 1} diff --git a/titles/wacca/index.py b/titles/wacca/index.py index e2f9758..b4a4aea 100644 --- a/titles/wacca/index.py +++ b/titles/wacca/index.py @@ -117,24 +117,24 @@ class WaccaServlet(): self.logger.info(f"v{req_json['appVersion']} {url_path} request from {request.getClientAddress().host} with chipId {req_json['chipId']}") self.logger.debug(req_json) + if not hasattr(self.versions[internal_ver], func_to_find): + self.logger.warn(f"{req_json['appVersion']} has no handler for {func_to_find}") + resp = BaseResponse().make() + return end(resp) + try: handler = getattr(self.versions[internal_ver], func_to_find) - if handler is not None: - resp = handler(req_json) - - else: - self.logger.warn(f"{req_json['appVersion']} has no handler for {func_to_find}") - resp = None - - if resp is None: - resp = BaseResponse().make() + resp = handler(req_json) self.logger.debug(f"{req_json['appVersion']} response {resp}") return end(resp) except Exception as e: self.logger.error(f"{req_json['appVersion']} Error handling method {url_path} -> {e}") - if self.game_cfg.server.loglevel <= logging.DEBUG: + if self.core_cfg.server.is_develop: raise - resp = BaseResponse().make() + + resp = BaseResponse() + resp.status = 1 + resp.message = "A server error occoured." return end(resp) From a76bb94eb15dbd14f6ac0bd4c63d19ef461b4ecb Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Thu, 9 Mar 2023 11:38:58 -0500 Subject: [PATCH 28/63] let black do it's magic --- core/__init__.py | 2 +- core/aimedb.py | 167 +++++-- core/allnet.py | 188 ++++--- core/config.py | 215 +++++--- core/const.py | 11 +- core/data/__init__.py | 2 +- core/data/cache.py | 14 +- core/data/database.py | 137 ++++-- core/data/schema/__init__.py | 2 +- core/data/schema/arcade.py | 207 +++++--- core/data/schema/base.py | 62 ++- core/data/schema/card.py | 45 +- core/data/schema/user.py | 67 +-- core/frontend.py | 117 +++-- core/mucha.py | 106 ++-- core/title.py | 53 +- core/utils.py | 3 +- dbutils.py | 26 +- index.py | 174 +++++-- read.py | 72 +-- titles/chuni/air.py | 7 +- titles/chuni/airplus.py | 7 +- titles/chuni/amazon.py | 5 +- titles/chuni/amazonplus.py | 5 +- titles/chuni/base.py | 340 +++++++------ titles/chuni/config.py | 35 +- titles/chuni/const.py | 21 +- titles/chuni/crystal.py | 5 +- titles/chuni/crystalplus.py | 5 +- titles/chuni/database.py | 3 +- titles/chuni/index.py | 122 +++-- titles/chuni/new.py | 75 +-- titles/chuni/newplus.py | 19 +- titles/chuni/paradise.py | 5 +- titles/chuni/plus.py | 7 +- titles/chuni/read.py | 153 +++--- titles/chuni/schema/__init__.py | 2 +- titles/chuni/schema/item.py | 112 +++-- titles/chuni/schema/profile.py | 249 ++++++---- titles/chuni/schema/score.py | 49 +- titles/chuni/schema/static.py | 222 +++++---- titles/chuni/star.py | 7 +- titles/chuni/starplus.py | 7 +- titles/cm/base.py | 44 +- titles/cm/cm136.py | 2 +- titles/cm/config.py | 12 +- titles/cm/const.py | 2 +- titles/cm/index.py | 43 +- titles/cm/read.py | 47 +- titles/cxb/__init__.py | 2 +- titles/cxb/base.py | 625 ++++++++++++++---------- titles/cxb/config.py | 42 +- titles/cxb/const.py | 11 +- titles/cxb/database.py | 2 +- titles/cxb/index.py | 109 +++-- titles/cxb/read.py | 81 +++- titles/cxb/rev.py | 276 ++++++----- titles/cxb/rss1.py | 287 ++++++----- titles/cxb/rss2.py | 299 +++++++----- titles/cxb/schema/item.py | 31 +- titles/cxb/schema/profile.py | 56 ++- titles/cxb/schema/score.py | 97 ++-- titles/cxb/schema/static.py | 83 ++-- titles/diva/__init__.py | 2 +- titles/diva/base.py | 498 ++++++++++++------- titles/diva/config.py | 22 +- titles/diva/const.py | 4 +- titles/diva/database.py | 10 +- titles/diva/index.py | 99 ++-- titles/diva/read.py | 214 +++++--- titles/diva/schema/__init__.py | 12 +- titles/diva/schema/customize.py | 29 +- titles/diva/schema/item.py | 32 +- titles/diva/schema/module.py | 23 +- titles/diva/schema/profile.py | 55 ++- titles/diva/schema/pv_customize.py | 47 +- titles/diva/schema/score.py | 117 +++-- titles/diva/schema/static.py | 261 ++++++---- titles/mai2/__init__.py | 2 +- titles/mai2/base.py | 384 +++++++++------ titles/mai2/config.py | 18 +- titles/mai2/const.py | 32 +- titles/mai2/database.py | 10 +- titles/mai2/index.py | 82 ++-- titles/mai2/plus.py | 3 +- titles/mai2/read.py | 98 ++-- titles/mai2/schema/__init__.py | 2 +- titles/mai2/schema/item.py | 212 +++++--- titles/mai2/schema/profile.py | 158 ++++-- titles/mai2/schema/score.py | 62 ++- titles/mai2/schema/static.py | 194 ++++---- titles/mai2/splash.py | 3 +- titles/mai2/splashplus.py | 3 +- titles/mai2/universe.py | 3 +- titles/mai2/universeplus.py | 3 +- titles/ongeki/__init__.py | 2 +- titles/ongeki/base.py | 456 ++++++++++------- titles/ongeki/bright.py | 193 ++++---- titles/ongeki/brightmemory.py | 105 +++- titles/ongeki/config.py | 18 +- titles/ongeki/const.py | 61 ++- titles/ongeki/database.py | 3 +- titles/ongeki/index.py | 92 ++-- titles/ongeki/plus.py | 3 +- titles/ongeki/read.py | 104 ++-- titles/ongeki/red.py | 3 +- titles/ongeki/redplus.py | 3 +- titles/ongeki/schema/__init__.py | 8 +- titles/ongeki/schema/item.py | 201 ++++---- titles/ongeki/schema/log.py | 32 +- titles/ongeki/schema/profile.py | 246 ++++++---- titles/ongeki/schema/score.py | 44 +- titles/ongeki/schema/static.py | 184 +++---- titles/ongeki/summer.py | 3 +- titles/ongeki/summerplus.py | 3 +- titles/pokken/__init__.py | 2 +- titles/pokken/base.py | 27 +- titles/pokken/config.py | 52 +- titles/pokken/const.py | 6 +- titles/pokken/database.py | 3 +- titles/pokken/index.py | 91 ++-- titles/pokken/proto/jackal_pb2.py | 244 +++++----- titles/wacca/__init__.py | 2 +- titles/wacca/base.py | 701 ++++++++++++++++++--------- titles/wacca/config.py | 48 +- titles/wacca/const.py | 156 +++--- titles/wacca/database.py | 3 +- titles/wacca/frontend.py | 13 +- titles/wacca/handlers/__init__.py | 2 +- titles/wacca/handlers/advertise.py | 31 +- titles/wacca/handlers/base.py | 8 +- titles/wacca/handlers/helpers.py | 421 ++++++++++------ titles/wacca/handlers/housing.py | 85 +++- titles/wacca/handlers/user_info.py | 14 +- titles/wacca/handlers/user_misc.py | 16 +- titles/wacca/handlers/user_music.py | 39 +- titles/wacca/handlers/user_status.py | 117 +++-- titles/wacca/handlers/user_trial.py | 12 +- titles/wacca/handlers/user_vip.py | 22 +- titles/wacca/index.py | 75 ++- titles/wacca/lily.py | 255 +++++++--- titles/wacca/lilyr.py | 39 +- titles/wacca/read.py | 97 +++- titles/wacca/reverse.py | 156 ++++-- titles/wacca/s.py | 5 +- titles/wacca/schema/__init__.py | 2 +- titles/wacca/schema/item.py | 164 ++++--- titles/wacca/schema/profile.py | 419 +++++++++------- titles/wacca/schema/score.py | 181 ++++--- titles/wacca/schema/static.py | 68 ++- 150 files changed, 8474 insertions(+), 4843 deletions(-) diff --git a/core/__init__.py b/core/__init__.py index 717de33..185d9bc 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -4,4 +4,4 @@ from core.aimedb import AimedbFactory from core.title import TitleServlet from core.utils import Utils from core.mucha import MuchaServlet -from core.frontend import FrontendServlet \ No newline at end of file +from core.frontend import FrontendServlet diff --git a/core/aimedb.py b/core/aimedb.py index 78ce350..64bac8d 100644 --- a/core/aimedb.py +++ b/core/aimedb.py @@ -8,17 +8,18 @@ from logging.handlers import TimedRotatingFileHandler from core.config import CoreConfig from core.data import Data + class AimedbProtocol(Protocol): AIMEDB_RESPONSE_CODES = { "felica_lookup": 0x03, "lookup": 0x06, - "log": 0x0a, - "campaign": 0x0c, - "touch": 0x0e, + "log": 0x0A, + "campaign": 0x0C, + "touch": 0x0E, "lookup2": 0x10, "felica_lookup2": 0x12, "log2": 0x14, - "hello": 0x65 + "hello": 0x65, } request_list: Dict[int, Any] = {} @@ -30,14 +31,14 @@ class AimedbProtocol(Protocol): if core_cfg.aimedb.key == "": self.logger.error("!!!KEY NOT SET!!!") exit(1) - + self.request_list[0x01] = self.handle_felica_lookup self.request_list[0x04] = self.handle_lookup self.request_list[0x05] = self.handle_register - self.request_list[0x09] = self.handle_log - self.request_list[0x0b] = self.handle_campaign - self.request_list[0x0d] = self.handle_touch - self.request_list[0x0f] = self.handle_lookup2 + self.request_list[0x09] = self.handle_log + self.request_list[0x0B] = self.handle_campaign + self.request_list[0x0D] = self.handle_touch + self.request_list[0x0F] = self.handle_lookup2 self.request_list[0x11] = self.handle_felica_lookup2 self.request_list[0x13] = self.handle_log2 self.request_list[0x64] = self.handle_hello @@ -53,8 +54,10 @@ class AimedbProtocol(Protocol): self.logger.debug(f"{self.transport.getPeer().host} Connected") def connectionLost(self, reason) -> None: - self.logger.debug(f"{self.transport.getPeer().host} Disconnected - {reason.value}") - + self.logger.debug( + f"{self.transport.getPeer().host} Disconnected - {reason.value}" + ) + def dataReceived(self, data: bytes) -> None: cipher = AES.new(self.config.aimedb.key.encode(), AES.MODE_ECB) @@ -66,7 +69,7 @@ class AimedbProtocol(Protocol): self.logger.debug(f"{self.transport.getPeer().host} wrote {decrypted.hex()}") - if not decrypted[1] == 0xa1 and not decrypted[0] == 0x3e: + if not decrypted[1] == 0xA1 and not decrypted[0] == 0x3E: self.logger.error(f"Bad magic") return None @@ -90,30 +93,46 @@ class AimedbProtocol(Protocol): except ValueError as e: self.logger.error(f"Failed to encrypt {resp.hex()} because {e}") return None - + def handle_campaign(self, data: bytes) -> bytes: self.logger.info(f"campaign from {self.transport.getPeer().host}") - ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["campaign"], 0x0200, 0x0001) + ret = struct.pack( + "<5H", + 0xA13E, + 0x3087, + self.AIMEDB_RESPONSE_CODES["campaign"], + 0x0200, + 0x0001, + ) return self.append_padding(ret) - + def handle_hello(self, data: bytes) -> bytes: self.logger.info(f"hello from {self.transport.getPeer().host}") - ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["hello"], 0x0020, 0x0001) + ret = struct.pack( + "<5H", 0xA13E, 0x3087, self.AIMEDB_RESPONSE_CODES["hello"], 0x0020, 0x0001 + ) return self.append_padding(ret) def handle_lookup(self, data: bytes) -> bytes: - luid = data[0x20: 0x2a].hex() + luid = data[0x20:0x2A].hex() user_id = self.data.card.get_user_id_from_card(access_code=luid) - if user_id is None: user_id = -1 + if user_id is None: + user_id = -1 - self.logger.info(f"lookup from {self.transport.getPeer().host}: luid {luid} -> user_id {user_id}") + self.logger.info( + f"lookup from {self.transport.getPeer().host}: luid {luid} -> user_id {user_id}" + ) - ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["lookup"], 0x0130, 0x0001) + ret = struct.pack( + "<5H", 0xA13E, 0x3087, self.AIMEDB_RESPONSE_CODES["lookup"], 0x0130, 0x0001 + ) ret += bytes(0x20 - len(ret)) - if user_id is None: ret += struct.pack(" bytes: @@ -125,66 +144,98 @@ class AimedbProtocol(Protocol): return bytes(ret) def handle_felica_lookup(self, data: bytes) -> bytes: - idm = data[0x20: 0x28].hex() - pmm = data[0x28: 0x30].hex() + idm = data[0x20:0x28].hex() + pmm = data[0x28:0x30].hex() access_code = self.data.card.to_access_code(idm) - self.logger.info(f"felica_lookup from {self.transport.getPeer().host}: idm {idm} pmm {pmm} -> access_code {access_code}") + self.logger.info( + f"felica_lookup from {self.transport.getPeer().host}: idm {idm} pmm {pmm} -> access_code {access_code}" + ) - ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["felica_lookup"], 0x0030, 0x0001) + ret = struct.pack( + "<5H", + 0xA13E, + 0x3087, + self.AIMEDB_RESPONSE_CODES["felica_lookup"], + 0x0030, + 0x0001, + ) ret += bytes(26) ret += bytes.fromhex(access_code) return self.append_padding(ret) def handle_felica_lookup2(self, data: bytes) -> bytes: - idm = data[0x30: 0x38].hex() - pmm = data[0x38: 0x40].hex() + idm = data[0x30:0x38].hex() + pmm = data[0x38:0x40].hex() access_code = self.data.card.to_access_code(idm) user_id = self.data.card.get_user_id_from_card(access_code=access_code) - if user_id is None: user_id = -1 + if user_id is None: + user_id = -1 - self.logger.info(f"felica_lookup2 from {self.transport.getPeer().host}: idm {idm} ipm {pmm} -> access_code {access_code} user_id {user_id}") + self.logger.info( + f"felica_lookup2 from {self.transport.getPeer().host}: idm {idm} ipm {pmm} -> access_code {access_code} user_id {user_id}" + ) - ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["felica_lookup2"], 0x0140, 0x0001) + ret = struct.pack( + "<5H", + 0xA13E, + 0x3087, + self.AIMEDB_RESPONSE_CODES["felica_lookup2"], + 0x0140, + 0x0001, + ) ret += bytes(22) - ret += struct.pack(" bytes: self.logger.info(f"touch from {self.transport.getPeer().host}") - ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["touch"], 0x0050, 0x0001) + ret = struct.pack( + "<5H", 0xA13E, 0x3087, self.AIMEDB_RESPONSE_CODES["touch"], 0x0050, 0x0001 + ) ret += bytes(5) - ret += struct.pack("<3H", 0x6f, 0, 1) + ret += struct.pack("<3H", 0x6F, 0, 1) return self.append_padding(ret) - def handle_register(self, data: bytes) -> bytes: - luid = data[0x20: 0x2a].hex() + def handle_register(self, data: bytes) -> bytes: + luid = data[0x20:0x2A].hex() if self.config.server.allow_user_registration: user_id = self.data.user.create_user() - if user_id is None: + if user_id is None: user_id = -1 self.logger.error("Failed to register user!") else: card_id = self.data.card.create_card(user_id, luid) - if card_id is None: + if card_id is None: user_id = -1 self.logger.error("Failed to register card!") - self.logger.info(f"register from {self.transport.getPeer().host}: luid {luid} -> user_id {user_id}") - + self.logger.info( + f"register from {self.transport.getPeer().host}: luid {luid} -> user_id {user_id}" + ) + else: - self.logger.info(f"register from {self.transport.getPeer().host} blocked!: luid {luid}") + self.logger.info( + f"register from {self.transport.getPeer().host} blocked!: luid {luid}" + ) user_id = -1 - ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["lookup"], 0x0030, 0x0001 if user_id > -1 else 0) + ret = struct.pack( + "<5H", + 0xA13E, + 0x3087, + self.AIMEDB_RESPONSE_CODES["lookup"], + 0x0030, + 0x0001 if user_id > -1 else 0, + ) ret += bytes(0x20 - len(ret)) ret += struct.pack(" bytes: # TODO: Save aimedb logs self.logger.info(f"log from {self.transport.getPeer().host}") - ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["log"], 0x0020, 0x0001) + ret = struct.pack( + "<5H", 0xA13E, 0x3087, self.AIMEDB_RESPONSE_CODES["log"], 0x0020, 0x0001 + ) return self.append_padding(ret) def handle_log2(self, data: bytes) -> bytes: self.logger.info(f"log2 from {self.transport.getPeer().host}") - ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["log2"], 0x0040, 0x0001) + ret = struct.pack( + "<5H", 0xA13E, 0x3087, self.AIMEDB_RESPONSE_CODES["log2"], 0x0040, 0x0001 + ) ret += bytes(22) ret += struct.pack("H", 1) return self.append_padding(ret) + class AimedbFactory(Factory): protocol = AimedbProtocol + def __init__(self, cfg: CoreConfig) -> None: self.config = cfg log_fmt_str = "[%(asctime)s] Aimedb | %(levelname)s | %(message)s" log_fmt = logging.Formatter(log_fmt_str) self.logger = logging.getLogger("aimedb") - fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.config.server.log_dir, "aimedb"), when="d", backupCount=10) + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.config.server.log_dir, "aimedb"), + when="d", + backupCount=10, + ) fileHandler.setFormatter(log_fmt) - + consoleHandler = logging.StreamHandler() consoleHandler.setFormatter(log_fmt) self.logger.addHandler(fileHandler) self.logger.addHandler(consoleHandler) - + self.logger.setLevel(self.config.aimedb.loglevel) - coloredlogs.install(level=cfg.aimedb.loglevel, logger=self.logger, fmt=log_fmt_str) - + coloredlogs.install( + level=cfg.aimedb.loglevel, logger=self.logger, fmt=log_fmt_str + ) + if self.config.aimedb.key == "": self.logger.error("Please set 'key' field in your config file.") exit(1) self.logger.info(f"Ready on port {self.config.aimedb.port}") - + def buildProtocol(self, addr): return AimedbProtocol(self.config) diff --git a/core/allnet.py b/core/allnet.py index a02c902..4c0c542 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -16,8 +16,9 @@ from core.data import Data from core.utils import Utils from core.const import * + class AllnetServlet: - def __init__(self, core_cfg: CoreConfig, cfg_folder: str): + def __init__(self, core_cfg: CoreConfig, cfg_folder: str): super().__init__() self.config = core_cfg self.config_folder = cfg_folder @@ -27,35 +28,45 @@ class AllnetServlet: self.logger = logging.getLogger("allnet") if not hasattr(self.logger, "initialized"): log_fmt_str = "[%(asctime)s] Allnet | %(levelname)s | %(message)s" - log_fmt = logging.Formatter(log_fmt_str) + log_fmt = logging.Formatter(log_fmt_str) - fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.config.server.log_dir, "allnet"), when="d", backupCount=10) + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.config.server.log_dir, "allnet"), + when="d", + backupCount=10, + ) fileHandler.setFormatter(log_fmt) - + consoleHandler = logging.StreamHandler() consoleHandler.setFormatter(log_fmt) self.logger.addHandler(fileHandler) self.logger.addHandler(consoleHandler) - + self.logger.setLevel(core_cfg.allnet.loglevel) - coloredlogs.install(level=core_cfg.allnet.loglevel, logger=self.logger, fmt=log_fmt_str) + coloredlogs.install( + level=core_cfg.allnet.loglevel, logger=self.logger, fmt=log_fmt_str + ) self.logger.initialized = True plugins = Utils.get_all_titles() if len(plugins) == 0: self.logger.error("No games detected!") - + for _, mod in plugins.items(): if hasattr(mod.index, "get_allnet_info"): for code in mod.game_codes: - enabled, uri, host = mod.index.get_allnet_info(code, self.config, self.config_folder) - + enabled, uri, host = mod.index.get_allnet_info( + code, self.config, self.config_folder + ) + if enabled: self.uri_registry[code] = (uri, host) - self.logger.info(f"Allnet serving {len(self.uri_registry)} games on port {core_cfg.allnet.port}") + self.logger.info( + f"Allnet serving {len(self.uri_registry)} games on port {core_cfg.allnet.port}" + ) def handle_poweron(self, request: Request, _: Dict): request_ip = request.getClientAddress().host @@ -67,14 +78,22 @@ 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.token or not req.serial or not req.ip: - raise AllnetRequestException(f"Bad auth request params from {request_ip} - {vars(req)}") - + if ( + not req.game_id + or not req.ver + or not req.token + or not req.serial + or not req.ip + ): + raise AllnetRequestException( + f"Bad auth request params from {request_ip} - {vars(req)}" + ) + except AllnetRequestException as e: if e.message != "": self.logger.error(e) return b"" - + if req.format_ver == 3: resp = AllnetPowerOnResponse3(req.token) else: @@ -83,26 +102,32 @@ class AllnetServlet: self.logger.debug(f"Allnet request: {vars(req)}") if req.game_id not in self.uri_registry: msg = f"Unrecognised game {req.game_id} attempted allnet auth from {request_ip}." - self.data.base.log_event("allnet", "ALLNET_AUTH_UNKNOWN_GAME", logging.WARN, msg) + self.data.base.log_event( + "allnet", "ALLNET_AUTH_UNKNOWN_GAME", logging.WARN, msg + ) self.logger.warn(msg) resp.stat = 0 return self.dict_to_http_form_string([vars(resp)]) - + resp.uri, resp.host = self.uri_registry[req.game_id] machine = self.data.arcade.get_machine(req.serial) if machine is None and not self.config.server.allow_unregistered_serials: msg = f"Unrecognised serial {req.serial} attempted allnet auth from {request_ip}." - self.data.base.log_event("allnet", "ALLNET_AUTH_UNKNOWN_SERIAL", logging.WARN, msg) + self.data.base.log_event( + "allnet", "ALLNET_AUTH_UNKNOWN_SERIAL", logging.WARN, msg + ) self.logger.warn(msg) resp.stat = 0 return self.dict_to_http_form_string([vars(resp)]) - + if machine is not None: arcade = self.data.arcade.get_arcade(machine["arcade"]) - country = arcade["country"] if machine["country"] is None else machine["country"] + country = ( + arcade["country"] if machine["country"] is None else machine["country"] + ) if country is None: country = AllnetCountryCode.JAPAN.value @@ -111,16 +136,30 @@ class AllnetServlet: resp.allnet_id = machine["id"] resp.name = arcade["name"] if arcade["name"] is not None else "" resp.nickname = arcade["nickname"] if arcade["nickname"] is not None else "" - resp.region0 = arcade["region_id"] if arcade["region_id"] is not None else AllnetJapanRegionId.AICHI.value - resp.region_name0 = arcade["country"] if arcade["country"] is not None else AllnetCountryCode.JAPAN.value - resp.region_name1 = arcade["state"] if arcade["state"] is not None else AllnetJapanRegionId.AICHI.name + resp.region0 = ( + arcade["region_id"] + if arcade["region_id"] is not None + else AllnetJapanRegionId.AICHI.value + ) + resp.region_name0 = ( + arcade["country"] + if arcade["country"] is not None + else AllnetCountryCode.JAPAN.value + ) + resp.region_name1 = ( + arcade["state"] + if arcade["state"] is not None + else AllnetJapanRegionId.AICHI.name + ) resp.region_name2 = arcade["city"] if arcade["city"] is not None else "" - resp.client_timezone = arcade["timezone"] if arcade["timezone"] is not None else "+0900" - + resp.client_timezone = ( + arcade["timezone"] if arcade["timezone"] is not None else "+0900" + ) + int_ver = req.ver.replace(".", "") resp.uri = resp.uri.replace("$v", int_ver) resp.host = resp.host.replace("$v", int_ver) - + msg = f"{req.serial} authenticated from {request_ip}: {req.game_id} v{req.ver}" self.data.base.log_event("allnet", "ALLNET_AUTH_SUCCESS", logging.INFO, msg) self.logger.info(msg) @@ -139,8 +178,10 @@ class AllnetServlet: # 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: - raise AllnetRequestException(f"Bad download request params from {request_ip} - {vars(req)}") - + raise AllnetRequestException( + f"Bad download request params from {request_ip} - {vars(req)}" + ) + except AllnetRequestException as e: if e.message != "": self.logger.error(e) @@ -149,8 +190,8 @@ class AllnetServlet: resp = AllnetDownloadOrderResponse() if not self.config.allnet.allow_online_updates: return self.dict_to_http_form_string([vars(resp)]) - - else: # TODO: Actual dlorder response + + else: # TODO: Actual dlorder response return self.dict_to_http_form_string([vars(resp)]) def handle_billing_request(self, request: Request, _: Dict): @@ -159,10 +200,10 @@ class AllnetServlet: 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()) + rsa = RSA.import_key(open(self.config.billing.signing_key, "rb").read()) signer = PKCS1_v1_5.new(rsa) digest = SHA.new() @@ -178,30 +219,34 @@ class AllnetServlet: machine = self.data.arcade.get_machine(kc_serial) if machine is None and not self.config.server.allow_unregistered_serials: msg = f"Unrecognised serial {kc_serial} attempted billing checkin from {request_ip} for game {kc_game}." - self.data.base.log_event("allnet", "BILLING_CHECKIN_NG_SERIAL", logging.WARN, msg) + self.data.base.log_event( + "allnet", "BILLING_CHECKIN_NG_SERIAL", logging.WARN, msg + ) self.logger.warn(msg) resp = BillingResponse("", "", "", "") resp.result = "1" return self.dict_to_http_form_string([vars(resp)]) - msg = f"Billing checkin from {request.getClientIP()}: game {kc_game} keychip {kc_serial} playcount " \ + msg = ( + f"Billing checkin from {request.getClientIP()}: game {kc_game} keychip {kc_serial} playcount " f"{kc_playcount} billing_type {kc_billigtype} nearfull {kc_nearfull} playlimit {kc_playlimit}" + ) self.logger.info(msg) - self.data.base.log_event('billing', 'BILLING_CHECKIN_OK', logging.INFO, msg) + self.data.base.log_event("billing", "BILLING_CHECKIN_OK", logging.INFO, msg) while kc_playcount > kc_playlimit: kc_playlimit += 1024 kc_nearfull += 1024 - + playlimit = kc_playlimit nearfull = kc_nearfull + (kc_billigtype * 0x00010000) - digest.update(playlimit.to_bytes(4, 'little') + kc_serial_bytes) + digest.update(playlimit.to_bytes(4, "little") + kc_serial_bytes) playlimit_sig = signer.sign(digest).hex() digest = SHA.new() - digest.update(nearfull.to_bytes(4, 'little') + kc_serial_bytes) + digest.update(nearfull.to_bytes(4, "little") + kc_serial_bytes) nearfull_sig = signer.sign(digest).hex() # TODO: playhistory @@ -222,16 +267,16 @@ class AllnetServlet: def kvp_to_dict(self, kvp: List[str]) -> List[Dict[str, Any]]: ret: List[Dict[str, Any]] = [] for x in kvp: - items = x.split('&') + items = x.split("&") tmp = {} for item in items: - kvp = item.split('=') + kvp = item.split("=") if len(kvp) == 2: tmp[kvp[0]] = kvp[1] ret.append(tmp) - + return ret def billing_req_to_dict(self, data: bytes): @@ -241,8 +286,8 @@ class AllnetServlet: try: decomp = zlib.decompressobj(-zlib.MAX_WBITS) unzipped = decomp.decompress(data) - sections = unzipped.decode('ascii').split('\r\n') - + sections = unzipped.decode("ascii").split("\r\n") + return self.kvp_to_dict(sections) except Exception as e: @@ -252,33 +297,38 @@ class AllnetServlet: def allnet_req_to_dict(self, data: str) -> Optional[List[Dict[str, Any]]]: """ Parses an allnet request string into a python dictionary - """ + """ try: zipped = base64.b64decode(data) unzipped = zlib.decompress(zipped) - sections = unzipped.decode('utf-8').split('\r\n') - + sections = unzipped.decode("utf-8").split("\r\n") + return self.kvp_to_dict(sections) except Exception as e: self.logger.error(f"allnet_req_to_dict: {e} while parsing {data}") return None - def dict_to_http_form_string(self, data:List[Dict[str, Any]], crlf: bool = False, trailing_newline: bool = True) -> Optional[str]: + def dict_to_http_form_string( + self, + data: List[Dict[str, Any]], + crlf: bool = False, + trailing_newline: bool = True, + ) -> Optional[str]: """ Takes a python dictionary and parses it into an allnet response string """ try: urlencode = "" for item in data: - for k,v in item.items(): + for k, v in item.items(): urlencode += f"{k}={v}&" if crlf: urlencode = urlencode[:-1] + "\r\n" else: urlencode = urlencode[:-1] + "\n" - + if not trailing_newline: if crlf: urlencode = urlencode[:-2] @@ -286,23 +336,24 @@ class AllnetServlet: urlencode = urlencode[:-1] return urlencode - + except Exception as e: self.logger.error(f"dict_to_http_form_string: {e} while parsing {data}") return None -class AllnetPowerOnRequest(): + +class AllnetPowerOnRequest: def __init__(self, req: Dict) -> None: if req is None: raise AllnetRequestException("Request processing failed") self.game_id: str = req["game_id"] if "game_id" in req else "" self.ver: str = req["ver"] if "ver" in req else "" - self.serial: str = req["serial"] if "serial" in req else "" - self.ip: str = req["ip"] if "ip" in req else "" + self.serial: str = req["serial"] if "serial" in req else "" + self.ip: str = req["ip"] if "ip" in req else "" self.firm_ver: str = req["firm_ver"] if "firm_ver" in req else "" self.boot_ver: str = req["boot_ver"] if "boot_ver" in req else "" self.encode: str = req["encode"] if "encode" in req else "" - + try: self.hops = int(req["hops"]) if "hops" in req else 0 self.format_ver = int(req["format_ver"]) if "format_ver" in req else 2 @@ -310,7 +361,8 @@ class AllnetPowerOnRequest(): except ValueError as e: raise AllnetRequestException(f"Failed to parse int: {e}") -class AllnetPowerOnResponse3(): + +class AllnetPowerOnResponse3: def __init__(self, token) -> None: self.stat = 1 self.uri = "" @@ -326,12 +378,15 @@ class AllnetPowerOnResponse3(): self.country = "JPN" self.allnet_id = "123" self.client_timezone = "+0900" - self.utc_time = datetime.now(tz=pytz.timezone('UTC')).strftime("%Y-%m-%dT%H:%M:%SZ") + self.utc_time = datetime.now(tz=pytz.timezone("UTC")).strftime( + "%Y-%m-%dT%H:%M:%SZ" + ) self.setting = "" self.res_ver = "3" self.token = str(token) -class AllnetPowerOnResponse2(): + +class AllnetPowerOnResponse2: def __init__(self) -> None: self.stat = 1 self.uri = "" @@ -355,23 +410,31 @@ class AllnetPowerOnResponse2(): self.timezone = "+0900" self.res_class = "PowerOnResponseV2" -class AllnetDownloadOrderRequest(): + +class AllnetDownloadOrderRequest: def __init__(self, req: Dict) -> None: self.game_id = req["game_id"] if "game_id" in req else "" self.ver = req["ver"] if "ver" in req else "" self.serial = req["serial"] if "serial" in req else "" self.encode = req["encode"] if "encode" in req else "" -class AllnetDownloadOrderResponse(): + +class AllnetDownloadOrderResponse: def __init__(self, stat: int = 1, serial: str = "", uri: str = "null") -> None: self.stat = stat self.serial = serial self.uri = uri -class BillingResponse(): - def __init__(self, playlimit: str = "", playlimit_sig: str = "", nearfull: str = "", nearfull_sig: str = "", - playhistory: str = "000000/0:000000/0:000000/0") -> None: +class BillingResponse: + def __init__( + self, + playlimit: str = "", + playlimit_sig: str = "", + nearfull: str = "", + nearfull_sig: str = "", + playhistory: str = "000000/0:000000/0:000000/0", + ) -> None: self.result = "0" self.waitime = "100" self.linelimit = "1" @@ -383,10 +446,11 @@ class BillingResponse(): self.nearfullsig = nearfull_sig self.fixlogincnt = "0" self.fixinterval = "5" - self.playhistory = playhistory + self.playhistory = playhistory # playhistory -> YYYYMM/C:... # YYYY -> 4 digit year, MM -> 2 digit month, C -> Playcount during that period + class AllnetRequestException(Exception): def __init__(self, message="") -> None: self.message = message diff --git a/core/config.py b/core/config.py index 04ad280..383f053 100644 --- a/core/config.py +++ b/core/config.py @@ -1,33 +1,47 @@ import logging, os from typing import Any + class ServerConfig: def __init__(self, parent_config: "CoreConfig") -> None: self.__config = parent_config @property def listen_address(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'server', 'listen_address', default='127.0.0.1') + return CoreConfig.get_config_field( + self.__config, "core", "server", "listen_address", default="127.0.0.1" + ) @property def allow_user_registration(self) -> bool: - return CoreConfig.get_config_field(self.__config, 'core', 'server', 'allow_user_registration', default=True) + return CoreConfig.get_config_field( + self.__config, "core", "server", "allow_user_registration", default=True + ) @property def allow_unregistered_serials(self) -> bool: - return CoreConfig.get_config_field(self.__config, 'core', 'server', 'allow_unregistered_serials', default=True) + return CoreConfig.get_config_field( + self.__config, "core", "server", "allow_unregistered_serials", default=True + ) @property def name(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'server', 'name', default="ARTEMiS") + return CoreConfig.get_config_field( + self.__config, "core", "server", "name", default="ARTEMiS" + ) @property def is_develop(self) -> bool: - return CoreConfig.get_config_field(self.__config, 'core', 'server', 'is_develop', default=True) + return CoreConfig.get_config_field( + self.__config, "core", "server", "is_develop", default=True + ) @property def log_dir(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'server', 'log_dir', default='logs') + return CoreConfig.get_config_field( + self.__config, "core", "server", "log_dir", default="logs" + ) + class TitleConfig: def __init__(self, parent_config: "CoreConfig") -> None: @@ -35,15 +49,24 @@ class TitleConfig: @property def loglevel(self) -> int: - return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'core', 'title', 'loglevel', default="info")) + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "core", "title", "loglevel", default="info" + ) + ) @property def hostname(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'title', 'hostname', default="localhost") + return CoreConfig.get_config_field( + self.__config, "core", "title", "hostname", default="localhost" + ) @property def port(self) -> int: - return CoreConfig.get_config_field(self.__config, 'core', 'title', 'port', default=8080) + return CoreConfig.get_config_field( + self.__config, "core", "title", "port", default=8080 + ) + class DatabaseConfig: def __init__(self, parent_config: "CoreConfig") -> None: @@ -51,43 +74,70 @@ class DatabaseConfig: @property def host(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'database', 'host', default="localhost") + return CoreConfig.get_config_field( + self.__config, "core", "database", "host", default="localhost" + ) @property def username(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'database', 'username', default='aime') + return CoreConfig.get_config_field( + self.__config, "core", "database", "username", default="aime" + ) @property def password(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'database', 'password', default='aime') + return CoreConfig.get_config_field( + self.__config, "core", "database", "password", default="aime" + ) @property def name(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'database', 'name', default='aime') + return CoreConfig.get_config_field( + self.__config, "core", "database", "name", default="aime" + ) @property def port(self) -> int: - return CoreConfig.get_config_field(self.__config, 'core', 'database', 'port', default=3306) + return CoreConfig.get_config_field( + self.__config, "core", "database", "port", default=3306 + ) @property def protocol(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'database', 'type', default="mysql") - + return CoreConfig.get_config_field( + self.__config, "core", "database", "type", default="mysql" + ) + @property def sha2_password(self) -> bool: - return CoreConfig.get_config_field(self.__config, 'core', 'database', 'sha2_password', default=False) - + return CoreConfig.get_config_field( + self.__config, "core", "database", "sha2_password", default=False + ) + @property def loglevel(self) -> int: - return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'core', 'database', 'loglevel', default="info")) + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "core", "database", "loglevel", default="info" + ) + ) @property def user_table_autoincrement_start(self) -> int: - return CoreConfig.get_config_field(self.__config, 'core', 'database', 'user_table_autoincrement_start', default=10000) - + return CoreConfig.get_config_field( + self.__config, + "core", + "database", + "user_table_autoincrement_start", + default=10000, + ) + @property def memcached_host(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'database', 'memcached_host', default="localhost") + return CoreConfig.get_config_field( + self.__config, "core", "database", "memcached_host", default="localhost" + ) + class FrontendConfig: def __init__(self, parent_config: "CoreConfig") -> None: @@ -95,15 +145,24 @@ class FrontendConfig: @property def enable(self) -> int: - return CoreConfig.get_config_field(self.__config, 'core', 'frontend', 'enable', default=False) + return CoreConfig.get_config_field( + self.__config, "core", "frontend", "enable", default=False + ) @property def port(self) -> int: - return CoreConfig.get_config_field(self.__config, 'core', 'frontend', 'port', default=8090) - + return CoreConfig.get_config_field( + self.__config, "core", "frontend", "port", default=8090 + ) + @property def loglevel(self) -> int: - return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'core', 'frontend', 'loglevel', default="info")) + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "core", "frontend", "loglevel", default="info" + ) + ) + class AllnetConfig: def __init__(self, parent_config: "CoreConfig") -> None: @@ -111,15 +170,24 @@ class AllnetConfig: @property def loglevel(self) -> int: - return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'core', 'allnet', 'loglevel', default="info")) + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "core", "allnet", "loglevel", default="info" + ) + ) @property def port(self) -> int: - return CoreConfig.get_config_field(self.__config, 'core', 'allnet', 'port', default=80) - + return CoreConfig.get_config_field( + self.__config, "core", "allnet", "port", default=80 + ) + @property def allow_online_updates(self) -> int: - return CoreConfig.get_config_field(self.__config, 'core', 'allnet', 'allow_online_updates', default=False) + return CoreConfig.get_config_field( + self.__config, "core", "allnet", "allow_online_updates", default=False + ) + class BillingConfig: def __init__(self, parent_config: "CoreConfig") -> None: @@ -127,35 +195,53 @@ class BillingConfig: @property def port(self) -> int: - return CoreConfig.get_config_field(self.__config, 'core', 'billing', 'port', default=8443) + return CoreConfig.get_config_field( + self.__config, "core", "billing", "port", default=8443 + ) @property def ssl_key(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'billing', 'ssl_key', default="cert/server.key") + return CoreConfig.get_config_field( + self.__config, "core", "billing", "ssl_key", default="cert/server.key" + ) @property def ssl_cert(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'billing', 'ssl_cert', default="cert/server.pem") - + return CoreConfig.get_config_field( + self.__config, "core", "billing", "ssl_cert", default="cert/server.pem" + ) + @property def signing_key(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'billing', 'signing_key', default="cert/billing.key") + return CoreConfig.get_config_field( + self.__config, "core", "billing", "signing_key", default="cert/billing.key" + ) + class AimedbConfig: def __init__(self, parent_config: "CoreConfig") -> None: self.__config = parent_config - + @property def loglevel(self) -> int: - return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'core', 'aimedb', 'loglevel', default="info")) + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "core", "aimedb", "loglevel", default="info" + ) + ) @property def port(self) -> int: - return CoreConfig.get_config_field(self.__config, 'core', 'aimedb', 'port', default=22345) + return CoreConfig.get_config_field( + self.__config, "core", "aimedb", "port", default=22345 + ) @property def key(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'aimedb', 'key', default="") + return CoreConfig.get_config_field( + self.__config, "core", "aimedb", "key", default="" + ) + class MuchaConfig: def __init__(self, parent_config: "CoreConfig") -> None: @@ -163,27 +249,42 @@ class MuchaConfig: @property def enable(self) -> int: - return CoreConfig.get_config_field(self.__config, 'core', 'mucha', 'enable', default=False) - + return CoreConfig.get_config_field( + self.__config, "core", "mucha", "enable", default=False + ) + @property def loglevel(self) -> int: - return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'core', 'mucha', 'loglevel', default="info")) - + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "core", "mucha", "loglevel", default="info" + ) + ) + @property def hostname(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'mucha', 'hostname', default="localhost") + return CoreConfig.get_config_field( + self.__config, "core", "mucha", "hostname", default="localhost" + ) @property def port(self) -> int: - return CoreConfig.get_config_field(self.__config, 'core', 'mucha', 'port', default=8444) - + return CoreConfig.get_config_field( + self.__config, "core", "mucha", "port", default=8444 + ) + @property def ssl_cert(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'mucha', 'ssl_cert', default="cert/server.pem") - + return CoreConfig.get_config_field( + self.__config, "core", "mucha", "ssl_cert", default="cert/server.pem" + ) + @property def signing_key(self) -> str: - return CoreConfig.get_config_field(self.__config, 'core', 'mucha', 'signing_key', default="cert/billing.key") + return CoreConfig.get_config_field( + self.__config, "core", "mucha", "signing_key", default="cert/billing.key" + ) + class CoreConfig(dict): def __init__(self) -> None: @@ -200,20 +301,22 @@ class CoreConfig(dict): def str_to_loglevel(cls, level_str: str): if level_str.lower() == "error": return logging.ERROR - elif level_str.lower().startswith("warn"): # Fits warn or warning + elif level_str.lower().startswith("warn"): # Fits warn or warning return logging.WARN elif level_str.lower() == "debug": return logging.DEBUG else: - return logging.INFO + return logging.INFO @classmethod - def get_config_field(cls, __config: dict, module, *path: str, default: Any = "") -> Any: - envKey = f'CFG_{module}_' + def get_config_field( + cls, __config: dict, module, *path: str, default: Any = "" + ) -> Any: + envKey = f"CFG_{module}_" for arg in path: - envKey += arg + '_' - - if envKey.endswith('_'): + envKey += arg + "_" + + if envKey.endswith("_"): envKey = envKey[:-1] if envKey in os.environ: diff --git a/core/const.py b/core/const.py index da018ee..98effb6 100644 --- a/core/const.py +++ b/core/const.py @@ -1,6 +1,7 @@ from enum import Enum -class MainboardPlatformCodes(): + +class MainboardPlatformCodes: RINGEDGE = "AALE" RINGWIDE = "AAML" NU = "AAVE" @@ -8,7 +9,8 @@ class MainboardPlatformCodes(): ALLS_UX = "ACAE" ALLS_HX = "ACAX" -class MainboardRevisions(): + +class MainboardRevisions: RINGEDGE = 1 RINGEDGE2 = 2 @@ -26,12 +28,14 @@ class MainboardRevisions(): ALLS_UX2 = 2 ALLS_HX2 = 12 -class KeychipPlatformsCodes(): + +class KeychipPlatformsCodes: RING = "A72E" NU = ("A60E", "A60E", "A60E") NUSX = ("A61X", "A69X") ALLS = "A63E" + class AllnetCountryCode(Enum): JAPAN = "JPN" UNITED_STATES = "USA" @@ -41,6 +45,7 @@ class AllnetCountryCode(Enum): TAIWAN = "TWN" CHINA = "CHN" + class AllnetJapanRegionId(Enum): NONE = 0 AICHI = 1 diff --git a/core/data/__init__.py b/core/data/__init__.py index 4eee928..eb30d05 100644 --- a/core/data/__init__.py +++ b/core/data/__init__.py @@ -1,2 +1,2 @@ from core.data.database import Data -from core.data.cache import cached \ No newline at end of file +from core.data.cache import cached diff --git a/core/data/cache.py b/core/data/cache.py index cdea825..cabf597 100644 --- a/core/data/cache.py +++ b/core/data/cache.py @@ -1,4 +1,3 @@ - from typing import Any, Callable from functools import wraps import hashlib @@ -6,15 +5,17 @@ import pickle import logging from core.config import CoreConfig -cfg:CoreConfig = None # type: ignore +cfg: CoreConfig = None # type: ignore # Make memcache optional try: import pylibmc # type: ignore + has_mc = True except ModuleNotFoundError: has_mc = False -def cached(lifetime: int=10, extra_key: Any=None) -> Callable: + +def cached(lifetime: int = 10, extra_key: Any = None) -> Callable: def _cached(func: Callable) -> Callable: if has_mc: hostname = "127.0.0.1" @@ -22,11 +23,10 @@ def cached(lifetime: int=10, extra_key: Any=None) -> Callable: hostname = cfg.database.memcached_host memcache = pylibmc.Client([hostname], binary=True) memcache.behaviors = {"tcp_nodelay": True, "ketama": True} - + @wraps(func) def wrapper(*args: Any, **kwargs: Any) -> Any: if lifetime is not None: - # Hash function args items = kwargs.items() hashable_args = (args[1:], sorted(list(items))) @@ -41,7 +41,7 @@ def cached(lifetime: int=10, extra_key: Any=None) -> Callable: except pylibmc.Error as e: logging.getLogger("database").error(f"Memcache failed: {e}") result = None - + if result is not None: logging.getLogger("database").debug(f"Cache hit: {result}") return result @@ -55,7 +55,9 @@ def cached(lifetime: int=10, extra_key: Any=None) -> Callable: memcache.set(cache_key, result, lifetime) return result + else: + @wraps(func) def wrapper(*args: Any, **kwargs: Any) -> Any: return func(*args, **kwargs) diff --git a/core/data/database.py b/core/data/database.py index ab4c587..1af5c08 100644 --- a/core/data/database.py +++ b/core/data/database.py @@ -13,6 +13,7 @@ from core.config import CoreConfig from core.data.schema import * from core.utils import Utils + class Data: def __init__(self, cfg: CoreConfig) -> None: self.config = cfg @@ -22,7 +23,7 @@ class Data: self.__url = f"{self.config.database.protocol}://{self.config.database.username}:{passwd.hex()}@{self.config.database.host}/{self.config.database.name}?charset=utf8mb4" else: self.__url = f"{self.config.database.protocol}://{self.config.database.username}:{self.config.database.password}@{self.config.database.host}/{self.config.database.name}?charset=utf8mb4" - + self.__engine = create_engine(self.__url, pool_recycle=3600) session = sessionmaker(bind=self.__engine, autoflush=True, autocommit=True) self.session = scoped_session(session) @@ -38,11 +39,15 @@ class Data: self.logger = logging.getLogger("database") # Prevent the logger from adding handlers multiple times - if not getattr(self.logger, 'handler_set', None): - fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.config.server.log_dir, "db"), encoding="utf-8", - when="d", backupCount=10) + if not getattr(self.logger, "handler_set", None): + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.config.server.log_dir, "db"), + encoding="utf-8", + when="d", + backupCount=10, + ) fileHandler.setFormatter(log_fmt) - + consoleHandler = logging.StreamHandler() consoleHandler.setFormatter(log_fmt) @@ -50,8 +55,10 @@ class Data: self.logger.addHandler(consoleHandler) self.logger.setLevel(self.config.database.loglevel) - coloredlogs.install(cfg.database.loglevel, logger=self.logger, fmt=log_fmt_str) - self.logger.handler_set = True # type: ignore + coloredlogs.install( + cfg.database.loglevel, logger=self.logger, fmt=log_fmt_str + ) + self.logger.handler_set = True # type: ignore def create_database(self): self.logger.info("Creating databases...") @@ -60,24 +67,32 @@ class Data: except SQLAlchemyError as e: self.logger.error(f"Failed to create databases! {e}") return - + games = Utils.get_all_titles() for game_dir, game_mod in games.items(): try: title_db = game_mod.database(self.config) metadata.create_all(self.__engine.connect()) - self.base.set_schema_ver(game_mod.current_schema_version, game_mod.game_codes[0]) + self.base.set_schema_ver( + game_mod.current_schema_version, game_mod.game_codes[0] + ) except Exception as e: - self.logger.warning(f"Could not load database schema from {game_dir} - {e}") - + self.logger.warning( + f"Could not load database schema from {game_dir} - {e}" + ) + self.logger.info(f"Setting base_schema_ver to {self.schema_ver_latest}") self.base.set_schema_ver(self.schema_ver_latest) - 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) - + self.logger.info( + f"Setting user auto_incrememnt to {self.config.database.user_table_autoincrement_start}" + ) + self.user.reset_autoincrement( + self.config.database.user_table_autoincrement_start + ) + def recreate_database(self): self.logger.info("Dropping all databases...") self.base.execute("SET FOREIGN_KEY_CHECKS=0") @@ -86,61 +101,79 @@ class Data: except SQLAlchemyError as e: self.logger.error(f"Failed to drop databases! {e}") return - + for root, dirs, files in os.walk("./titles"): - for dir in dirs: + for dir in dirs: if not dir.startswith("__"): try: mod = importlib.import_module(f"titles.{dir}") - + try: title_db = 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}") + 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}") + 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: int, action: str) -> None: old_ver = self.base.get_schema_ver(game) sql = "" - + 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 not os.path.exists(f"core/data/schema/versions/{game.upper()}_{version}_{action}.sql"): - self.logger.error(f"Could not find {action} script {game.upper()}_{version}_{action}.sql in core/data/schema/versions folder") + self.logger.error( + f"Schema for game {game} does not exist, did you run the creation script?" + ) return - with open(f"core/data/schema/versions/{game.upper()}_{version}_{action}.sql", "r", encoding="utf-8") as f: + if old_ver == version: + self.logger.info( + f"Schema for game {game} is already version {old_ver}, nothing to do" + ) + return + + if not os.path.exists( + f"core/data/schema/versions/{game.upper()}_{version}_{action}.sql" + ): + self.logger.error( + f"Could not find {action} script {game.upper()}_{version}_{action}.sql in core/data/schema/versions folder" + ) + return + + with open( + f"core/data/schema/versions/{game.upper()}_{version}_{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)) + pw = "".join( + secrets.choice(string.ascii_letters + string.digits) for i in range(20) + ) hash = bcrypt.hashpw(pw.encode(), bcrypt.gensalt()) user_id = self.user.create_user(email=email, permission=255, password=hash) @@ -153,32 +186,38 @@ class Data: self.logger.error(f"Failed to create card for owner with id {user_id}") return - self.logger.warn(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!") - + self.logger.warn( + f"Successfully created owner with email {email}, access code 00000000000000000000, and password {pw} Make sure to change this password and assign a real card ASAP!" + ) + def migrate_card(self, old_ac: str, new_ac: str, should_force: bool) -> None: if old_ac == new_ac: self.logger.error("Both access codes are the same!") return - + new_card = self.card.get_card_by_access_code(new_ac) if new_card is None: self.card.update_access_code(old_ac, new_ac) return - + if not should_force: - self.logger.warn(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}.") + self.logger.warn( + f"Card already exists for access code {new_ac} (id {new_card['id']}). If you wish to continue, rerun with the '--force' flag." + f" All exiting data on the target card {new_ac} will be perminently erased and replaced with data from card {old_ac}." + ) return - - self.logger.info(f"All exiting data on the target card {new_ac} will be perminently erased and replaced with data from card {old_ac}.") + + self.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']) - + 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. @@ -186,13 +225,13 @@ class Data: 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']) + 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']) + self.user.delete_user(user["id"]) diff --git a/core/data/schema/__init__.py b/core/data/schema/__init__.py index 9032a68..45931d7 100644 --- a/core/data/schema/__init__.py +++ b/core/data/schema/__init__.py @@ -3,4 +3,4 @@ from core.data.schema.card import CardData from core.data.schema.base import BaseData, metadata from core.data.schema.arcade import ArcadeData -__all__ = ["UserData", "CardData", "BaseData", "metadata", "ArcadeData"] \ No newline at end of file +__all__ = ["UserData", "CardData", "BaseData", "metadata", "ArcadeData"] diff --git a/core/data/schema/arcade.py b/core/data/schema/arcade.py index af4069d..e1d9b1f 100644 --- a/core/data/schema/arcade.py +++ b/core/data/schema/arcade.py @@ -14,131 +14,186 @@ arcade = Table( metadata, Column("id", Integer, primary_key=True, nullable=False), Column("name", String(255)), - Column("nickname", String(255)), + Column("nickname", String(255)), Column("country", String(3)), Column("country_id", Integer), Column("state", String(255)), Column("city", String(255)), Column("region_id", Integer), Column("timezone", String(255)), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) machine = Table( "machine", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("arcade", ForeignKey("arcade.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "arcade", + ForeignKey("arcade.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("serial", String(15), nullable=False), Column("board", String(15)), Column("game", String(4)), - Column("country", String(3)), # overwrites if not null + Column("country", String(3)), # overwrites if not null Column("timezone", String(255)), Column("ota_enable", Boolean), Column("is_cab", Boolean), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) arcade_owner = Table( - 'arcade_owner', + "arcade_owner", metadata, - Column('user', Integer, ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column('arcade', Integer, ForeignKey("arcade.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column('permissions', Integer, nullable=False), - PrimaryKeyConstraint('user', 'arcade', name='arcade_owner_pk'), - mysql_charset='utf8mb4' + Column( + "user", + Integer, + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column( + "arcade", + Integer, + ForeignKey("arcade.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("permissions", Integer, nullable=False), + PrimaryKeyConstraint("user", "arcade", name="arcade_owner_pk"), + mysql_charset="utf8mb4", ) + class ArcadeData(BaseData): def get_machine(self, serial: str = None, id: int = None) -> Optional[Dict]: if serial is not None: serial = serial.replace("-", "") if len(serial) == 11: sql = machine.select(machine.c.serial.like(f"{serial}%")) - + elif len(serial) == 15: sql = machine.select(machine.c.serial == serial) - + else: self.logger.error(f"{__name__ }: Malformed serial {serial}") return None - + elif id is not None: sql = machine.select(machine.c.id == id) - - else: + + else: self.logger.error(f"{__name__ }: Need either serial or ID to look up!") return None - + result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() - - def put_machine(self, arcade_id: int, serial: str = "", board: str = None, game: str = None, is_cab: bool = False) -> Optional[int]: + + def put_machine( + self, + arcade_id: int, + serial: str = "", + board: str = None, + game: str = None, + is_cab: bool = False, + ) -> Optional[int]: if arcade_id: self.logger.error(f"{__name__ }: Need arcade id!") return None - sql = machine.insert().values(arcade = arcade_id, keychip = serial, board = board, game = game, is_cab = is_cab) - - result = self.execute(sql) - if result is None: return None - return result.lastrowid - - def set_machine_serial(self, machine_id: int, serial: str) -> None: - result = self.execute(machine.update(machine.c.id == machine_id).values(keychip = serial)) - if result is None: - self.logger.error(f"Failed to update serial for machine {machine_id} -> {serial}") - return result.lastrowid - - def set_machine_boardid(self, machine_id: int, boardid: str) -> None: - result = self.execute(machine.update(machine.c.id == machine_id).values(board = boardid)) - if result is None: - self.logger.error(f"Failed to update board id for machine {machine_id} -> {boardid}") - - def get_arcade(self, id: int) -> Optional[Dict]: - sql = arcade.select(arcade.c.id == id) - result = self.execute(sql) - if result is None: return None - return result.fetchone() - - def put_arcade(self, name: str, nickname: str = None, country: str = "JPN", country_id: int = 1, - state: str = "", city: str = "", regional_id: int = 1) -> Optional[int]: - if nickname is None: nickname = name - - sql = arcade.insert().values(name = name, nickname = nickname, country = country, country_id = country_id, - state = state, city = city, regional_id = regional_id) - - result = self.execute(sql) - if result is None: return None - return result.lastrowid - - def get_arcade_owners(self, arcade_id: int) -> Optional[Dict]: - sql = select(arcade_owner).where(arcade_owner.c.arcade==arcade_id) - - result = self.execute(sql) - if result is None: return None - return result.fetchall() - - def add_arcade_owner(self, arcade_id: int, user_id: int) -> None: - sql = insert(arcade_owner).values( - arcade=arcade_id, - user=user_id + sql = machine.insert().values( + arcade=arcade_id, keychip=serial, board=board, game=game, is_cab=is_cab ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.lastrowid - def 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 set_machine_serial(self, machine_id: int, serial: str) -> None: + result = self.execute( + machine.update(machine.c.id == machine_id).values(keychip=serial) + ) + if result is None: + self.logger.error( + f"Failed to update serial for machine {machine_id} -> {serial}" + ) + return result.lastrowid + + def set_machine_boardid(self, machine_id: int, boardid: str) -> None: + result = self.execute( + machine.update(machine.c.id == machine_id).values(board=boardid) + ) + if result is None: + self.logger.error( + f"Failed to update board id for machine {machine_id} -> {boardid}" + ) + + def get_arcade(self, id: int) -> Optional[Dict]: + sql = arcade.select(arcade.c.id == id) + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def put_arcade( + self, + name: str, + nickname: str = None, + country: str = "JPN", + country_id: int = 1, + state: str = "", + city: str = "", + regional_id: int = 1, + ) -> Optional[int]: + if nickname is None: + nickname = name + + sql = arcade.insert().values( + name=name, + nickname=nickname, + country=country, + country_id=country_id, + state=state, + city=city, + regional_id=regional_id, + ) + + result = self.execute(sql) + if result is None: + return None + return result.lastrowid + + def get_arcade_owners(self, arcade_id: int) -> Optional[Dict]: + sql = select(arcade_owner).where(arcade_owner.c.arcade == arcade_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def add_arcade_owner(self, arcade_id: int, user_id: int) -> None: + sql = insert(arcade_owner).values(arcade=arcade_id, user=user_id) + + result = self.execute(sql) + if result is None: + return None + return result.lastrowid + + def format_serial( + self, platform_code: str, platform_rev: int, serial_num: int, append: int = 4152 + ) -> str: + return f"{platform_code}{platform_rev:02d}A{serial_num:04d}{append:04d}" # 0x41 = A, 0x52 = R + def validate_keychip_format(self, serial: str) -> bool: serial = serial.replace("-", "") if len(serial) != 11 or len(serial) != 15: - self.logger.error(f"Serial validate failed: Incorrect length for {serial} (len {len(serial)})") + self.logger.error( + f"Serial validate failed: Incorrect length for {serial} (len {len(serial)})" + ) return False - + platform_code = serial[:4] platform_rev = serial[4:6] const_a = serial[6] @@ -150,11 +205,15 @@ class ArcadeData(BaseData): return False if len(append) != 0 or len(append) != 4: - self.logger.error(f"Serial validate failed: {serial} had malformed append {append}") + self.logger.error( + f"Serial validate failed: {serial} had malformed append {append}" + ) return False if len(num) != 4: - self.logger.error(f"Serial validate failed: {serial} had malformed number {num}") + self.logger.error( + f"Serial validate failed: {serial} had malformed number {num}" + ) return False - + return True diff --git a/core/data/schema/base.py b/core/data/schema/base.py index 955c772..9899f29 100644 --- a/core/data/schema/base.py +++ b/core/data/schema/base.py @@ -19,7 +19,7 @@ schema_ver = Table( metadata, Column("game", String(4), primary_key=True, nullable=False), Column("version", Integer, nullable=False, server_default="1"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) event_log = Table( @@ -32,16 +32,17 @@ event_log = Table( Column("message", String(1000), nullable=False), Column("details", JSON, nullable=False), Column("when_logged", TIMESTAMP, nullable=False, server_default=func.now()), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) -class BaseData(): + +class BaseData: def __init__(self, cfg: CoreConfig, conn: Connection) -> None: self.config = cfg self.conn = conn self.logger = logging.getLogger("database") - - def execute(self, sql: str, opts: Dict[str, Any]={}) -> Optional[CursorResult]: + + def execute(self, sql: str, opts: Dict[str, Any] = {}) -> Optional[CursorResult]: res = None try: @@ -51,7 +52,7 @@ class BaseData(): except SQLAlchemyError as e: self.logger.error(f"SQLAlchemy error {e}") return None - + except UnicodeEncodeError as e: self.logger.error(f"UnicodeEncodeError error {e}") return None @@ -63,7 +64,7 @@ class BaseData(): except SQLAlchemyError as e: self.logger.error(f"SQLAlchemy error {e}") return None - + except UnicodeEncodeError as e: self.logger.error(f"UnicodeEncodeError error {e}") return None @@ -73,58 +74,71 @@ class BaseData(): raise return res - + def generate_id(self) -> int: """ Generate a random 5-7 digit id """ return randrange(10000, 9999999) - + 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 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) - + 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})") + self.logger.error( + f"Failed to update schema version for game {game} (v{ver})" + ) return None return result.lastrowid - def log_event(self, system: str, type: str, severity: int, message: str, details: Dict = {}) -> Optional[int]: - sql = event_log.insert().values(system = system, type = type, severity = severity, message = message, details = json.dumps(details)) + def log_event( + self, system: str, type: str, severity: int, message: str, details: Dict = {} + ) -> Optional[int]: + sql = event_log.insert().values( + system=system, + type=type, + severity=severity, + message=message, + details=json.dumps(details), + ) result = self.execute(sql) if result is None: - self.logger.error(f"{__name__}: Failed to insert event into event log! system = {system}, type = {type}, severity = {severity}, message = {message}") + self.logger.error( + f"{__name__}: Failed to insert event into event log! system = {system}, type = {type}, severity = {severity}, message = {message}" + ) return None return result.lastrowid - + def get_event_log(self, entries: int = 100) -> Optional[List[Dict]]: sql = event_log.select().limit(entries).all() result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - + def fix_bools(self, data: Dict) -> Dict: - for k,v in data.items(): + for k, v in data.items(): if type(v) == str and v.lower() == "true": data[k] = True elif type(v) == str and v.lower() == "false": data[k] = False - + return data diff --git a/core/data/schema/card.py b/core/data/schema/card.py index dc74832..d8f5fc0 100644 --- a/core/data/schema/card.py +++ b/core/data/schema/card.py @@ -8,55 +8,67 @@ from sqlalchemy.engine import Row from core.data.schema.base import BaseData, metadata aime_card = Table( - 'aime_card', + "aime_card", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("access_code", String(20)), Column("created_date", TIMESTAMP, server_default=func.now()), Column("last_login_date", TIMESTAMP, onupdate=func.now()), Column("is_locked", Boolean, server_default="0"), Column("is_banned", Boolean, server_default="0"), UniqueConstraint("user", "access_code", name="aime_card_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) + class CardData(BaseData): def get_card_by_access_code(self, access_code: str) -> Optional[Row]: sql = aime_card.select(aime_card.c.access_code == access_code) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() def get_card_by_id(self, card_id: int) -> Optional[Row]: sql = aime_card.select(aime_card.c.id == card_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() - + def update_access_code(self, old_ac: str, new_ac: str) -> None: - sql = aime_card.update(aime_card.c.access_code == old_ac).values(access_code = new_ac) + sql = aime_card.update(aime_card.c.access_code == old_ac).values( + access_code=new_ac + ) result = self.execute(sql) - if result is None: - self.logger.error(f"Failed to change card access code from {old_ac} to {new_ac}") + if result is None: + self.logger.error( + f"Failed to change card access code from {old_ac} to {new_ac}" + ) def get_user_id_from_card(self, access_code: str) -> Optional[int]: """ Given a 20 digit access code as a string, get the user id associated with that card """ card = self.get_card_by_access_code(access_code) - if card is None: return None + if card is None: + return None return int(card["user"]) - + def delete_card(self, card_id: int) -> None: sql = aime_card.delete(aime_card.c.id == card_id) result = self.execute(sql) - if result is None: + if result is None: self.logger.error(f"Failed to delete card with id {card_id}") def get_user_cards(self, aime_id: int) -> Optional[List[Row]]: @@ -65,17 +77,18 @@ class CardData(BaseData): """ sql = aime_card.select(aime_card.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - def create_card(self, user_id: int, access_code: str) -> Optional[int]: """ Given a aime_user id and a 20 digit access code as a string, create a card and return the ID if successful """ sql = aime_card.insert().values(user=user_id, access_code=access_code) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.lastrowid def to_access_code(self, luid: str) -> str: @@ -88,4 +101,4 @@ class CardData(BaseData): """ Given a 20 digit access code as a string, return the 16 hex character luid """ - return f'{int(access_code):0{16}x}' \ No newline at end of file + return f"{int(access_code):0{16}x}" diff --git a/core/data/schema/user.py b/core/data/schema/user.py index aee07e9..98663d1 100644 --- a/core/data/schema/user.py +++ b/core/data/schema/user.py @@ -17,72 +17,81 @@ aime_user = Table( Column("username", String(25), unique=True), Column("email", String(255), unique=True), Column("password", String(255)), - Column("permissions", Integer), + Column("permissions", Integer), Column("created_date", TIMESTAMP, server_default=func.now()), Column("last_login_date", TIMESTAMP, onupdate=func.now()), Column("suspend_expire_time", TIMESTAMP), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) + class PermissionBits(Enum): PermUser = 1 PermMod = 2 PermSysAdmin = 4 + class UserData(BaseData): - def create_user(self, id: int = None, username: str = None, email: str = None, password: str = None, permission: int = 1) -> Optional[int]: + def create_user( + self, + id: int = None, + username: str = None, + email: str = None, + password: str = None, + permission: int = 1, + ) -> Optional[int]: if id is None: sql = insert(aime_user).values( - username=username, - email=email, - password=password, - permissions=permission + username=username, + email=email, + password=password, + permissions=permission, ) else: sql = insert(aime_user).values( - id=id, - username=username, - email=email, - password=password, - permissions=permission + id=id, + username=username, + email=email, + password=password, + permissions=permission, ) conflict = sql.on_duplicate_key_update( - username=username, - email=email, - password=password, - permissions=permission + username=username, email=email, password=password, permissions=permission ) - + result = self.execute(conflict) - if result is None: return None + if result is None: + return None return result.lastrowid - + def get_user(self, user_id: int) -> Optional[Row]: sql = select(aime_user).where(aime_user.c.id == user_id) result = self.execute(sql) - if result is None: return False + if result is None: + return False return result.fetchone() - + def check_password(self, user_id: int, passwd: bytes = None) -> bool: usr = self.get_user(user_id) - if usr is None: return False - - if usr['password'] is None: + if usr is None: return False - return bcrypt.checkpw(passwd, usr['password'].encode()) + if usr["password"] is None: + return False + + return bcrypt.checkpw(passwd, usr["password"].encode()) 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 = self.execute(sql) - if result is None: + if result is None: self.logger.error(f"Failed to delete user with id {user_id}") def get_unregistered_users(self) -> List[Row]: @@ -92,6 +101,6 @@ class UserData(BaseData): sql = select(aime_user).where(aime_user.c.password == None) result = self.execute(sql) - if result is None: + if result is None: return None - return result.fetchall() \ No newline at end of file + return result.fetchall() diff --git a/core/frontend.py b/core/frontend.py index 6f95073..2fdefae 100644 --- a/core/frontend.py +++ b/core/frontend.py @@ -14,11 +14,13 @@ from core.config import CoreConfig from core.data import Data from core.utils import Utils + class IUserSession(Interface): userId = Attribute("User's ID") current_ip = Attribute("User's current ip address") permissions = Attribute("User's permission level") - + + @implementer(IUserSession) class UserSession(object): def __init__(self, session): @@ -26,10 +28,11 @@ class UserSession(object): self.current_ip = "0.0.0.0" self.permissions = 0 + class FrontendServlet(resource.Resource): def getChild(self, name: bytes, request: Request): self.logger.debug(f"{request.getClientIP()} -> {name.decode()}") - if name == b'': + if name == b"": return self return resource.Resource.getChild(self, name, request) @@ -42,17 +45,23 @@ class FrontendServlet(resource.Resource): self.game_list: List[Dict[str, str]] = [] self.children: Dict[str, Any] = {} - fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.config.server.log_dir, "frontend"), when="d", backupCount=10) + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.config.server.log_dir, "frontend"), + when="d", + backupCount=10, + ) fileHandler.setFormatter(log_fmt) - + consoleHandler = logging.StreamHandler() consoleHandler.setFormatter(log_fmt) self.logger.addHandler(fileHandler) self.logger.addHandler(consoleHandler) - + self.logger.setLevel(cfg.frontend.loglevel) - coloredlogs.install(level=cfg.frontend.loglevel, logger=self.logger, fmt=log_fmt_str) + coloredlogs.install( + level=cfg.frontend.loglevel, logger=self.logger, fmt=log_fmt_str + ) registerAdapter(UserSession, Session, IUserSession) fe_game = FE_Game(cfg, self.environment) @@ -65,18 +74,26 @@ class FrontendServlet(resource.Resource): fe_game.putChild(game_dir.encode(), game_fe) except: raise - + self.environment.globals["game_list"] = self.game_list self.putChild(b"gate", FE_Gate(cfg, self.environment)) self.putChild(b"user", FE_User(cfg, self.environment)) self.putChild(b"game", fe_game) - self.logger.info(f"Ready on port {self.config.frontend.port} serving {len(fe_game.children)} games") + 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"{request.getClientIP()} -> {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") + return template.render( + server_name=self.config.server.name, + title=self.config.server.name, + game_list=self.game_list, + sesh=vars(IUserSession(request.getSession())), + ).encode("utf-16") + class FE_Base(resource.Resource): """ @@ -84,43 +101,51 @@ class FE_Base(resource.Resource): 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 + + 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.logger = logging.getLogger("frontend") self.environment = environment self.nav_name = "nav_name" + class FE_Gate(FE_Base): - def render_GET(self, request: Request): + def render_GET(self, request: Request): self.logger.debug(f"{request.getClientIP()} -> {request.uri.decode()}") uri: str = request.uri.decode() - + sesh = request.getSession() usr_sesh = IUserSession(sesh) if usr_sesh.userId > 0: return redirectTo(b"/user", request) - + if uri.startswith("/gate/create"): return self.create_user(request) - if b'e' in request.args: + if b"e" in request.args: try: - err = int(request.args[b'e'][0].decode()) + err = int(request.args[b"e"][0].decode()) except: err = 0 - else: err = 0 + else: + err = 0 + + template = self.environment.get_template("core/frontend/gate/gate.jinja") + return template.render( + title=f"{self.core_config.server.name} | Login Gate", + error=err, + sesh=vars(usr_sesh), + ).encode("utf-16") - template = self.environment.get_template("core/frontend/gate/gate.jinja") - return template.render(title=f"{self.core_config.server.name} | Login Gate", error=err, sesh=vars(usr_sesh)).encode("utf-16") - def render_POST(self, request: Request): uri = request.uri.decode() ip = request.getClientAddress().host - - if uri == "/gate/gate.login": + + 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"": @@ -129,26 +154,28 @@ class FE_Gate(FE_Base): uid = self.data.card.get_user_id_from_card(access_code) if uid is None: return redirectTo(b"/gate?e=1", request) - + if passwd is None: sesh = self.data.user.check_password(uid) if sesh is not None: - return redirectTo(f"/gate/create?ac={access_code}".encode(), request) + return redirectTo( + f"/gate/create?ac={access_code}".encode(), request + ) return redirectTo(b"/gate?e=1", request) if not self.data.user.check_password(uid, passwd): return redirectTo(b"/gate?e=1", request) - + self.logger.info(f"Successful login of user {uid} at {ip}") - + sesh = request.getSession() usr_sesh = IUserSession(sesh) usr_sesh.userId = uid usr_sesh.current_ip = ip - + return redirectTo(b"/user", request) - + elif uri == "/gate/gate.create": access_code: str = request.args[b"access_code"][0].decode() username: str = request.args[b"username"][0] @@ -162,26 +189,33 @@ class FE_Gate(FE_Base): salt = bcrypt.gensalt() hashed = bcrypt.hashpw(passwd, salt) - result = self.data.user.create_user(uid, username, email, hashed.decode(), 1) + result = self.data.user.create_user( + uid, username, email, hashed.decode(), 1 + ) if result is None: return redirectTo(b"/gate?e=3", request) - + if not self.data.user.check_password(uid, passwd.encode()): return redirectTo(b"/gate", request) - + return redirectTo(b"/user", request) else: return b"" def create_user(self, request: Request): - if b'ac' not in request.args or len(request.args[b'ac'][0].decode()) != 20: + 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() - - 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}).encode("utf-16") + ac = request.args[b"ac"][0].decode() + + 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}, + ).encode("utf-16") + class FE_User(FE_Base): def render_GET(self, request: Request): @@ -191,17 +225,20 @@ class FE_User(FE_Base): usr_sesh = IUserSession(sesh) if usr_sesh.userId == 0: return redirectTo(b"/gate", request) - - return template.render(title=f"{self.core_config.server.name} | Account", sesh=vars(usr_sesh)).encode("utf-16") + + return template.render( + title=f"{self.core_config.server.name} | Account", sesh=vars(usr_sesh) + ).encode("utf-16") + class FE_Game(FE_Base): isLeaf = False children: Dict[str, Any] = {} def getChild(self, name: bytes, request: Request): - if name == b'': + if name == b"": return self return resource.Resource.getChild(self, name, request) def render_GET(self, request: Request) -> bytes: - return redirectTo(b"/user", request) \ No newline at end of file + return redirectTo(b"/user", request) diff --git a/core/mucha.py b/core/mucha.py index 1f0312a..1b73b84 100644 --- a/core/mucha.py +++ b/core/mucha.py @@ -9,25 +9,30 @@ import pytz from core.config import CoreConfig from core.utils import Utils + class MuchaServlet: - def __init__(self, cfg: CoreConfig, cfg_dir: str) -> None: + def __init__(self, cfg: CoreConfig, cfg_dir: str) -> None: self.config = cfg self.config_dir = cfg_dir self.mucha_registry: List[str] = [] - self.logger = logging.getLogger('mucha') + self.logger = logging.getLogger("mucha") log_fmt_str = "[%(asctime)s] Mucha | %(levelname)s | %(message)s" log_fmt = logging.Formatter(log_fmt_str) - - fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.config.server.log_dir, "mucha"), when="d", backupCount=10) + + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.config.server.log_dir, "mucha"), + when="d", + backupCount=10, + ) fileHandler.setFormatter(log_fmt) - + consoleHandler = logging.StreamHandler() consoleHandler.setFormatter(log_fmt) self.logger.addHandler(fileHandler) self.logger.addHandler(consoleHandler) - + self.logger.setLevel(logging.INFO) coloredlogs.install(level=logging.INFO, logger=self.logger, fmt=log_fmt_str) @@ -35,43 +40,57 @@ class MuchaServlet: for _, mod in all_titles.items(): if hasattr(mod, "index") and hasattr(mod.index, "get_mucha_info"): - enabled, game_cd = mod.index.get_mucha_info(self.config, self.config_dir) + enabled, game_cd = mod.index.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 on port {self.config.mucha.port}") + + self.logger.info( + f"Serving {len(self.mucha_registry)} games on port {self.config.mucha.port}" + ) def handle_boardauth(self, request: Request, _: Dict) -> bytes: req_dict = self.mucha_preprocess(request.content.getvalue()) if req_dict is None: - self.logger.error(f"Error processing mucha request {request.content.getvalue()}") + self.logger.error( + f"Error processing mucha request {request.content.getvalue()}" + ) return b"" req = MuchaAuthRequest(req_dict) self.logger.debug(f"Mucha request {vars(req)}") - self.logger.info(f"Boardauth request from {request.getClientAddress().host} for {req.gameVer}") + self.logger.info( + f"Boardauth request from {request.getClientAddress().host} for {req.gameVer}" + ) if req.gameCd not in self.mucha_registry: self.logger.warn(f"Unknown gameCd {req.gameCd}") return b"" # TODO: Decrypt S/N - - resp = MuchaAuthResponse(f"{self.config.mucha.hostname}{':' + self.config.mucha.port if self.config.server.is_develop else ''}") + + resp = MuchaAuthResponse( + f"{self.config.mucha.hostname}{':' + self.config.mucha.port if self.config.server.is_develop else ''}" + ) self.logger.debug(f"Mucha response {vars(resp)}") return self.mucha_postprocess(vars(resp)) - + def handle_updatecheck(self, request: Request, _: Dict) -> bytes: req_dict = self.mucha_preprocess(request.content.getvalue()) if req_dict is None: - self.logger.error(f"Error processing mucha request {request.content.getvalue()}") + self.logger.error( + f"Error processing mucha request {request.content.getvalue()}" + ) return b"" req = MuchaUpdateRequest(req_dict) self.logger.debug(f"Mucha request {vars(req)}") - self.logger.info(f"Updatecheck request from {request.getClientAddress().host} for {req.gameVer}") + self.logger.info( + f"Updatecheck request from {request.getClientAddress().host} for {req.gameVer}" + ) if req.gameCd not in self.mucha_registry: self.logger.warn(f"Unknown gameCd {req.gameCd}") @@ -86,14 +105,14 @@ class MuchaServlet: def mucha_preprocess(self, data: bytes) -> Optional[Dict]: try: ret: Dict[str, Any] = {} - - for x in data.decode().split('&'): - kvp = x.split('=') + + for x in data.decode().split("&"): + kvp = x.split("=") if len(kvp) == 2: ret[kvp[0]] = kvp[1] return ret - + except: self.logger.error(f"Error processing mucha request {data}") return None @@ -101,7 +120,7 @@ class MuchaServlet: def mucha_postprocess(self, data: dict) -> Optional[bytes]: try: urlencode = "" - for k,v in data.items(): + for k, v in data.items(): urlencode += f"{k}={v}&" return urlencode.encode() @@ -110,36 +129,44 @@ class MuchaServlet: self.logger.error("Error processing mucha response") return None -class MuchaAuthRequest(): + +class MuchaAuthRequest: def __init__(self, request: Dict) -> None: - self.gameVer = "" if "gameVer" not in request else request["gameVer"] # gameCd + boardType + countryCd + version - self.sendDate = "" if "sendDate" not in request else request["sendDate"] # %Y%m%d + self.gameVer = ( + "" if "gameVer" not in request else request["gameVer"] + ) # gameCd + boardType + countryCd + version + self.sendDate = ( + "" if "sendDate" not in request else request["sendDate"] + ) # %Y%m%d self.serialNum = "" if "serialNum" not in request else request["serialNum"] self.gameCd = "" if "gameCd" not in request else request["gameCd"] self.boardType = "" if "boardType" not in request else request["boardType"] self.boardId = "" if "boardId" not in request else request["boardId"] self.mac = "" if "mac" not in request else request["mac"] self.placeId = "" if "placeId" not in request else request["placeId"] - self.storeRouterIp = "" if "storeRouterIp" not in request else request["storeRouterIp"] + self.storeRouterIp = ( + "" if "storeRouterIp" not in request else request["storeRouterIp"] + ) self.countryCd = "" if "countryCd" not in request else request["countryCd"] self.useToken = "" if "useToken" not in request else request["useToken"] self.allToken = "" if "allToken" not in request else request["allToken"] -class MuchaAuthResponse(): + +class MuchaAuthResponse: def __init__(self, mucha_url: str) -> None: - self.RESULTS = "001" + self.RESULTS = "001" self.AUTH_INTERVAL = "86400" self.SERVER_TIME = datetime.strftime(datetime.now(), "%Y%m%d%H%M") self.UTC_SERVER_TIME = datetime.strftime(datetime.now(pytz.UTC), "%Y%m%d%H%M") - self.CHARGE_URL = f"https://{mucha_url}/charge/" + self.CHARGE_URL = f"https://{mucha_url}/charge/" self.FILE_URL = f"https://{mucha_url}/file/" self.URL_1 = f"https://{mucha_url}/url1/" self.URL_2 = f"https://{mucha_url}/url2/" self.URL_3 = f"https://{mucha_url}/url3/" - - self.PLACE_ID = "JPN123" - self.COUNTRY_CD = "JPN" + + self.PLACE_ID = "JPN123" + self.COUNTRY_CD = "JPN" self.SHOP_NAME = "TestShop!" self.SHOP_NICKNAME = "TestShop" self.AREA_0 = "008" @@ -150,7 +177,7 @@ class MuchaAuthResponse(): self.AREA_FULL_1 = "" self.AREA_FULL_2 = "" self.AREA_FULL_3 = "" - + self.SHOP_NAME_EN = "TestShop!" self.SHOP_NICKNAME_EN = "TestShop" self.AREA_0_EN = "008" @@ -162,23 +189,27 @@ class MuchaAuthResponse(): self.AREA_FULL_2_EN = "" self.AREA_FULL_3_EN = "" - self.PREFECTURE_ID = "1" + self.PREFECTURE_ID = "1" self.EXPIRATION_DATE = "null" self.USE_TOKEN = "0" self.CONSUME_TOKEN = "0" self.DONGLE_FLG = "1" self.FORCE_BOOT = "0" -class MuchaUpdateRequest(): + +class MuchaUpdateRequest: def __init__(self, request: Dict) -> None: self.gameVer = "" if "gameVer" not in request else request["gameVer"] self.gameCd = "" if "gameCd" not in request else request["gameCd"] self.serialNum = "" if "serialNum" not in request else request["serialNum"] self.countryCd = "" if "countryCd" not in request else request["countryCd"] self.placeId = "" if "placeId" not in request else request["placeId"] - self.storeRouterIp = "" if "storeRouterIp" not in request else request["storeRouterIp"] + self.storeRouterIp = ( + "" if "storeRouterIp" not in request else request["storeRouterIp"] + ) -class MuchaUpdateResponse(): + +class MuchaUpdateResponse: def __init__(self, game_ver: str, mucha_url: str) -> None: self.RESULTS = "001" self.UPDATE_VER_1 = game_ver @@ -194,7 +225,8 @@ class MuchaUpdateResponse(): self.USER_ID = "" self.PASSWORD = "" -class MuchaUpdateResponseStub(): + +class MuchaUpdateResponseStub: def __init__(self, game_ver: str) -> None: self.RESULTS = "001" self.UPDATE_VER_1 = game_ver diff --git a/core/title.py b/core/title.py index f201d8b..c9580d2 100644 --- a/core/title.py +++ b/core/title.py @@ -7,8 +7,9 @@ from core.config import CoreConfig from core.data import Data from core.utils import Utils -class TitleServlet(): - def __init__(self, core_cfg: CoreConfig, cfg_folder: str): + +class TitleServlet: + def __init__(self, core_cfg: CoreConfig, cfg_folder: str): super().__init__() self.config = core_cfg self.config_folder = cfg_folder @@ -18,47 +19,57 @@ class TitleServlet(): self.logger = logging.getLogger("title") if not hasattr(self.logger, "initialized"): log_fmt_str = "[%(asctime)s] Title | %(levelname)s | %(message)s" - log_fmt = logging.Formatter(log_fmt_str) + log_fmt = logging.Formatter(log_fmt_str) - fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.config.server.log_dir, "title"), when="d", backupCount=10) + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.config.server.log_dir, "title"), + when="d", + backupCount=10, + ) fileHandler.setFormatter(log_fmt) - + consoleHandler = logging.StreamHandler() consoleHandler.setFormatter(log_fmt) self.logger.addHandler(fileHandler) self.logger.addHandler(consoleHandler) - + self.logger.setLevel(core_cfg.title.loglevel) - coloredlogs.install(level=core_cfg.title.loglevel, logger=self.logger, fmt=log_fmt_str) + coloredlogs.install( + level=core_cfg.title.loglevel, logger=self.logger, fmt=log_fmt_str + ) self.logger.initialized = True - + plugins = Utils.get_all_titles() - + for folder, mod in plugins.items(): if hasattr(mod, "game_codes") and hasattr(mod, "index"): should_call_setup = True - - if hasattr(mod.index, "get_allnet_info"): + + if hasattr(mod.index, "get_allnet_info"): for code in mod.game_codes: - enabled, _, _ = mod.index.get_allnet_info(code, self.config, self.config_folder) + enabled, _, _ = mod.index.get_allnet_info( + code, self.config, self.config_folder + ) if enabled: handler_cls = mod.index(self.config, self.config_folder) - + if hasattr(handler_cls, "setup") and should_call_setup: handler_cls.setup() should_call_setup = False self.title_registry[code] = handler_cls - + else: self.logger.warn(f"Game {folder} has no get_allnet_info") - + else: self.logger.error(f"{folder} missing game_code or index in __init__.py") - self.logger.info(f"Serving {len(self.title_registry)} game codes on port {core_cfg.title.port}") + self.logger.info( + f"Serving {len(self.title_registry)} game codes on port {core_cfg.title.port}" + ) def render_GET(self, request: Request, endpoints: dict) -> bytes: code = endpoints["game"] @@ -66,7 +77,7 @@ class TitleServlet(): self.logger.warn(f"Unknown game code {code}") request.setResponseCode(404) return b"" - + index = self.title_registry[code] if not hasattr(index, "render_GET"): self.logger.warn(f"{code} does not dispatch GET") @@ -74,18 +85,20 @@ class TitleServlet(): return b"" return index.render_GET(request, endpoints["version"], endpoints["endpoint"]) - + def render_POST(self, request: Request, endpoints: dict) -> bytes: code = endpoints["game"] if code not in self.title_registry: self.logger.warn(f"Unknown game code {code}") request.setResponseCode(404) return b"" - + index = self.title_registry[code] if not hasattr(index, "render_POST"): self.logger.warn(f"{code} does not dispatch POST") request.setResponseCode(405) return b"" - return index.render_POST(request, int(endpoints["version"]), endpoints["endpoint"]) + return index.render_POST( + request, int(endpoints["version"]), endpoints["endpoint"] + ) diff --git a/core/utils.py b/core/utils.py index d4b7f16..24417bb 100644 --- a/core/utils.py +++ b/core/utils.py @@ -4,13 +4,14 @@ import logging import importlib from os import walk + class Utils: @classmethod def get_all_titles(cls) -> Dict[str, ModuleType]: ret: Dict[str, Any] = {} for root, dirs, files in walk("titles"): - for dir in dirs: + for dir in dirs: if not dir.startswith("__"): try: mod = importlib.import_module(f"titles.{dir}") diff --git a/dbutils.py b/dbutils.py index a2aa36b..ea9555a 100644 --- a/dbutils.py +++ b/dbutils.py @@ -4,16 +4,30 @@ from core.config import CoreConfig from core.data import Data from os import path -if __name__=='__main__': +if __name__ == "__main__": parser = argparse.ArgumentParser(description="Database utilities") - parser.add_argument("--config", "-c", type=str, help="Config folder to use", default="config") - parser.add_argument("--version", "-v", type=str, help="Version of the database to upgrade/rollback to") - parser.add_argument("--game", "-g", type=str, help="Game code of the game who's schema will be updated/rolled back. Ex. SDFE") + parser.add_argument( + "--config", "-c", type=str, help="Config folder to use", default="config" + ) + parser.add_argument( + "--version", + "-v", + type=str, + help="Version of the database to upgrade/rollback to", + ) + parser.add_argument( + "--game", + "-g", + type=str, + help="Game code of the game who's schema will be updated/rolled back. Ex. SDFE", + ) parser.add_argument("--email", "-e", type=str, help="Email for the new user") parser.add_argument("--old_ac", "-o", type=str, help="Access code to transfer from") parser.add_argument("--new_ac", "-n", type=str, help="Access code to transfer to") parser.add_argument("--force", "-f", type=bool, help="Force the action to happen") - parser.add_argument("action", type=str, help="DB Action, create, recreate, upgrade, or rollback") + parser.add_argument( + "action", type=str, help="DB Action, create, recreate, upgrade, or rollback" + ) args = parser.parse_args() cfg = CoreConfig() @@ -23,7 +37,7 @@ if __name__=='__main__': if args.action == "create": data.create_database() - + elif args.action == "recreate": data.recreate_database() diff --git a/index.py b/index.py index f545edb..d30d51f 100644 --- a/index.py +++ b/index.py @@ -12,6 +12,7 @@ from twisted.internet import reactor, endpoints from twisted.web.http import Request from routes import Mapper + class HttpDispatcher(resource.Resource): def __init__(self, cfg: CoreConfig, config_dir: str): super().__init__() @@ -20,73 +21,142 @@ class HttpDispatcher(resource.Resource): self.map_get = Mapper() self.map_post = Mapper() self.logger = logging.getLogger("core") - + self.allnet = AllnetServlet(cfg, config_dir) self.title = TitleServlet(cfg, config_dir) self.mucha = MuchaServlet(cfg, config_dir) - self.map_post.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_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'])) + self.map_post.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_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"]), + ) - 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_updatacheck', conditions=dict(method=['POST'])) + 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_updatacheck", + conditions=dict(method=["POST"]), + ) - self.map_get.connect("title_get", "/{game}/{version}/{endpoint:.*?}", controller="title", action="render_GET", conditions=dict(method=['GET']), requirements=dict(game=R"S...")) - self.map_post.connect("title_post", "/{game}/{version}/{endpoint:.*?}", controller="title", action="render_POST", conditions=dict(method=['POST']), requirements=dict(game=R"S...")) + self.map_get.connect( + "title_get", + "/{game}/{version}/{endpoint:.*?}", + controller="title", + action="render_GET", + conditions=dict(method=["GET"]), + requirements=dict(game=R"S..."), + ) + self.map_post.connect( + "title_post", + "/{game}/{version}/{endpoint:.*?}", + controller="title", + action="render_POST", + conditions=dict(method=["POST"]), + requirements=dict(game=R"S..."), + ) - def render_GET(self, request: Request) -> bytes: + def render_GET(self, request: Request) -> bytes: test = self.map_get.match(request.uri.decode()) if test is None: - self.logger.debug(f"Unknown GET endpoint {request.uri.decode()} from {request.getClientAddress().host} to port {request.getHost().port}") + self.logger.debug( + f"Unknown GET endpoint {request.uri.decode()} from {request.getClientAddress().host} to port {request.getHost().port}" + ) request.setResponseCode(404) return b"Endpoint not found." return self.dispatch(test, request) - def render_POST(self, request: Request) -> bytes: + def render_POST(self, request: Request) -> bytes: test = self.map_post.match(request.uri.decode()) if test is None: - self.logger.debug(f"Unknown POST endpoint {request.uri.decode()} from {request.getClientAddress().host} to port {request.getHost().port}") + self.logger.debug( + f"Unknown POST endpoint {request.uri.decode()} from {request.getClientAddress().host} to port {request.getHost().port}" + ) request.setResponseCode(404) return b"Endpoint not found." - + return self.dispatch(test, request) 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()}") + 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()}") + 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: return ret else: return b"" - + + if __name__ == "__main__": parser = argparse.ArgumentParser(description="ARTEMiS main entry point") - parser.add_argument("--config", "-c", type=str, default="config", help="Configuration folder") + parser.add_argument( + "--config", "-c", type=str, default="config", help="Configuration folder" + ) 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.\nDid you copy the example folder?") + print( + 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() @@ -95,56 +165,74 @@ if __name__ == "__main__": logger = logging.getLogger("core") log_fmt_str = "[%(asctime)s] Core | %(levelname)s | %(message)s" - log_fmt = logging.Formatter(log_fmt_str) + log_fmt = logging.Formatter(log_fmt_str) - fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(cfg.server.log_dir, "core"), when="d", backupCount=10) + 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 path.exists(cfg.server.log_dir): mkdir(cfg.server.log_dir) - + if not access(cfg.server.log_dir, W_OK): - logger.error(f"Log directory {cfg.server.log_dir} NOT writable, please check permissions") + logger.error( + f"Log directory {cfg.server.log_dir} NOT writable, please check permissions" + ) exit(1) 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}" + 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}" 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}" + 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}"\ + 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, 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))) + 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)) - - reactor.run() # type: ignore \ No newline at end of file + 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) + ) + + reactor.run() # type: ignore diff --git a/read.py b/read.py index 1f89cb6..538198a 100644 --- a/read.py +++ b/read.py @@ -12,56 +12,64 @@ from typing import List, Optional from core import CoreConfig from core.utils import Utils -class BaseReader(): - def __init__(self, config: CoreConfig, version: int, bin_dir: Optional[str], opt_dir: Optional[str], extra: Optional[str]) -> None: + +class BaseReader: + def __init__( + self, + config: CoreConfig, + version: int, + bin_dir: Optional[str], + opt_dir: Optional[str], + extra: Optional[str], + ) -> None: self.logger = logging.getLogger("reader") self.config = config self.bin_dir = bin_dir self.opt_dir = opt_dir self.version = version self.extra = extra - - + def get_data_directories(self, directory: str) -> List[str]: ret: List[str] = [] for root, dirs, files in os.walk(directory): - for dir in dirs: - if re.fullmatch("[A-Z0-9]{4,4}", dir) is not None: - ret.append(f"{root}/{dir}") - + for dir in dirs: + if re.fullmatch("[A-Z0-9]{4,4}", dir) is not None: + ret.append(f"{root}/{dir}") + return ret + if __name__ == "__main__": - parser = argparse.ArgumentParser(description='Import Game Information') + parser = argparse.ArgumentParser(description="Import Game Information") parser.add_argument( - '--series', - action='store', + "--series", + action="store", type=str, required=True, - help='The game series we are importing.', + help="The game series we are importing.", ) parser.add_argument( - '--version', - dest='version', - action='store', + "--version", + dest="version", + action="store", type=int, required=True, - help='The game version we are importing.', + help="The game version we are importing.", ) parser.add_argument( - '--binfolder', - dest='bin', - action='store', + "--binfolder", + dest="bin", + action="store", type=str, - help='Folder containing A000 base data', + help="Folder containing A000 base data", ) parser.add_argument( - '--optfolder', - dest='opt', - action='store', + "--optfolder", + dest="opt", + action="store", type=str, - help='Folder containing Option data folders', + help="Folder containing Option data folders", ) parser.add_argument( "--config", @@ -86,15 +94,17 @@ if __name__ == "__main__": log_fmt = logging.Formatter(log_fmt_str) logger = logging.getLogger("reader") - fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(config.server.log_dir, "reader"), when="d", backupCount=10) + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(config.server.log_dir, "reader"), 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 config.server.is_develop else logging.INFO logger.setLevel(log_lv) coloredlogs.install(level=log_lv, logger=logger, fmt=log_fmt_str) @@ -102,8 +112,8 @@ if __name__ == "__main__": if args.series is None or args.version is None: logger.error("Game or version not specified") parser.print_help() - exit(1) - + exit(1) + if args.bin is None and args.opt is None: logger.error("Must specify either bin or opt directory") parser.print_help() @@ -113,7 +123,7 @@ if __name__ == "__main__": bin_arg = args.bin[:-1] else: bin_arg = args.bin - + if args.opt is not None and (args.opt.endswith("\\") or args.opt.endswith("/")): opt_arg = args.opt[:-1] else: @@ -127,5 +137,5 @@ if __name__ == "__main__": if args.series in mod.game_codes: handler = mod.reader(config, args.version, bin_arg, opt_arg, args.extra) handler.read() - + logger.info("Done") diff --git a/titles/chuni/air.py b/titles/chuni/air.py index 46f8337..b9bc1d3 100644 --- a/titles/chuni/air.py +++ b/titles/chuni/air.py @@ -5,12 +5,13 @@ from titles.chuni.base import ChuniBase from titles.chuni.const import ChuniConstants from titles.chuni.config import ChuniConfig + class ChuniAir(ChuniBase): def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None: super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_AIR - + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + ret = super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.10.00" - return ret \ No newline at end of file + return ret diff --git a/titles/chuni/airplus.py b/titles/chuni/airplus.py index 77498fb..f0d8224 100644 --- a/titles/chuni/airplus.py +++ b/titles/chuni/airplus.py @@ -5,12 +5,13 @@ from titles.chuni.base import ChuniBase from titles.chuni.const import ChuniConstants from titles.chuni.config import ChuniConfig + class ChuniAirPlus(ChuniBase): def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None: super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_AIR_PLUS - + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + ret = super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.15.00" - return ret \ No newline at end of file + return ret diff --git a/titles/chuni/amazon.py b/titles/chuni/amazon.py index d822665..b765c2f 100644 --- a/titles/chuni/amazon.py +++ b/titles/chuni/amazon.py @@ -7,12 +7,13 @@ from titles.chuni.base import ChuniBase from titles.chuni.const import ChuniConstants from titles.chuni.config import ChuniConfig + class ChuniAmazon(ChuniBase): def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None: super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_AMAZON - + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + 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 5e901cd..ea8d704 100644 --- a/titles/chuni/amazonplus.py +++ b/titles/chuni/amazonplus.py @@ -7,12 +7,13 @@ from titles.chuni.base import ChuniBase from titles.chuni.const import ChuniConstants from titles.chuni.config import ChuniConfig + class ChuniAmazonPlus(ChuniBase): def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None: super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_AMAZON_PLUS - + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + 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 65d991d..f66eac8 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -11,7 +11,8 @@ from titles.chuni.const import ChuniConstants from titles.chuni.database import ChuniData from titles.chuni.config import ChuniConfig -class ChuniBase(): + +class ChuniBase: def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None: self.core_cfg = core_cfg self.game_cfg = game_cfg @@ -22,32 +23,31 @@ class ChuniBase(): self.version = ChuniConstants.VER_CHUNITHM def handle_game_login_api_request(self, data: Dict) -> Dict: - #self.data.base.log_event("chuni", "login", logging.INFO, {"version": self.version, "user": data["userId"]}) - return { "returnCode": 1 } - + # self.data.base.log_event("chuni", "login", logging.INFO, {"version": self.version, "user": data["userId"]}) + return {"returnCode": 1} + 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 } + # self.data.base.log_event("chuni", "logout", logging.INFO, {"version": self.version, "user": data["userId"]}) + return {"returnCode": 1} def handle_get_game_charge_api_request(self, data: Dict) -> Dict: game_charge_list = self.data.static.get_enabled_charges(self.version) charges = [] - for x in range(len(game_charge_list)): - charges.append({ - "orderId": x, - "chargeId": game_charge_list[x]["chargeId"], - "price": 1, - "startDate": "2017-12-05 07:00:00.0", - "endDate": "2099-12-31 00:00:00.0", - "salePrice": 1, - "saleStartDate": "2017-12-05 07:00:00.0", - "saleEndDate": "2099-12-31 00:00:00.0" - }) - return { - "length": len(charges), - "gameChargeList": charges - } + for x in range(len(game_charge_list)): + charges.append( + { + "orderId": x, + "chargeId": game_charge_list[x]["chargeId"], + "price": 1, + "startDate": "2017-12-05 07:00:00.0", + "endDate": "2099-12-31 00:00:00.0", + "salePrice": 1, + "saleStartDate": "2017-12-05 07:00:00.0", + "saleEndDate": "2099-12-31 00:00:00.0", + } + ) + return {"length": len(charges), "gameChargeList": charges} def handle_get_game_event_api_request(self, data: Dict) -> Dict: game_events = self.data.static.get_enabled_events(self.version) @@ -62,26 +62,30 @@ class ChuniBase(): event_list.append(tmp) return { - "type": data["type"], - "length": len(event_list), - "gameEventList": event_list + "type": data["type"], + "length": len(event_list), + "gameEventList": event_list, } def handle_get_game_idlist_api_request(self, data: Dict) -> Dict: - return { "type": data["type"], "length": 0, "gameIdlistList": [] } + return {"type": data["type"], "length": 0, "gameIdlistList": []} def handle_get_game_message_api_request(self, data: Dict) -> Dict: - return { "type": data["type"], "length": "0", "gameMessageList": [] } + return {"type": data["type"], "length": "0", "gameMessageList": []} def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: - return { "type": data["type"], "gameRankingList": [] } + return {"type": data["type"], "gameRankingList": []} def handle_get_game_sale_api_request(self, data: Dict) -> Dict: - return { "type": data["type"], "length": 0, "gameSaleList": [] } + return {"type": data["type"], "length": 0, "gameSaleList": []} def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - reboot_start = datetime.strftime(datetime.now() - timedelta(hours=4), self.date_time_format) - reboot_end = datetime.strftime(datetime.now() - timedelta(hours=3), self.date_time_format) + reboot_start = datetime.strftime( + datetime.now() - timedelta(hours=4), self.date_time_format + ) + reboot_end = datetime.strftime( + datetime.now() - timedelta(hours=3), self.date_time_format + ) return { "gameSetting": { "dataVersion": "1.00.00", @@ -94,15 +98,17 @@ class ChuniBase(): "maxCountItem": 300, "maxCountMusic": 300, }, - "isDumpUpload": "false", - "isAou": "false", + "isDumpUpload": "false", + "isAou": "false", } def handle_get_user_activity_api_request(self, data: Dict) -> Dict: - user_activity_list = self.data.profile.get_profile_activity(data["userId"], data["kind"]) - + user_activity_list = self.data.profile.get_profile_activity( + data["userId"], data["kind"] + ) + activity_list = [] - + for activity in user_activity_list: tmp = activity._asdict() tmp.pop("user") @@ -111,15 +117,16 @@ class ChuniBase(): activity_list.append(tmp) return { - "userId": data["userId"], - "length": len(activity_list), - "kind": data["kind"], - "userActivityList": activity_list + "userId": data["userId"], + "length": len(activity_list), + "kind": data["kind"], + "userActivityList": activity_list, } def handle_get_user_character_api_request(self, data: Dict) -> Dict: characters = self.data.item.get_characters(data["userId"]) - if characters is None: return {} + if characters is None: + return {} next_idx = -1 characterList = [] @@ -131,15 +138,17 @@ class ChuniBase(): if len(characterList) >= int(data["maxCount"]): break - - if len(characterList) >= int(data["maxCount"]) and len(characters) > int(data["maxCount"]) + int(data["nextIndex"]): + + if len(characterList) >= int(data["maxCount"]) and len(characters) > int( + data["maxCount"] + ) + int(data["nextIndex"]): next_idx = int(data["maxCount"]) + int(data["nextIndex"]) + 1 - + return { - "userId": data["userId"], + "userId": data["userId"], "length": len(characterList), - "nextIndex": next_idx, - "userCharacterList": characterList + "nextIndex": next_idx, + "userCharacterList": characterList, } def handle_get_user_charge_api_request(self, data: Dict) -> Dict: @@ -153,21 +162,21 @@ class ChuniBase(): charge_list.append(tmp) return { - "userId": data["userId"], + "userId": data["userId"], "length": len(charge_list), - "userChargeList": charge_list + "userChargeList": charge_list, } 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: + if user_course_list is None: return { - "userId": data["userId"], + "userId": data["userId"], "length": 0, - "nextIndex": -1, - "userCourseList": [] + "nextIndex": -1, + "userCourseList": [], } - + course_list = [] next_idx = int(data["nextIndex"]) max_ct = int(data["maxCount"]) @@ -180,51 +189,48 @@ class ChuniBase(): if len(user_course_list) >= max_ct: break - + if len(user_course_list) >= max_ct: next_idx = next_idx + max_ct else: next_idx = -1 - + return { - "userId": data["userId"], + "userId": data["userId"], "length": len(course_list), - "nextIndex": next_idx, - "userCourseList": course_list + "nextIndex": next_idx, + "userCourseList": course_list, } def handle_get_user_data_api_request(self, data: Dict) -> Dict: p = self.data.profile.get_profile_data(data["userId"], self.version) - if p is None: return {} + if p is None: + return {} profile = p._asdict() profile.pop("id") profile.pop("user") profile.pop("version") - return { - "userId": data["userId"], - "userData": profile - } + return {"userId": data["userId"], "userData": profile} 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 {} + if p is None: + return {} profile = p._asdict() profile.pop("id") profile.pop("user") profile.pop("version") - return { - "userId": data["userId"], - "userDataEx": profile - } + return {"userId": data["userId"], "userDataEx": profile} 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 {} - + if user_duel_list is None: + return {} + duel_list = [] for duel in user_duel_list: tmp = duel._asdict() @@ -233,18 +239,18 @@ class ChuniBase(): duel_list.append(tmp) return { - "userId": data["userId"], + "userId": data["userId"], "length": len(duel_list), - "userDuelList": duel_list + "userDuelList": duel_list, } def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict: return { - "userId": data["userId"], + "userId": data["userId"], "length": 0, - "kind": data["kind"], - "nextIndex": -1, - "userFavoriteItemList": [] + "kind": data["kind"], + "nextIndex": -1, + "userFavoriteItemList": [], } def handle_get_user_favorite_music_api_request(self, data: Dict) -> Dict: @@ -252,22 +258,23 @@ class ChuniBase(): This is handled via the webui, which we don't have right now """ - return { - "userId": data["userId"], - "length": 0, - "userFavoriteMusicList": [] - } + return {"userId": data["userId"], "length": 0, "userFavoriteMusicList": []} 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 = self.data.item.get_items(data["userId"], kind) - if user_item_list is None or len(user_item_list) == 0: - return {"userId": data["userId"], "nextIndex": -1, "itemKind": kind, "userItemList": []} + if user_item_list is None or len(user_item_list) == 0: + return { + "userId": data["userId"], + "nextIndex": -1, + "itemKind": kind, + "userItemList": [], + } items: list[Dict[str, Any]] = [] - for i in range(next_idx, len(user_item_list)): + for i in range(next_idx, len(user_item_list)): tmp = user_item_list[i]._asdict() tmp.pop("user") tmp.pop("id") @@ -277,38 +284,47 @@ class ChuniBase(): xout = kind * 10000000000 + next_idx + len(items) - if len(items) < int(data["maxCount"]): nextIndex = 0 - else: nextIndex = xout + if len(items) < int(data["maxCount"]): + nextIndex = 0 + else: + nextIndex = xout - return {"userId": data["userId"], "nextIndex": nextIndex, "itemKind": kind, "length": len(items), "userItemList": items} + return { + "userId": data["userId"], + "nextIndex": nextIndex, + "itemKind": kind, + "length": len(items), + "userItemList": items, + } def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: """ Unsure how to get this to trigger... """ return { - "userId": data["userId"], + "userId": data["userId"], "length": 2, "userLoginBonusList": [ { - "presetId": '10', - "bonusCount": '0', - "lastUpdateDate": "1970-01-01 09:00:00", - "isWatched": "true" + "presetId": "10", + "bonusCount": "0", + "lastUpdateDate": "1970-01-01 09:00:00", + "isWatched": "true", }, { - "presetId": '20', - "bonusCount": '0', - "lastUpdateDate": "1970-01-01 09:00:00", - "isWatched": "true" + "presetId": "20", + "bonusCount": "0", + "lastUpdateDate": "1970-01-01 09:00:00", + "isWatched": "true", }, - ] + ], } 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 {} - + if user_map_list is None: + return {} + map_list = [] for map in user_map_list: tmp = map._asdict() @@ -317,19 +333,19 @@ class ChuniBase(): map_list.append(tmp) return { - "userId": data["userId"], + "userId": data["userId"], "length": len(map_list), - "userMapList": map_list + "userMapList": map_list, } 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: + if music_detail is None: return { - "userId": data["userId"], - "length": 0, + "userId": data["userId"], + "length": 0, "nextIndex": -1, - "userMusicList": [] #240 + "userMusicList": [], # 240 } song_list = [] next_idx = int(data["nextIndex"]) @@ -340,66 +356,60 @@ class ChuniBase(): tmp = music_detail[x]._asdict() tmp.pop("user") tmp.pop("id") - + for song in song_list: if song["userMusicDetailList"][0]["musicId"] == tmp["musicId"]: found = True song["userMusicDetailList"].append(tmp) song["length"] = len(song["userMusicDetailList"]) - + if not found: - song_list.append({ - "length": 1, - "userMusicDetailList": [tmp] - }) - + song_list.append({"length": 1, "userMusicDetailList": [tmp]}) + if len(song_list) >= max_ct: break - + if len(song_list) >= max_ct: next_idx += max_ct else: next_idx = 0 return { - "userId": data["userId"], - "length": len(song_list), + "userId": data["userId"], + "length": len(song_list), "nextIndex": next_idx, - "userMusicList": song_list #240 + "userMusicList": song_list, # 240 } 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") option.pop("user") - return { - "userId": data["userId"], - "userGameOption": option - } + return {"userId": data["userId"], "userGameOption": option} 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") option.pop("user") - return { - "userId": data["userId"], - "userGameOptionEx": option - } + return {"userId": data["userId"], "userGameOptionEx": option} def read_wtf8(self, src): return bytes([ord(c) for c in src]).decode("utf-8") 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 = self.data.item.get_character(data["userId"], profile["characterId"]) - + if profile is None: + return None + profile_character = self.data.item.get_character( + data["userId"], profile["characterId"] + ) + if profile_character is None: chara = {} else: @@ -408,8 +418,8 @@ class ChuniBase(): chara.pop("user") return { - "userId": data["userId"], - # Current Login State + "userId": data["userId"], + # Current Login State "isLogin": False, "lastLoginDate": profile["lastPlayDate"], # User Profile @@ -421,14 +431,14 @@ class ChuniBase(): "lastGameId": profile["lastGameId"], "lastRomVersion": profile["lastRomVersion"], "lastDataVersion": profile["lastDataVersion"], - "lastPlayDate": profile["lastPlayDate"], - "trophyId": profile["trophyId"], + "lastPlayDate": profile["lastPlayDate"], + "trophyId": profile["trophyId"], "nameplateId": profile["nameplateId"], # Current Selected Character "userCharacter": chara, # User Game Options - "playerLevel": profile["playerLevel"], - "rating": profile["rating"], + "playerLevel": profile["playerLevel"], + "rating": profile["rating"], "headphone": profile["headphone"], "chargeState": "1", "userNameEx": profile["userName"], @@ -436,7 +446,7 @@ class ChuniBase(): def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict: recet_rating_list = self.data.profile.get_profile_recent_rating(data["userId"]) - if recet_rating_list is None: + if recet_rating_list is None: return { "userId": data["userId"], "length": 0, @@ -459,11 +469,8 @@ class ChuniBase(): def handle_get_user_team_api_request(self, data: Dict) -> Dict: # TODO: Team - return { - "userId": data["userId"], - "teamId": 0 - } - + return {"userId": data["userId"], "teamId": 0} + def handle_get_team_course_setting_api_request(self, data: Dict) -> Dict: return { "userId": data["userId"], @@ -486,19 +493,30 @@ class ChuniBase(): if "userData" in upsert: try: - upsert["userData"][0]["userName"] = self.read_wtf8(upsert["userData"][0]["userName"]) - except: pass + upsert["userData"][0]["userName"] = self.read_wtf8( + upsert["userData"][0]["userName"] + ) + except: + pass - self.data.profile.put_profile_data(user_id, self.version, upsert["userData"][0]) + self.data.profile.put_profile_data( + user_id, self.version, upsert["userData"][0] + ) if "userDataEx" in upsert: - self.data.profile.put_profile_data_ex(user_id, self.version, upsert["userDataEx"][0]) + self.data.profile.put_profile_data_ex( + user_id, self.version, upsert["userDataEx"][0] + ) if "userGameOption" in upsert: self.data.profile.put_profile_option(user_id, upsert["userGameOption"][0]) if "userGameOptionEx" in upsert: - self.data.profile.put_profile_option_ex(user_id, upsert["userGameOptionEx"][0]) + self.data.profile.put_profile_option_ex( + user_id, upsert["userGameOptionEx"][0] + ) if "userRecentRatingList" in upsert: - self.data.profile.put_profile_recent_rating(user_id, upsert["userRecentRatingList"]) - + self.data.profile.put_profile_recent_rating( + user_id, upsert["userRecentRatingList"] + ) + if "userCharacterList" in upsert: for character in upsert["userCharacterList"]: self.data.item.put_character(user_id, character) @@ -514,7 +532,7 @@ class ChuniBase(): if "userDuelList" in upsert: for duel in upsert["userDuelList"]: self.data.item.put_duel(user_id, duel) - + if "userItemList" in upsert: for item in upsert["userItemList"]: self.data.item.put_item(user_id, item) @@ -522,23 +540,23 @@ class ChuniBase(): if "userActivityList" in upsert: for activity in upsert["userActivityList"]: self.data.profile.put_profile_activity(user_id, activity) - + if "userChargeList" in upsert: for charge in upsert["userChargeList"]: self.data.profile.put_profile_charge(user_id, charge) - + if "userMusicDetailList" in upsert: for song in upsert["userMusicDetailList"]: self.data.score.put_score(user_id, song) - + if "userPlaylogList" in upsert: for playlog in upsert["userPlaylogList"]: self.data.score.put_playlog(user_id, playlog) - + if "userTeamPoint" in upsert: # TODO: team stuff pass - + if "userMapAreaList" in upsert: for map_area in upsert["userMapAreaList"]: self.data.item.put_map_area(user_id, map_area) @@ -551,22 +569,22 @@ class ChuniBase(): for emoney in upsert["userEmoneyList"]: self.data.profile.put_profile_emoney(user_id, emoney) - return { "returnCode": "1" } + return {"returnCode": "1"} def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: - return { "returnCode": "1" } + return {"returnCode": "1"} def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: - return { "returnCode": "1" } + return {"returnCode": "1"} def handle_upsert_client_develop_api_request(self, data: Dict) -> Dict: - return { "returnCode": "1" } + return {"returnCode": "1"} def handle_upsert_client_error_api_request(self, data: Dict) -> Dict: - return { "returnCode": "1" } + return {"returnCode": "1"} def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: - return { "returnCode": "1" } + return {"returnCode": "1"} def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict: - return { "returnCode": "1" } + return {"returnCode": "1"} diff --git a/titles/chuni/config.py b/titles/chuni/config.py index f3a0a7e..c4a351f 100644 --- a/titles/chuni/config.py +++ b/titles/chuni/config.py @@ -1,36 +1,49 @@ from core.config import CoreConfig from typing import Dict -class ChuniServerConfig(): + +class ChuniServerConfig: def __init__(self, parent_config: "ChuniConfig") -> None: self.__config = parent_config - + @property def enable(self) -> bool: - return CoreConfig.get_config_field(self.__config, 'chuni', 'server', 'enable', default=True) - + return CoreConfig.get_config_field( + self.__config, "chuni", "server", "enable", default=True + ) + @property def loglevel(self) -> int: - return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'chuni', 'server', 'loglevel', default="info")) + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "chuni", "server", "loglevel", default="info" + ) + ) -class ChuniCryptoConfig(): + +class ChuniCryptoConfig: def __init__(self, parent_config: "ChuniConfig") -> None: self.__config = parent_config - + @property def keys(self) -> Dict: """ in the form of: - internal_version: [key, iv] + internal_version: [key, iv] all values are hex strings """ - return CoreConfig.get_config_field(self.__config, 'chuni', 'crypto', 'keys', default={}) + return CoreConfig.get_config_field( + self.__config, "chuni", "crypto", "keys", default={} + ) @property def encrypted_only(self) -> bool: - return CoreConfig.get_config_field(self.__config, 'chuni', 'crypto', 'encrypted_only', default=False) + return CoreConfig.get_config_field( + self.__config, "chuni", "crypto", "encrypted_only", default=False + ) + class ChuniConfig(dict): def __init__(self) -> None: self.server = ChuniServerConfig(self) - self.crypto = ChuniCryptoConfig(self) \ No newline at end of file + self.crypto = ChuniCryptoConfig(self) diff --git a/titles/chuni/const.py b/titles/chuni/const.py index 3a111f8..6ab3cc3 100644 --- a/titles/chuni/const.py +++ b/titles/chuni/const.py @@ -1,4 +1,4 @@ -class ChuniConstants(): +class ChuniConstants: GAME_CODE = "SDBT" GAME_CODE_NEW = "SDHD" @@ -8,7 +8,7 @@ class ChuniConstants(): VER_CHUNITHM_PLUS = 1 VER_CHUNITHM_AIR = 2 VER_CHUNITHM_AIR_PLUS = 3 - VER_CHUNITHM_STAR = 4 + VER_CHUNITHM_STAR = 4 VER_CHUNITHM_STAR_PLUS = 5 VER_CHUNITHM_AMAZON = 6 VER_CHUNITHM_AMAZON_PLUS = 7 @@ -18,8 +18,21 @@ class ChuniConstants(): VER_CHUNITHM_NEW = 11 VER_CHUNITHM_NEW_PLUS = 12 - VERSION_NAMES = ["Chunithm", "Chunithm+", "Chunithm Air", "Chunithm Air+", "Chunithm Star", "Chunithm Star+", "Chunithm Amazon", - "Chunithm Amazon+", "Chunithm Crystal", "Chunithm Crystal+", "Chunithm Paradise", "Chunithm New!!", "Chunithm New!!+"] + VERSION_NAMES = [ + "Chunithm", + "Chunithm+", + "Chunithm Air", + "Chunithm Air+", + "Chunithm Star", + "Chunithm Star+", + "Chunithm Amazon", + "Chunithm Amazon+", + "Chunithm Crystal", + "Chunithm Crystal+", + "Chunithm Paradise", + "Chunithm New!!", + "Chunithm New!!+", + ] @classmethod def game_ver_to_string(cls, ver: int): diff --git a/titles/chuni/crystal.py b/titles/chuni/crystal.py index d492f0b..a727ac3 100644 --- a/titles/chuni/crystal.py +++ b/titles/chuni/crystal.py @@ -7,12 +7,13 @@ from titles.chuni.base import ChuniBase from titles.chuni.const import ChuniConstants from titles.chuni.config import ChuniConfig + class ChuniCrystal(ChuniBase): def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None: super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_CRYSTAL - + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + 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 b06eb5b..fbb3969 100644 --- a/titles/chuni/crystalplus.py +++ b/titles/chuni/crystalplus.py @@ -7,12 +7,13 @@ from titles.chuni.base import ChuniBase from titles.chuni.const import ChuniConstants from titles.chuni.config import ChuniConfig + class ChuniCrystalPlus(ChuniBase): def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None: super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS - + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + 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 c55149b..eeb588c 100644 --- a/titles/chuni/database.py +++ b/titles/chuni/database.py @@ -2,6 +2,7 @@ from core.data import Data from core.config import CoreConfig from titles.chuni.schema import * + class ChuniData(Data): def __init__(self, cfg: CoreConfig) -> None: super().__init__(cfg) @@ -9,4 +10,4 @@ class ChuniData(Data): self.item = ChuniItemData(cfg, self.session) self.profile = ChuniProfileData(cfg, self.session) self.score = ChuniScoreData(cfg, self.session) - self.static = ChuniStaticData(cfg, self.session) \ No newline at end of file + self.static = ChuniStaticData(cfg, self.session) diff --git a/titles/chuni/index.py b/titles/chuni/index.py index 5ee03a4..a8b581e 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -28,12 +28,15 @@ from titles.chuni.paradise import ChuniParadise from titles.chuni.new import ChuniNew from titles.chuni.newplus import ChuniNewPlus -class ChuniServlet(): + +class ChuniServlet: def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: self.core_cfg = core_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.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}")) + ) self.versions = [ ChuniBase(core_cfg, self.game_cfg), @@ -56,33 +59,47 @@ class ChuniServlet(): if not hasattr(self.logger, "inited"): log_fmt_str = "[%(asctime)s] Chunithm | %(levelname)s | %(message)s" log_fmt = logging.Formatter(log_fmt_str) - fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.core_cfg.server.log_dir, "chuni"), encoding='utf8', - when="d", backupCount=10) + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.core_cfg.server.log_dir, "chuni"), + encoding="utf8", + when="d", + backupCount=10, + ) fileHandler.setFormatter(log_fmt) - + consoleHandler = logging.StreamHandler() consoleHandler.setFormatter(log_fmt) self.logger.addHandler(fileHandler) self.logger.addHandler(consoleHandler) - + self.logger.setLevel(self.game_cfg.server.loglevel) - coloredlogs.install(level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str) + coloredlogs.install( + level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str + ) self.logger.inited = True @classmethod - def get_allnet_info(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str, str]: + def get_allnet_info( + cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str + ) -> Tuple[bool, str, str]: game_cfg = ChuniConfig() if path.exists(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"): - game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"))) + game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}")) + ) if not game_cfg.server.enable: return (False, "", "") - + if core_cfg.server.is_develop: - return (True, f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/", "") - + return ( + True, + f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/", + "", + ) + return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", "") def render_POST(self, request: Request, version: int, url_path: str) -> bytes: @@ -95,67 +112,75 @@ class ChuniServlet(): internal_ver = 0 endpoint = url_split[len(url_split) - 1] - if version < 105: # 1.0 + if version < 105: # 1.0 internal_ver = ChuniConstants.VER_CHUNITHM - elif version >= 105 and version < 110: # Plus + elif version >= 105 and version < 110: # Plus internal_ver = ChuniConstants.VER_CHUNITHM_PLUS - elif version >= 110 and version < 115: # Air + elif version >= 110 and version < 115: # Air internal_ver = ChuniConstants.VER_CHUNITHM_AIR - elif version >= 115 and version < 120: # Air Plus + elif version >= 115 and version < 120: # Air Plus internal_ver = ChuniConstants.VER_CHUNITHM_AIR_PLUS - elif version >= 120 and version < 125: # Star + elif version >= 120 and version < 125: # Star internal_ver = ChuniConstants.VER_CHUNITHM_STAR - elif version >= 125 and version < 130: # Star Plus + elif version >= 125 and version < 130: # Star Plus internal_ver = ChuniConstants.VER_CHUNITHM_STAR_PLUS - elif version >= 130 and version < 135: # Amazon + elif version >= 130 and version < 135: # Amazon internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON - elif version >= 135 and version < 140: # Amazon Plus + elif version >= 135 and version < 140: # Amazon Plus internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON_PLUS - elif version >= 140 and version < 145: # Crystal + elif version >= 140 and version < 145: # Crystal internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL - elif version >= 145 and version < 150: # Crystal Plus + elif version >= 145 and version < 150: # Crystal Plus internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS - elif version >= 150 and version < 200: # Paradise + elif version >= 150 and version < 200: # Paradise internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE - elif version >= 200 and version < 205: # New + elif version >= 200 and version < 205: # New internal_ver = ChuniConstants.VER_CHUNITHM_NEW - elif version >= 205 and version < 210: # New Plus + elif version >= 205 and version < 210: # New Plus internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32: - # If we get a 32 character long hex string, it's a hash and we're - # doing encrypted. The likelyhood of false positives is low but + # If we get a 32 character long hex string, it's a hash and we're + # doing encrypted. The likelyhood of false positives is low but # technically not 0 endpoint = request.getHeader("User-Agent").split("#")[0] try: crypt = AES.new( - bytes.fromhex(self.game_cfg.crypto.keys[str(internal_ver)][0]), - AES.MODE_CBC, - bytes.fromhex(self.game_cfg.crypto.keys[str(internal_ver)][1]) + bytes.fromhex(self.game_cfg.crypto.keys[str(internal_ver)][0]), + AES.MODE_CBC, + bytes.fromhex(self.game_cfg.crypto.keys[str(internal_ver)][1]), ) req_raw = crypt.decrypt(req_raw) except: - self.logger.error(f"Failed to decrypt v{version} request to {endpoint} -> {req_raw}") + self.logger.error( + f"Failed to decrypt v{version} request to {endpoint} -> {req_raw}" + ) return zlib.compress(b'{"stat": "0"}') encrtped = True - + if not encrtped and self.game_cfg.crypto.encrypted_only: - self.logger.error(f"Unencrypted v{version} {endpoint} request, but config is set to encrypted only: {req_raw}") + self.logger.error( + f"Unencrypted v{version} {endpoint} request, but config is set to encrypted only: {req_raw}" + ) return zlib.compress(b'{"stat": "0"}') - try: + try: unzip = zlib.decompress(req_raw) - + except zlib.error as e: - self.logger.error(f"Failed to decompress v{version} {endpoint} request -> {e}") + self.logger.error( + f"Failed to decompress v{version} {endpoint} request -> {e}" + ) return b"" - + req_data = json.loads(unzip) - - self.logger.info(f"v{version} {endpoint} request from {request.getClientAddress().host}") + + self.logger.info( + f"v{version} {endpoint} request from {request.getClientAddress().host}" + ) self.logger.debug(req_data) func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" @@ -163,9 +188,8 @@ class ChuniServlet(): if not hasattr(self.versions[internal_ver], func_to_find): self.logger.warning(f"Unhandled v{version} request {endpoint}") resp = {"returnCode": 1} - - else: + else: try: handler = getattr(self.versions[internal_ver], func_to_find) resp = handler(req_data) @@ -173,23 +197,23 @@ class ChuniServlet(): except Exception as e: self.logger.error(f"Error handling v{version} method {endpoint} - {e}") return zlib.compress(b'{"stat": "0"}') - + if resp == None: - resp = {'returnCode': 1} - + resp = {"returnCode": 1} + self.logger.debug(f"Response {resp}") - + zipped = zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) - + if not encrtped: return zipped padded = pad(zipped, 16) crypt = AES.new( - bytes.fromhex(self.game_cfg.crypto.keys[str(internal_ver)][0]), - AES.MODE_CBC, - bytes.fromhex(self.game_cfg.crypto.keys[str(internal_ver)][1]) + bytes.fromhex(self.game_cfg.crypto.keys[str(internal_ver)][0]), + AES.MODE_CBC, + bytes.fromhex(self.game_cfg.crypto.keys[str(internal_ver)][1]), ) return crypt.encrypt(padded) diff --git a/titles/chuni/new.py b/titles/chuni/new.py index 4b5ba2d..03ad305 100644 --- a/titles/chuni/new.py +++ b/titles/chuni/new.py @@ -9,13 +9,9 @@ from titles.chuni.database import ChuniData from titles.chuni.base import ChuniBase from titles.chuni.config import ChuniConfig -class ChuniNew(ChuniBase): - ITEM_TYPE = { - "character": 20, - "story": 21, - "card": 22 - } +class ChuniNew(ChuniBase): + ITEM_TYPE = {"character": 20, "story": 21, "card": 22} def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None: self.core_cfg = core_cfg @@ -25,12 +21,20 @@ class ChuniNew(ChuniBase): self.logger = logging.getLogger("chuni") self.game = ChuniConstants.GAME_CODE self.version = ChuniConstants.VER_CHUNITHM_NEW - + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - match_start = datetime.strftime(datetime.now() - timedelta(hours=10), self.date_time_format) - match_end = datetime.strftime(datetime.now() + timedelta(hours=10), self.date_time_format) - reboot_start = datetime.strftime(datetime.now() - timedelta(hours=11), self.date_time_format) - reboot_end = datetime.strftime(datetime.now() - timedelta(hours=10), self.date_time_format) + match_start = datetime.strftime( + datetime.now() - timedelta(hours=10), self.date_time_format + ) + match_end = datetime.strftime( + datetime.now() + timedelta(hours=10), self.date_time_format + ) + reboot_start = datetime.strftime( + datetime.now() - timedelta(hours=11), self.date_time_format + ) + reboot_end = datetime.strftime( + datetime.now() - timedelta(hours=10), self.date_time_format + ) return { "gameSetting": { "isMaintenance": "false", @@ -52,16 +56,16 @@ class ChuniNew(ChuniBase): "udpHolePunchUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", "reflectorUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", }, - "isDumpUpload": "false", - "isAou": "false", + "isDumpUpload": "false", + "isAou": "false", } - + def handle_delete_token_api_request(self, data: Dict) -> Dict: - return { "returnCode": "1" } - + return {"returnCode": "1"} + def handle_create_token_api_request(self, data: Dict) -> Dict: - return { "returnCode": "1" } - + return {"returnCode": "1"} + def handle_get_user_map_area_api_request(self, data: Dict) -> Dict: user_map_areas = self.data.item.get_map_areas(data["userId"]) @@ -72,32 +76,29 @@ class ChuniNew(ChuniBase): tmp.pop("user") map_areas.append(tmp) - return { - "userId": data["userId"], - "userMapAreaList": map_areas - } - + return {"userId": data["userId"], "userMapAreaList": map_areas} + def handle_get_user_symbol_chat_setting_api_request(self, data: Dict) -> Dict: - return { - "userId": data["userId"], - "symbolCharInfoList": [] - } + return {"userId": data["userId"], "symbolCharInfoList": []} 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 = self.data.item.get_character(data["userId"], profile["characterId"]) - + if profile is None: + return None + profile_character = self.data.item.get_character( + data["userId"], profile["characterId"] + ) + if profile_character is None: chara = {} else: chara = profile_character._asdict() chara.pop("id") chara.pop("user") - + data1 = { - "userId": data["userId"], - # Current Login State + "userId": data["userId"], + # Current Login State "isLogin": False, "lastLoginDate": profile["lastPlayDate"], # User Profile @@ -109,14 +110,14 @@ class ChuniNew(ChuniBase): "lastGameId": profile["lastGameId"], "lastRomVersion": profile["lastRomVersion"], "lastDataVersion": profile["lastDataVersion"], - "lastPlayDate": profile["lastPlayDate"], + "lastPlayDate": profile["lastPlayDate"], "emoneyBrandId": 0, - "trophyId": profile["trophyId"], + "trophyId": profile["trophyId"], # Current Selected Character "userCharacter": chara, # User Game Options - "playerLevel": profile["playerLevel"], - "rating": profile["rating"], + "playerLevel": profile["playerLevel"], + "rating": profile["rating"], "headphone": profile["headphone"], "chargeState": 0, "userNameEx": "0", diff --git a/titles/chuni/newplus.py b/titles/chuni/newplus.py index 7ebdc96..7e15985 100644 --- a/titles/chuni/newplus.py +++ b/titles/chuni/newplus.py @@ -7,17 +7,26 @@ from titles.chuni.new import ChuniNew from titles.chuni.const import ChuniConstants from titles.chuni.config import ChuniConfig + class ChuniNewPlus(ChuniNew): def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None: super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_NEW_PLUS - + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: ret = super().handle_get_game_setting_api_request(data) ret["gameSetting"]["romVersion"] = "2.05.00" ret["gameSetting"]["dataVersion"] = "2.05.00" - 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/" + 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 diff --git a/titles/chuni/paradise.py b/titles/chuni/paradise.py index 19e92ca..19155d6 100644 --- a/titles/chuni/paradise.py +++ b/titles/chuni/paradise.py @@ -7,12 +7,13 @@ from titles.chuni.base import ChuniBase from titles.chuni.const import ChuniConstants from titles.chuni.config import ChuniConfig + class ChuniParadise(ChuniBase): def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None: super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_PARADISE - + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + 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 492d4f6..62d9e0d 100644 --- a/titles/chuni/plus.py +++ b/titles/chuni/plus.py @@ -5,12 +5,13 @@ from titles.chuni.base import ChuniBase from titles.chuni.const import ChuniConstants from titles.chuni.config import ChuniConfig + class ChuniPlus(ChuniBase): def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None: super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_PLUS - + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + ret = super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.05.00" - return ret \ No newline at end of file + return ret diff --git a/titles/chuni/read.py b/titles/chuni/read.py index 1a666e6..abf78e3 100644 --- a/titles/chuni/read.py +++ b/titles/chuni/read.py @@ -7,48 +7,60 @@ from core.config import CoreConfig from titles.chuni.database import ChuniData from titles.chuni.const import ChuniConstants + class ChuniReader(BaseReader): - def __init__(self, config: CoreConfig, version: int, bin_dir: Optional[str], opt_dir: Optional[str], extra: Optional[str]) -> None: + def __init__( + self, + config: CoreConfig, + version: int, + bin_dir: Optional[str], + opt_dir: Optional[str], + extra: Optional[str], + ) -> None: super().__init__(config, version, bin_dir, opt_dir, extra) self.data = ChuniData(config) try: - self.logger.info(f"Start importer for {ChuniConstants.game_ver_to_string(version)}") + self.logger.info( + f"Start importer for {ChuniConstants.game_ver_to_string(version)}" + ) except IndexError: self.logger.error(f"Invalid chunithm version {version}") exit(1) - + def read(self) -> None: data_dirs = [] if self.bin_dir is not None: data_dirs += self.get_data_directories(self.bin_dir) - + if self.opt_dir is not None: - data_dirs += self.get_data_directories(self.opt_dir) - + data_dirs += self.get_data_directories(self.opt_dir) + for dir in data_dirs: self.logger.info(f"Read from {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") - + 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"): - with open(f"{root}/{dir}/Event.xml", 'rb') as fp: + with open(f"{root}/{dir}/Event.xml", "rb") as fp: bytedata = fp.read() - strdata = bytedata.decode('UTF-8') + strdata = bytedata.decode("UTF-8") xml_root = ET.fromstring(strdata) - for name in xml_root.findall('name'): - id = name.find('id').text - name = name.find('str').text - for substances in xml_root.findall('substances'): - event_type = substances.find('type').text - - result = self.data.static.put_event(self.version, id, event_type, name) + for name in xml_root.findall("name"): + id = name.find("id").text + name = name.find("str").text + for substances in xml_root.findall("substances"): + event_type = substances.find("type").text + + result = self.data.static.put_event( + self.version, id, event_type, name + ) if result is not None: self.logger.info(f"Inserted event {id}") else: @@ -58,73 +70,90 @@ class ChuniReader(BaseReader): for root, dirs, files in walk(music_dir): for dir in dirs: if path.exists(f"{root}/{dir}/Music.xml"): - with open(f"{root}/{dir}/Music.xml", 'rb') as fp: + with open(f"{root}/{dir}/Music.xml", "rb") as fp: bytedata = fp.read() - strdata = bytedata.decode('UTF-8') + strdata = bytedata.decode("UTF-8") xml_root = ET.fromstring(strdata) - for name in xml_root.findall('name'): - song_id = name.find('id').text - title = name.find('str').text + for name in xml_root.findall("name"): + song_id = name.find("id").text + title = name.find("str").text - for artistName in xml_root.findall('artistName'): - artist = artistName.find('str').text + for artistName in xml_root.findall("artistName"): + artist = artistName.find("str").text - for genreNames in xml_root.findall('genreNames'): - for list_ in genreNames.findall('list'): - for StringID in list_.findall('StringID'): - genre = StringID.find('str').text + for genreNames in xml_root.findall("genreNames"): + for list_ in genreNames.findall("list"): + for StringID in list_.findall("StringID"): + genre = StringID.find("str").text - for jaketFile in xml_root.findall('jaketFile'): #nice typo, SEGA - jacket_path = jaketFile.find('path').text + for jaketFile in xml_root.findall("jaketFile"): # nice typo, SEGA + jacket_path = jaketFile.find("path").text + + for fumens in xml_root.findall("fumens"): + for MusicFumenData in fumens.findall("MusicFumenData"): + fumen_path = MusicFumenData.find("file").find("path") - for fumens in xml_root.findall('fumens'): - for MusicFumenData in fumens.findall('MusicFumenData'): - fumen_path = MusicFumenData.find('file').find("path") - if fumen_path is not None: - chart_id = MusicFumenData.find('type').find('id').text + chart_id = MusicFumenData.find("type").find("id").text if chart_id == "4": level = float(xml_root.find("starDifType").text) - we_chara = xml_root.find("worldsEndTagName").find("str").text + we_chara = ( + xml_root.find("worldsEndTagName") + .find("str") + .text + ) else: - level = float(f"{MusicFumenData.find('level').text}.{MusicFumenData.find('levelDecimal').text}") + level = float( + f"{MusicFumenData.find('level').text}.{MusicFumenData.find('levelDecimal').text}" + ) we_chara = None - + result = self.data.static.put_music( self.version, song_id, - chart_id, + chart_id, title, artist, level, genre, jacket_path, - we_chara + we_chara, ) if result is not None: - self.logger.info(f"Inserted music {song_id} chart {chart_id}") + self.logger.info( + f"Inserted music {song_id} chart {chart_id}" + ) else: - self.logger.warn(f"Failed to insert music {song_id} chart {chart_id}") + self.logger.warn( + f"Failed to insert music {song_id} chart {chart_id}" + ) 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"): - with open(f"{root}/{dir}/ChargeItem.xml", 'rb') as fp: + with open(f"{root}/{dir}/ChargeItem.xml", "rb") as fp: bytedata = fp.read() - strdata = bytedata.decode('UTF-8') + strdata = bytedata.decode("UTF-8") xml_root = ET.fromstring(strdata) - for name in xml_root.findall('name'): - id = name.find('id').text - name = name.find('str').text - expirationDays = xml_root.find('expirationDays').text - consumeType = xml_root.find('consumeType').text - sellingAppeal = bool(xml_root.find('sellingAppeal').text) + for name in xml_root.findall("name"): + id = name.find("id").text + name = name.find("str").text + expirationDays = xml_root.find("expirationDays").text + consumeType = xml_root.find("consumeType").text + sellingAppeal = bool(xml_root.find("sellingAppeal").text) - result = self.data.static.put_charge(self.version, id, name, expirationDays, consumeType, sellingAppeal) + result = self.data.static.put_charge( + self.version, + id, + name, + expirationDays, + consumeType, + sellingAppeal, + ) if result is not None: self.logger.info(f"Inserted charge {id}") @@ -135,21 +164,23 @@ class ChuniReader(BaseReader): for root, dirs, files in walk(avatar_dir): for dir in dirs: if path.exists(f"{root}/{dir}/AvatarAccessory.xml"): - with open(f"{root}/{dir}/AvatarAccessory.xml", 'rb') as fp: + with open(f"{root}/{dir}/AvatarAccessory.xml", "rb") as fp: bytedata = fp.read() - strdata = bytedata.decode('UTF-8') + strdata = bytedata.decode("UTF-8") xml_root = ET.fromstring(strdata) - for name in xml_root.findall('name'): - id = name.find('id').text - name = name.find('str').text - category = xml_root.find('category').text - for image in xml_root.findall('image'): - iconPath = image.find('path').text - for texture in xml_root.findall('texture'): - texturePath = texture.find('path').text + for name in xml_root.findall("name"): + id = name.find("id").text + name = name.find("str").text + category = xml_root.find("category").text + for image in xml_root.findall("image"): + iconPath = image.find("path").text + for texture in xml_root.findall("texture"): + texturePath = texture.find("path").text - result = self.data.static.put_avatar(self.version, id, name, category, iconPath, texturePath) + result = self.data.static.put_avatar( + self.version, id, name, category, iconPath, texturePath + ) if result is not None: self.logger.info(f"Inserted avatarAccessory {id}") diff --git a/titles/chuni/schema/__init__.py b/titles/chuni/schema/__init__.py index 18c408e..51d950b 100644 --- a/titles/chuni/schema/__init__.py +++ b/titles/chuni/schema/__init__.py @@ -3,4 +3,4 @@ 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"] \ No newline at end of file +__all__ = ["ChuniProfileData", "ChuniScoreData", "ChuniItemData", "ChuniStaticData"] diff --git a/titles/chuni/schema/item.py b/titles/chuni/schema/item.py index 6973558..cc519fa 100644 --- a/titles/chuni/schema/item.py +++ b/titles/chuni/schema/item.py @@ -13,7 +13,11 @@ character = Table( "chuni_item_character", 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("characterId", Integer), Column("level", Integer), Column("param1", Integer), @@ -26,27 +30,35 @@ character = Table( Column("assignIllust", Integer), Column("exMaxLv", Integer), UniqueConstraint("user", "characterId", name="chuni_item_character_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) item = Table( "chuni_item_item", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("itemId", Integer), Column("itemKind", Integer), Column("stock", Integer), Column("isValid", Boolean), UniqueConstraint("user", "itemId", "itemKind", name="chuni_item_item_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) duel = Table( "chuni_item_duel", 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("duelId", Integer), Column("progress", Integer), Column("point", Integer), @@ -57,14 +69,18 @@ duel = Table( Column("param3", Integer), Column("param4", Integer), UniqueConstraint("user", "duelId", name="chuni_item_duel_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) map = Table( "chuni_item_map", 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("mapId", Integer), Column("position", Integer), Column("isClear", Boolean), @@ -72,17 +88,21 @@ map = Table( Column("routeNumber", Integer), Column("eventId", Integer), Column("rate", Integer), - Column("statusCount", Integer), + Column("statusCount", Integer), Column("isValid", Boolean), UniqueConstraint("user", "mapId", name="chuni_item_map_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) map_area = Table( "chuni_item_map_area", 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("mapAreaId", Integer), Column("rate", Integer), Column("isClear", Boolean), @@ -91,9 +111,10 @@ map_area = Table( Column("statusCount", Integer), Column("remainGridCount", Integer), UniqueConstraint("user", "mapAreaId", name="chuni_item_map_area_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) + class ChuniItemData(BaseData): def put_character(self, user_id: int, character_data: Dict) -> Optional[int]: character_data["user"] = user_id @@ -104,24 +125,26 @@ class ChuniItemData(BaseData): conflict = sql.on_duplicate_key_update(**character_data) result = self.execute(conflict) - if result is None: return None + if result is None: + return None return result.lastrowid 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 - )) - + sql = select(character).where( + and_(character.c.user == user_id, character.c.characterId == character_id) + ) + result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() - + def get_characters(self, user_id: int) -> Optional[List[Row]]: sql = select(character).where(character.c.user == user_id) - + result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() def put_item(self, user_id: int, item_data: Dict) -> Optional[int]: @@ -133,22 +156,23 @@ class ChuniItemData(BaseData): conflict = sql.on_duplicate_key_update(**item_data) result = self.execute(conflict) - if result is None: return None + if result is None: + return None return result.lastrowid 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: - sql = select(item).where(and_( - item.c.user == user_id, - item.c.itemKind == kind - )) - + sql = select(item).where( + and_(item.c.user == user_id, item.c.itemKind == kind) + ) + result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - + def put_duel(self, user_id: int, duel_data: Dict) -> Optional[int]: duel_data["user"] = user_id @@ -158,14 +182,16 @@ class ChuniItemData(BaseData): conflict = sql.on_duplicate_key_update(**duel_data) result = self.execute(conflict) - if result is None: return None + if result is None: + return None return result.lastrowid def get_duels(self, user_id: int) -> Optional[List[Row]]: sql = select(duel).where(duel.c.user == user_id) - + result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() def put_map(self, user_id: int, map_data: Dict) -> Optional[int]: @@ -177,16 +203,18 @@ class ChuniItemData(BaseData): conflict = sql.on_duplicate_key_update(**map_data) result = self.execute(conflict) - if result is None: return None + if result is None: + return None return result.lastrowid def get_maps(self, user_id: int) -> Optional[List[Row]]: sql = select(map).where(map.c.user == user_id) - + result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - + def put_map_area(self, user_id: int, map_area_data: Dict) -> Optional[int]: map_area_data["user"] = user_id @@ -196,12 +224,14 @@ class ChuniItemData(BaseData): conflict = sql.on_duplicate_key_update(**map_area_data) result = self.execute(conflict) - if result is None: return None + if result is None: + return None return result.lastrowid - + def get_map_areas(self, user_id: int) -> Optional[List[Row]]: sql = select(map_area).where(map_area.c.user == user_id) - + result = self.execute(sql) - if result is None: return None - return result.fetchall() \ No newline at end of file + if result is None: + return None + return result.fetchall() diff --git a/titles/chuni/schema/profile.py b/titles/chuni/schema/profile.py index f23f54d..9000b9b 100644 --- a/titles/chuni/schema/profile.py +++ b/titles/chuni/schema/profile.py @@ -13,7 +13,11 @@ profile = Table( "chuni_profile_data", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("version", Integer, nullable=False), Column("exp", Integer), Column("level", Integer), @@ -62,7 +66,7 @@ profile = Table( Column("firstTutorialCancelNum", Integer), Column("totalAdvancedHighScore", Integer), Column("masterTutorialCancelNum", Integer), - Column("ext1", Integer), # Added in chunew + Column("ext1", Integer), # Added in chunew Column("ext2", Integer), Column("ext3", Integer), Column("ext4", Integer), @@ -71,16 +75,20 @@ profile = Table( Column("ext7", Integer), Column("ext8", Integer), Column("ext9", Integer), - Column("ext10", Integer), + Column("ext10", Integer), Column("extStr1", String(255)), Column("extStr2", String(255)), Column("extLong1", Integer), Column("extLong2", Integer), Column("mapIconId", Integer), Column("compatibleCmVersion", String(25)), - Column("medal", Integer), + Column("medal", Integer), Column("voiceId", Integer), - Column("teamId", Integer, ForeignKey("chuni_profile_team.id", ondelete="SET NULL", onupdate="SET NULL")), + Column( + "teamId", + Integer, + ForeignKey("chuni_profile_team.id", ondelete="SET NULL", onupdate="SET NULL"), + ), Column("avatarBack", Integer, server_default="0"), Column("avatarFace", Integer, server_default="0"), Column("eliteRankPoint", Integer, server_default="0"), @@ -121,14 +129,18 @@ profile = Table( Column("netBattleEndState", Integer, server_default="0"), Column("avatarHead", Integer, server_default="0"), UniqueConstraint("user", "version", name="chuni_profile_profile_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) profile_ex = Table( "chuni_profile_data_ex", 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("ext1", Integer), Column("ext2", Integer), @@ -165,14 +177,18 @@ profile_ex = Table( Column("mapIconId", Integer), Column("compatibleCmVersion", String(25)), UniqueConstraint("user", "version", name="chuni_profile_data_ex_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) option = Table( "chuni_profile_option", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("speed", Integer), Column("bgInfo", Integer), Column("rating", Integer), @@ -195,7 +211,7 @@ option = Table( Column("successSkill", Integer), Column("successSlideHold", Integer), Column("successTapTimbre", Integer), - Column("ext1", Integer), # Added in chunew + Column("ext1", Integer), # Added in chunew Column("ext2", Integer), Column("ext3", Integer), Column("ext4", Integer), @@ -224,14 +240,18 @@ option = Table( Column("playTimingOffset", Integer, server_default="0"), Column("fieldWallPosition_120", Integer, server_default="0"), UniqueConstraint("user", name="chuni_profile_option_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) option_ex = Table( "chuni_profile_option_ex", 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("ext1", Integer), Column("ext2", Integer), Column("ext3", Integer), @@ -253,51 +273,69 @@ option_ex = Table( Column("ext19", Integer), Column("ext20", Integer), UniqueConstraint("user", name="chuni_profile_option_ex_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) recent_rating = Table( "chuni_profile_recent_rating", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("recentRating", JSON), UniqueConstraint("user", name="chuni_profile_recent_rating_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) region = Table( "chuni_profile_region", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("regionId", Integer), Column("playCount", Integer), UniqueConstraint("user", "regionId", name="chuni_profile_region_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) activity = Table( "chuni_profile_activity", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("kind", Integer), - Column("activityId", Integer), # Reminder: Change this to ID in base.py or the game will be sad + Column( + "activityId", Integer + ), # Reminder: Change this to ID in base.py or the game will be sad Column("sortNumber", Integer), Column("param1", Integer), Column("param2", Integer), Column("param3", Integer), Column("param4", Integer), UniqueConstraint("user", "kind", "activityId", name="chuni_profile_activity_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) charge = Table( "chuni_profile_charge", 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("chargeId", Integer), Column("stock", Integer), Column("purchaseDate", String(25)), @@ -306,14 +344,18 @@ charge = Table( Column("param2", Integer), Column("paramDate", String(25)), UniqueConstraint("user", "chargeId", name="chuni_profile_charge_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) emoney = Table( "chuni_profile_emoney", 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("ext1", Integer), Column("ext2", Integer), Column("ext3", Integer), @@ -321,20 +363,24 @@ emoney = Table( Column("emoneyBrand", Integer), Column("emoneyCredit", Integer), UniqueConstraint("user", "emoneyBrand", name="chuni_profile_emoney_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) overpower = Table( "chuni_profile_overpower", 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("genreId", Integer), Column("difficulty", Integer), Column("rate", Integer), Column("point", Integer), UniqueConstraint("user", "genreId", "difficulty", name="chuni_profile_emoney_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) team = Table( @@ -343,18 +389,21 @@ team = Table( Column("id", Integer, primary_key=True, nullable=False), Column("teamName", String(255)), Column("teamPoint", Integer), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) + class ChuniProfileData(BaseData): - def put_profile_data(self, aime_id: int, version: int, profile_data: Dict) -> Optional[int]: + def put_profile_data( + self, aime_id: int, version: int, profile_data: Dict + ) -> Optional[int]: profile_data["user"] = aime_id profile_data["version"] = version if "accessCode" in profile_data: profile_data.pop("accessCode") - + profile_data = self.fix_bools(profile_data) - + sql = insert(profile).values(**profile_data) conflict = sql.on_duplicate_key_update(**profile_data) result = self.execute(conflict) @@ -363,51 +412,64 @@ class ChuniProfileData(BaseData): self.logger.warn(f"put_profile_data: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - + 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) + sql = ( + select([profile, option]) + .join(option, profile.c.user == option.c.user) + .filter(and_(profile.c.user == aime_id, profile.c.version == version)) ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() def get_profile_data(self, aime_id: int, version: int) -> Optional[Row]: - sql = select(profile).where(and_( - profile.c.user == aime_id, - profile.c.version == version, - )) + sql = select(profile).where( + and_( + profile.c.user == aime_id, + profile.c.version == version, + ) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() - - def put_profile_data_ex(self, aime_id: int, version: int, profile_ex_data: Dict) -> Optional[int]: + + def put_profile_data_ex( + self, aime_id: int, version: int, profile_ex_data: Dict + ) -> Optional[int]: profile_ex_data["user"] = aime_id profile_ex_data["version"] = version if "accessCode" in profile_ex_data: profile_ex_data.pop("accessCode") - + sql = insert(profile_ex).values(**profile_ex_data) conflict = sql.on_duplicate_key_update(**profile_ex_data) result = self.execute(conflict) if result is None: - self.logger.warn(f"put_profile_data_ex: Failed to update! aime_id: {aime_id}") + self.logger.warn( + f"put_profile_data_ex: Failed to update! aime_id: {aime_id}" + ) return None return result.lastrowid - + 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, - )) + sql = select(profile_ex).where( + and_( + profile_ex.c.user == aime_id, + profile_ex.c.version == version, + ) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() - + def put_profile_option(self, aime_id: int, option_data: Dict) -> Optional[int]: option_data["user"] = aime_id @@ -416,7 +478,9 @@ class ChuniProfileData(BaseData): result = self.execute(conflict) if result is None: - self.logger.warn(f"put_profile_option: Failed to update! aime_id: {aime_id}") + self.logger.warn( + f"put_profile_option: Failed to update! aime_id: {aime_id}" + ) return None return result.lastrowid @@ -424,18 +488,23 @@ class ChuniProfileData(BaseData): sql = select(option).where(option.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() - - def put_profile_option_ex(self, aime_id: int, option_ex_data: Dict) -> Optional[int]: + + 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 = self.execute(conflict) if result is None: - self.logger.warn(f"put_profile_option_ex: Failed to update! aime_id: {aime_id}") + self.logger.warn( + f"put_profile_option_ex: Failed to update! aime_id: {aime_id}" + ) return None return result.lastrowid @@ -443,27 +512,32 @@ class ChuniProfileData(BaseData): sql = select(option_ex).where(option_ex.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() - def put_profile_recent_rating(self, aime_id: int, recent_rating_data: List[Dict]) -> Optional[int]: + def put_profile_recent_rating( + self, aime_id: int, recent_rating_data: List[Dict] + ) -> Optional[int]: sql = insert(recent_rating).values( - user = aime_id, - recentRating = recent_rating_data + user=aime_id, recentRating=recent_rating_data ) - conflict = sql.on_duplicate_key_update(recentRating = recent_rating_data) + conflict = sql.on_duplicate_key_update(recentRating=recent_rating_data) result = self.execute(conflict) if result is None: - self.logger.warn(f"put_profile_recent_rating: Failed to update! aime_id: {aime_id}") + self.logger.warn( + f"put_profile_recent_rating: Failed to update! aime_id: {aime_id}" + ) return None return result.lastrowid - + def get_profile_recent_rating(self, aime_id: int) -> Optional[Row]: sql = select(recent_rating).where(recent_rating.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() def put_profile_activity(self, aime_id: int, activity_data: Dict) -> Optional[int]: @@ -471,35 +545,39 @@ class ChuniProfileData(BaseData): activity_data["user"] = aime_id activity_data["activityId"] = activity_data["id"] activity_data.pop("id") - + sql = insert(activity).values(**activity_data) conflict = sql.on_duplicate_key_update(**activity_data) result = self.execute(conflict) if result is None: - self.logger.warn(f"put_profile_activity: Failed to update! aime_id: {aime_id}") + self.logger.warn( + f"put_profile_activity: Failed to update! aime_id: {aime_id}" + ) return None return result.lastrowid 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 - )) + sql = select(activity).where( + and_(activity.c.user == aime_id, activity.c.kind == kind) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() 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 = self.execute(conflict) if result is None: - self.logger.warn(f"put_profile_charge: Failed to update! aime_id: {aime_id}") + self.logger.warn( + f"put_profile_charge: Failed to update! aime_id: {aime_id}" + ) return None return result.lastrowid @@ -507,9 +585,10 @@ class ChuniProfileData(BaseData): sql = select(charge).where(charge.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - + def add_profile_region(self, aime_id: int, region_id: int) -> Optional[int]: pass @@ -523,29 +602,35 @@ class ChuniProfileData(BaseData): conflict = sql.on_duplicate_key_update(**emoney_data) result = self.execute(conflict) - if result is None: return None + if result is None: + return None return result.lastrowid - + def get_profile_emoney(self, aime_id: int) -> Optional[List[Row]]: sql = select(emoney).where(emoney.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - def put_profile_overpower(self, aime_id: int, overpower_data: Dict) -> Optional[int]: + def put_profile_overpower( + self, aime_id: int, overpower_data: Dict + ) -> Optional[int]: overpower_data["user"] = aime_id sql = insert(overpower).values(**overpower_data) conflict = sql.on_duplicate_key_update(**overpower_data) result = self.execute(conflict) - if result is None: return None + if result is None: + return None return result.lastrowid - + def get_profile_overpower(self, aime_id: int) -> Optional[List[Row]]: sql = select(overpower).where(overpower.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() diff --git a/titles/chuni/schema/score.py b/titles/chuni/schema/score.py index 81f3212..6a94813 100644 --- a/titles/chuni/schema/score.py +++ b/titles/chuni/schema/score.py @@ -13,7 +13,11 @@ course = Table( "chuni_score_course", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("courseId", Integer), Column("classId", Integer), Column("playCount", Integer), @@ -33,14 +37,18 @@ course = Table( Column("orderId", Integer), Column("playerRating", Integer), UniqueConstraint("user", "courseId", name="chuni_score_course_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) best_score = Table( "chuni_score_best", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("musicId", Integer), Column("level", Integer), Column("playCount", Integer), @@ -60,14 +68,18 @@ best_score = Table( Column("ext1", Integer), Column("theoryCount", Integer), UniqueConstraint("user", "musicId", "level", name="chuni_score_best_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) playlog = Table( "chuni_score_playlog", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("orderId", Integer), Column("sortNumber", Integer), Column("placeId", Integer), @@ -122,15 +134,17 @@ playlog = Table( Column("charaIllustId", Integer), Column("romVersion", String(255)), Column("judgeHeaven", Integer), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) + class ChuniScoreData(BaseData): def get_courses(self, aime_id: int) -> Optional[Row]: sql = select(course).where(course.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() def put_course(self, aime_id: int, course_data: Dict) -> Optional[int]: @@ -141,16 +155,18 @@ class ChuniScoreData(BaseData): conflict = sql.on_duplicate_key_update(**course_data) result = self.execute(conflict) - if result is None: return None + if result is None: + return None return result.lastrowid - + def get_scores(self, aime_id: int) -> Optional[Row]: sql = select(best_score).where(best_score.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - + def put_score(self, aime_id: int, score_data: Dict) -> Optional[int]: score_data["user"] = aime_id score_data = self.fix_bools(score_data) @@ -159,16 +175,18 @@ class ChuniScoreData(BaseData): conflict = sql.on_duplicate_key_update(**score_data) result = self.execute(conflict) - if result is None: return None + if result is None: + return None return result.lastrowid def get_playlogs(self, aime_id: int) -> Optional[Row]: sql = select(playlog).where(playlog.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - + def put_playlog(self, aime_id: int, playlog_data: Dict) -> Optional[int]: playlog_data["user"] = aime_id playlog_data = self.fix_bools(playlog_data) @@ -177,5 +195,6 @@ class ChuniScoreData(BaseData): conflict = sql.on_duplicate_key_update(**playlog_data) result = self.execute(conflict) - if result is None: return None + if result is None: + return None return result.lastrowid diff --git a/titles/chuni/schema/static.py b/titles/chuni/schema/static.py index fbfae11..0d58c45 100644 --- a/titles/chuni/schema/static.py +++ b/titles/chuni/schema/static.py @@ -19,7 +19,7 @@ events = Table( Column("name", String(255)), Column("enabled", Boolean, server_default="1"), UniqueConstraint("version", "eventId", name="chuni_static_events_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) music = Table( @@ -30,13 +30,13 @@ music = Table( Column("songId", Integer), Column("chartId", Integer), Column("title", String(255)), - Column("artist", String(255)), + Column("artist", String(255)), Column("level", Float), Column("genre", String(255)), Column("jacketPath", String(255)), Column("worldsEndTag", String(7)), UniqueConstraint("version", "songId", "chartId", name="chuni_static_music_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) charge = Table( @@ -51,7 +51,7 @@ charge = Table( Column("sellingAppeal", Boolean), Column("enabled", Boolean, server_default="1"), UniqueConstraint("version", "chargeId", name="chuni_static_charge_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) avatar = Table( @@ -65,159 +65,203 @@ avatar = Table( Column("iconPath", String(255)), Column("texturePath", String(255)), UniqueConstraint("version", "avatarAccessoryId", name="chuni_static_avatar_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) + class ChuniStaticData(BaseData): - def put_event(self, version: int, event_id: int, type: int, name: str) -> Optional[int]: + def put_event( + self, version: int, event_id: int, type: int, name: str + ) -> Optional[int]: sql = insert(events).values( - version = version, - eventId = event_id, - type = type, - name = name + version=version, eventId=event_id, type=type, name=name ) - conflict = sql.on_duplicate_key_update( - name = name - ) + conflict = sql.on_duplicate_key_update(name=name) result = self.execute(conflict) - if result is None: return None + if result is None: + return None return result.lastrowid - - 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 - ) + + 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 = self.execute(sql) - if result is None: - self.logger.warn(f"update_event: failed to update event! version: {version}, event_id: {event_id}, enabled: {enabled}") + if result is None: + self.logger.warn( + f"update_event: failed to update event! version: {version}, event_id: {event_id}, enabled: {enabled}" + ) return None event = self.get_event(version, event_id) if event is None: - self.logger.warn(f"update_event: failed to fetch event {event_id} after updating") + self.logger.warn( + f"update_event: failed to fetch event {event_id} after updating" + ) return None return event["enabled"] 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)) - + sql = select(events).where( + and_(events.c.version == version, events.c.eventId == event_id) + ) + result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() def get_enabled_events(self, version: int) -> Optional[List[Row]]: - sql = select(events).where(and_(events.c.version == version, events.c.enabled == True)) + sql = select(events).where( + and_(events.c.version == version, events.c.enabled == True) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() def get_events(self, version: int) -> Optional[List[Row]]: sql = select(events).where(events.c.version == version) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - - def put_music(self, version: int, song_id: int, chart_id: int, title: int, artist: str, - level: float, genre: str, jacketPath: str, we_tag: str) -> Optional[int]: + def put_music( + self, + version: int, + song_id: int, + chart_id: int, + title: int, + artist: str, + level: float, + genre: str, + jacketPath: str, + we_tag: str, + ) -> Optional[int]: sql = insert(music).values( - version = version, - songId = song_id, - chartId = chart_id, - title = title, - artist = artist, - level = level, - genre = genre, - jacketPath = jacketPath, - worldsEndTag = we_tag, + version=version, + songId=song_id, + chartId=chart_id, + title=title, + artist=artist, + level=level, + genre=genre, + jacketPath=jacketPath, + worldsEndTag=we_tag, ) conflict = sql.on_duplicate_key_update( - title = title, - artist = artist, - level = level, - genre = genre, - jacketPath = jacketPath, - worldsEndTag = we_tag, + title=title, + artist=artist, + level=level, + genre=genre, + jacketPath=jacketPath, + worldsEndTag=we_tag, ) result = self.execute(conflict) - if result is None: return None + if result is None: + return None return result.lastrowid - - def put_charge(self, version: int, charge_id: int, name: str, expiration_days: int, - consume_type: int, selling_appeal: bool) -> Optional[int]: + + def put_charge( + self, + version: int, + charge_id: int, + name: str, + expiration_days: int, + consume_type: int, + selling_appeal: bool, + ) -> Optional[int]: sql = insert(charge).values( - version = version, - chargeId = charge_id, - name = name, - expirationDays = expiration_days, - consumeType = consume_type, - sellingAppeal = selling_appeal, + version=version, + chargeId=charge_id, + name=name, + expirationDays=expiration_days, + consumeType=consume_type, + sellingAppeal=selling_appeal, ) conflict = sql.on_duplicate_key_update( - name = name, - expirationDays = expiration_days, - consumeType = consume_type, - sellingAppeal = selling_appeal, + name=name, + expirationDays=expiration_days, + consumeType=consume_type, + sellingAppeal=selling_appeal, ) result = self.execute(conflict) - if result is None: return None + if result is None: + return None return result.lastrowid - + def get_enabled_charges(self, version: int) -> Optional[List[Row]]: - sql = select(charge).where(and_( - charge.c.version == version, - charge.c.enabled == True - )) + sql = select(charge).where( + and_(charge.c.version == version, charge.c.enabled == True) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - + def get_charges(self, version: int) -> Optional[List[Row]]: sql = select(charge).where(charge.c.version == version) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - def get_music_chart(self, version: int, song_id: int, chart_id: int) -> Optional[List[Row]]: - sql = select(music).where(and_( - music.c.version == version, - music.c.songId == song_id, - music.c.chartId == chart_id - )) + def get_music_chart( + self, version: int, song_id: int, chart_id: int + ) -> Optional[List[Row]]: + sql = select(music).where( + and_( + music.c.version == version, + music.c.songId == song_id, + music.c.chartId == chart_id, + ) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() - def put_avatar(self, version: int, avatarAccessoryId: int, name: str, category: int, iconPath: str, texturePath: str) -> Optional[int]: + def put_avatar( + self, + version: int, + avatarAccessoryId: int, + name: str, + category: int, + iconPath: str, + texturePath: str, + ) -> Optional[int]: sql = insert(avatar).values( - version = version, - avatarAccessoryId = avatarAccessoryId, - name = name, - category = category, - iconPath = iconPath, - texturePath = texturePath, + version=version, + avatarAccessoryId=avatarAccessoryId, + name=name, + category=category, + iconPath=iconPath, + texturePath=texturePath, ) conflict = sql.on_duplicate_key_update( - name = name, - category = category, - iconPath = iconPath, - texturePath = texturePath, + name=name, + category=category, + iconPath=iconPath, + texturePath=texturePath, ) result = self.execute(conflict) - if result is None: return None + if result is None: + return None return result.lastrowid - diff --git a/titles/chuni/star.py b/titles/chuni/star.py index 03408dc..4c071e8 100644 --- a/titles/chuni/star.py +++ b/titles/chuni/star.py @@ -5,12 +5,13 @@ from titles.chuni.base import ChuniBase from titles.chuni.const import ChuniConstants from titles.chuni.config import ChuniConfig + class ChuniStar(ChuniBase): def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None: super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_STAR - + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + ret = super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.20.00" - return ret \ No newline at end of file + return ret diff --git a/titles/chuni/starplus.py b/titles/chuni/starplus.py index 95000ef..8c24cc8 100644 --- a/titles/chuni/starplus.py +++ b/titles/chuni/starplus.py @@ -5,12 +5,13 @@ from titles.chuni.base import ChuniBase from titles.chuni.const import ChuniConstants from titles.chuni.config import ChuniConfig + class ChuniStarPlus(ChuniBase): def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None: super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_STAR_PLUS - + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + ret = super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.25.00" - return ret \ No newline at end of file + return ret diff --git a/titles/cm/base.py b/titles/cm/base.py index b5d65b7..ff38489 100644 --- a/titles/cm/base.py +++ b/titles/cm/base.py @@ -10,12 +10,14 @@ from titles.cm.const import CardMakerConstants from titles.cm.config import CardMakerConfig -class CardMakerBase(): +class CardMakerBase: def __init__(self, core_cfg: CoreConfig, game_cfg: CardMakerConfig) -> None: self.core_cfg = core_cfg self.game_cfg = game_cfg self.date_time_format = "%Y-%m-%d %H:%M:%S" - self.date_time_format_ext = "%Y-%m-%d %H:%M:%S.%f" # needs to be lopped off at [:-5] + self.date_time_format_ext = ( + "%Y-%m-%d %H:%M:%S.%f" # needs to be lopped off at [:-5] + ) self.date_time_format_short = "%Y-%m-%d" self.logger = logging.getLogger("cardmaker") self.game = CardMakerConstants.GAME_CODE @@ -31,27 +33,19 @@ class CardMakerBase(): return { "length": 3, "gameConnectList": [ - { - "modelKind": 0, - "type": 1, - "titleUri": f"{uri}/SDHD/200/" - }, - { - "modelKind": 1, - "type": 1, - "titleUri": f"{uri}/SDEZ/120/" - }, - { - "modelKind": 2, - "type": 1, - "titleUri": f"{uri}/SDDT/130/" - } - ] + {"modelKind": 0, "type": 1, "titleUri": f"{uri}/SDHD/200/"}, + {"modelKind": 1, "type": 1, "titleUri": f"{uri}/SDEZ/120/"}, + {"modelKind": 2, "type": 1, "titleUri": f"{uri}/SDDT/130/"}, + ], } def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - reboot_start = date.strftime(datetime.now() + timedelta(hours=3), self.date_time_format) - reboot_end = date.strftime(datetime.now() + timedelta(hours=4), self.date_time_format) + reboot_start = date.strftime( + datetime.now() + timedelta(hours=3), self.date_time_format + ) + reboot_end = date.strftime( + datetime.now() + timedelta(hours=4), self.date_time_format + ) return { "gameSetting": { @@ -67,18 +61,14 @@ class CardMakerBase(): "maxCountCard": 100, "watermark": False, "isMaintenance": False, - "isBackgroundDistribute": False + "isBackgroundDistribute": False, }, "isDumpUpload": False, - "isAou": False + "isAou": False, } def handle_get_client_bookkeeping_api_request(self, data: Dict) -> Dict: - return { - "placeId": data["placeId"], - "length": 0, - "clientBookkeepingList": [] - } + return {"placeId": data["placeId"], "length": 0, "clientBookkeepingList": []} def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "UpsertClientSettingApi"} diff --git a/titles/cm/cm136.py b/titles/cm/cm136.py index 2298dc8..5feeca4 100644 --- a/titles/cm/cm136.py +++ b/titles/cm/cm136.py @@ -22,7 +22,7 @@ class CardMaker136(CardMakerBase): uri = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}" else: uri = f"http://{self.core_cfg.title.hostname}" - + ret["gameConnectList"][0]["titleUri"] = f"{uri}/SDHD/205/" ret["gameConnectList"][1]["titleUri"] = f"{uri}/SDEZ/125/" ret["gameConnectList"][2]["titleUri"] = f"{uri}/SDDT/135/" diff --git a/titles/cm/config.py b/titles/cm/config.py index df9f65a..ea96ca1 100644 --- a/titles/cm/config.py +++ b/titles/cm/config.py @@ -1,17 +1,23 @@ from core.config import CoreConfig -class CardMakerServerConfig(): +class CardMakerServerConfig: def __init__(self, parent_config: "CardMakerConfig") -> None: self.__config = parent_config @property def enable(self) -> bool: - return CoreConfig.get_config_field(self.__config, 'cardmaker', 'server', 'enable', default=True) + return CoreConfig.get_config_field( + self.__config, "cardmaker", "server", "enable", default=True + ) @property def loglevel(self) -> int: - return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'cardmaker', 'server', 'loglevel', default="info")) + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "cardmaker", "server", "loglevel", default="info" + ) + ) class CardMakerConfig(dict): diff --git a/titles/cm/const.py b/titles/cm/const.py index 3dec4fe..c5627ee 100644 --- a/titles/cm/const.py +++ b/titles/cm/const.py @@ -1,4 +1,4 @@ -class CardMakerConstants(): +class CardMakerConstants: GAME_CODE = "SDED" CONFIG_NAME = "cardmaker.yaml" diff --git a/titles/cm/index.py b/titles/cm/index.py index ff9ea1f..d082aad 100644 --- a/titles/cm/index.py +++ b/titles/cm/index.py @@ -18,23 +18,29 @@ from titles.cm.base import CardMakerBase from titles.cm.cm136 import CardMaker136 -class CardMakerServlet(): +class CardMakerServlet: def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: self.core_cfg = core_cfg self.game_cfg = CardMakerConfig() if path.exists(f"{cfg_dir}/{CardMakerConstants.CONFIG_NAME}"): - self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{CardMakerConstants.CONFIG_NAME}"))) + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{CardMakerConstants.CONFIG_NAME}")) + ) self.versions = [ CardMakerBase(core_cfg, self.game_cfg), - CardMaker136(core_cfg, self.game_cfg) + CardMaker136(core_cfg, self.game_cfg), ] self.logger = logging.getLogger("cardmaker") log_fmt_str = "[%(asctime)s] Card Maker | %(levelname)s | %(message)s" log_fmt = logging.Formatter(log_fmt_str) - fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.core_cfg.server.log_dir, "cardmaker"), encoding='utf8', - when="d", backupCount=10) + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.core_cfg.server.log_dir, "cardmaker"), + encoding="utf8", + when="d", + backupCount=10, + ) fileHandler.setFormatter(log_fmt) @@ -45,20 +51,29 @@ class CardMakerServlet(): self.logger.addHandler(consoleHandler) self.logger.setLevel(self.game_cfg.server.loglevel) - coloredlogs.install(level=self.game_cfg.server.loglevel, - logger=self.logger, fmt=log_fmt_str) + coloredlogs.install( + level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str + ) @classmethod - def get_allnet_info(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str, str]: + def get_allnet_info( + cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str + ) -> Tuple[bool, str, str]: game_cfg = CardMakerConfig() if path.exists(f"{cfg_dir}/{CardMakerConstants.CONFIG_NAME}"): - game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{CardMakerConstants.CONFIG_NAME}"))) + game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{CardMakerConstants.CONFIG_NAME}")) + ) if not game_cfg.server.enable: return (False, "", "") if core_cfg.server.is_develop: - return (True, f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/", "") + return ( + True, + f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/", + "", + ) return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", "") @@ -86,7 +101,8 @@ class CardMakerServlet(): except zlib.error as e: self.logger.error( - f"Failed to decompress v{version} {endpoint} request -> {e}") + f"Failed to decompress v{version} {endpoint} request -> {e}" + ) return zlib.compress(b'{"stat": "0"}') req_data = json.loads(unzip) @@ -104,12 +120,11 @@ class CardMakerServlet(): resp = handler(req_data) except Exception as e: - self.logger.error( - f"Error handling v{version} method {endpoint} - {e}") + self.logger.error(f"Error handling v{version} method {endpoint} - {e}") return zlib.compress(b'{"stat": "0"}') if resp is None: - resp = {'returnCode': 1} + resp = {"returnCode": 1} self.logger.info(f"Response {resp}") diff --git a/titles/cm/read.py b/titles/cm/read.py index 57d9279..3a4635f 100644 --- a/titles/cm/read.py +++ b/titles/cm/read.py @@ -15,14 +15,21 @@ from titles.ongeki.config import OngekiConfig class CardMakerReader(BaseReader): - def __init__(self, config: CoreConfig, version: int, bin_dir: Optional[str], - opt_dir: Optional[str], extra: Optional[str]) -> None: + def __init__( + self, + config: CoreConfig, + version: int, + bin_dir: Optional[str], + opt_dir: Optional[str], + extra: Optional[str], + ) -> None: super().__init__(config, version, bin_dir, opt_dir, extra) self.ongeki_data = OngekiData(config) try: self.logger.info( - f"Start importer for {CardMakerConstants.game_ver_to_string(version)}") + f"Start importer for {CardMakerConstants.game_ver_to_string(version)}" + ) except IndexError: self.logger.error(f"Invalid Card Maker version {version}") exit(1) @@ -30,7 +37,7 @@ class CardMakerReader(BaseReader): def read(self) -> None: static_datas = { "static_gachas.csv": "read_ongeki_gacha_csv", - "static_gacha_cards.csv": "read_ongeki_gacha_card_csv" + "static_gacha_cards.csv": "read_ongeki_gacha_card_csv", } data_dirs = [] @@ -41,7 +48,9 @@ class CardMakerReader(BaseReader): read_csv = getattr(CardMakerReader, func) read_csv(self, f"{self.bin_dir}/MU3/{file}") else: - self.logger.warn(f"Couldn't find {file} file in {self.bin_dir}, skipping") + self.logger.warn( + f"Couldn't find {file} file in {self.bin_dir}, skipping" + ) if self.opt_dir is not None: data_dirs += self.get_data_directories(self.opt_dir) @@ -64,7 +73,7 @@ class CardMakerReader(BaseReader): row["kind"], type=row["type"], isCeiling=True if row["isCeiling"] == "1" else False, - maxSelectPoint=row["maxSelectPoint"] + maxSelectPoint=row["maxSelectPoint"], ) self.logger.info(f"Added gacha {row['gachaId']}") @@ -81,7 +90,7 @@ class CardMakerReader(BaseReader): rarity=row["rarity"], weight=row["weight"], isPickup=True if row["isPickup"] == "1" else False, - isSelect=True if row["isSelect"] == "1" else False + isSelect=True if row["isSelect"] == "1" else False, ) self.logger.info(f"Added card {row['cardId']} to gacha") @@ -95,7 +104,7 @@ class CardMakerReader(BaseReader): "Pickup": "Pickup", "RecoverFiveShotFlag": "BonusRestored", "Free": "Free", - "FreeSR": "Free" + "FreeSR": "Free", } for root, dirs, files in os.walk(base_dir): @@ -104,13 +113,19 @@ class CardMakerReader(BaseReader): with open(f"{root}/{dir}/Gacha.xml", "r", encoding="utf-8") as f: troot = ET.fromstring(f.read()) - name = troot.find('Name').find('str').text - gacha_id = int(troot.find('Name').find('id').text) + name = troot.find("Name").find("str").text + gacha_id = int(troot.find("Name").find("id").text) # skip already existing gachas - if self.ongeki_data.static.get_gacha( - OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY, gacha_id) is not None: - self.logger.info(f"Gacha {gacha_id} already added, skipping") + if ( + self.ongeki_data.static.get_gacha( + OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY, gacha_id + ) + is not None + ): + self.logger.info( + f"Gacha {gacha_id} already added, skipping" + ) continue # 1140 is the first bright memory gacha @@ -120,7 +135,8 @@ class CardMakerReader(BaseReader): version = OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY gacha_kind = OngekiConstants.CM_GACHA_KINDS[ - type_to_kind[troot.find('Type').text]].value + type_to_kind[troot.find("Type").text] + ].value # hardcode which gachas get "Select Gacha" with 33 points is_ceiling, max_select_point = 0, 0 @@ -134,5 +150,6 @@ class CardMakerReader(BaseReader): name, gacha_kind, isCeiling=is_ceiling, - maxSelectPoint=max_select_point) + maxSelectPoint=max_select_point, + ) self.logger.info(f"Added gacha {gacha_id}") diff --git a/titles/cxb/__init__.py b/titles/cxb/__init__.py index 0a9db97..37abdab 100644 --- a/titles/cxb/__init__.py +++ b/titles/cxb/__init__.py @@ -7,4 +7,4 @@ index = CxbServlet database = CxbData reader = CxbReader game_codes = [CxbConstants.GAME_CODE] -current_schema_version = 1 \ No newline at end of file +current_schema_version = 1 diff --git a/titles/cxb/base.py b/titles/cxb/base.py index 7b021bb..6b6a5d5 100644 --- a/titles/cxb/base.py +++ b/titles/cxb/base.py @@ -11,82 +11,91 @@ from titles.cxb.config import CxbConfig from titles.cxb.const import CxbConstants from titles.cxb.database import CxbData -class CxbBase(): + +class CxbBase: def __init__(self, cfg: CoreConfig, game_cfg: CxbConfig) -> None: - self.config = cfg # Config file + self.config = cfg # Config file self.game_config = game_cfg - self.data = CxbData(cfg) # Database + self.data = CxbData(cfg) # Database self.game = CxbConstants.GAME_CODE self.logger = logging.getLogger("cxb") self.version = CxbConstants.VER_CROSSBEATS_REV - + def handle_action_rpreq_request(self, data: Dict) -> Dict: - return({}) - + return {} + def handle_action_hitreq_request(self, data: Dict) -> Dict: - return({"data":[]}) + return {"data": []} def handle_auth_usercheck_request(self, data: Dict) -> Dict: - profile = self.data.profile.get_profile_index(0, data["usercheck"]["authid"], self.version) + profile = self.data.profile.get_profile_index( + 0, data["usercheck"]["authid"], self.version + ) if profile is not None: self.logger.info(f"User {data['usercheck']['authid']} has CXB profile") - return({"exist": "true", "logout": "true"}) + return {"exist": "true", "logout": "true"} self.logger.info(f"No profile for aime id {data['usercheck']['authid']}") - return({"exist": "false", "logout": "true"}) + return {"exist": "false", "logout": "true"} 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"]}) + return {"token": data["entry"]["authid"], "uid": data["entry"]["authid"]} def handle_auth_login_request(self, data: Dict) -> Dict: - profile = self.data.profile.get_profile_index(0, data["login"]["authid"], self.version) - + profile = self.data.profile.get_profile_index( + 0, data["login"]["authid"], self.version + ) + if profile is not None: self.logger.info(f"Login user {data['login']['authid']}") - return({"token": data["login"]["authid"], "uid": data["login"]["authid"]}) - + return {"token": data["login"]["authid"], "uid": data["login"]["authid"]} + self.logger.warn(f"User {data['login']['authid']} does not have a profile") - return({}) - + return {} + 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'] + 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 = self.data.profile.get_profile(uid, self.version) - songs = self.data.score.get_best_scores(uid) + songs = self.data.score.get_best_scores(uid) data1 = [] index = [] versionindex = [] - + for profile_index in profile: profile_data = profile_index["data"] if int(range_start) == 800000: - return({"index":range_start, "data":[], "version":10400}) - - if not ( int(range_start) <= int(profile_index[3]) <= int(range_end) ): + return {"index": range_start, "data": [], "version": 10400} + + if not (int(range_start) <= int(profile_index[3]) <= int(range_end)): continue - #Prevent loading of the coupons within the profile to use the force unlock instead + # Prevent loading of the coupons within the profile to use the force unlock instead elif 500 <= int(profile_index[3]) <= 510: continue - #Prevent loading of songs saved in the profile + # Prevent loading of songs saved in the profile elif 100000 <= int(profile_index[3]) <= 110000: continue - #Prevent loading of the shop list / unlocked titles & icons saved in the profile + # Prevent loading of the shop list / unlocked titles & icons saved in the profile elif 200000 <= int(profile_index[3]) <= 210000: continue - #Prevent loading of stories in the profile + # Prevent loading of stories in the profile elif 900000 <= int(profile_index[3]) <= 900200: continue else: index.append(profile_index[3]) - data1.append(b64encode(bytes(json.dumps(profile_data, separators=(',', ':')), 'utf-8')).decode('utf-8')) + data1.append( + b64encode( + bytes(json.dumps(profile_data, separators=(",", ":")), "utf-8") + ).decode("utf-8") + ) - ''' + """ 100000 = Songs 200000 = Shop 300000 = Courses @@ -96,101 +105,140 @@ class CxbBase(): 700000 = rcLog 800000 = Partners 900000 = Stories - ''' + """ # Coupons - for i in range(500,510): + for i in range(500, 510): index.append(str(i)) couponid = int(i) - 500 - dataValue = [{ - "couponId":str(couponid), - "couponNum":"1", - "couponLog":[], - }] - data1.append(b64encode(bytes(json.dumps(dataValue[0], separators=(',', ':')), 'utf-8')).decode('utf-8')) - + dataValue = [ + { + "couponId": str(couponid), + "couponNum": "1", + "couponLog": [], + } + ] + data1.append( + b64encode( + bytes(json.dumps(dataValue[0], separators=(",", ":")), "utf-8") + ).decode("utf-8") + ) # ShopList_Title - for i in range(200000,201451): + for i in range(200000, 201451): index.append(str(i)) shopid = int(i) - 200000 - dataValue = [{ - "shopId":shopid, - "shopState":"2", - "isDisable":"t", - "isDeleted":"f", - "isSpecialFlag":"f" - }] - data1.append(b64encode(bytes(json.dumps(dataValue[0], separators=(',', ':')), 'utf-8')).decode('utf-8')) + dataValue = [ + { + "shopId": shopid, + "shopState": "2", + "isDisable": "t", + "isDeleted": "f", + "isSpecialFlag": "f", + } + ] + data1.append( + b64encode( + bytes(json.dumps(dataValue[0], separators=(",", ":")), "utf-8") + ).decode("utf-8") + ) - #ShopList_Icon - for i in range(202000,202264): + # ShopList_Icon + for i in range(202000, 202264): index.append(str(i)) shopid = int(i) - 200000 - dataValue = [{ - "shopId":shopid, - "shopState":"2", - "isDisable":"t", - "isDeleted":"f", - "isSpecialFlag":"f" - }] - data1.append(b64encode(bytes(json.dumps(dataValue[0], separators=(',', ':')), 'utf-8')).decode('utf-8')) + dataValue = [ + { + "shopId": shopid, + "shopState": "2", + "isDisable": "t", + "isDeleted": "f", + "isSpecialFlag": "f", + } + ] + data1.append( + b64encode( + bytes(json.dumps(dataValue[0], separators=(",", ":")), "utf-8") + ).decode("utf-8") + ) - #Stories - for i in range(900000,900003): + # Stories + for i in range(900000, 900003): index.append(str(i)) storyid = int(i) - 900000 - dataValue = [{ - "storyId":storyid, - "unlockState1":["t"] * 10, - "unlockState2":["t"] * 10, - "unlockState3":["t"] * 10, - "unlockState4":["t"] * 10, - "unlockState5":["t"] * 10, - "unlockState6":["t"] * 10, - "unlockState7":["t"] * 10, - "unlockState8":["t"] * 10, - "unlockState9":["t"] * 10, - "unlockState10":["t"] * 10, - "unlockState11":["t"] * 10, - "unlockState12":["t"] * 10, - "unlockState13":["t"] * 10, - "unlockState14":["t"] * 10, - "unlockState15":["t"] * 10, - "unlockState16":["t"] * 10 - }] - data1.append(b64encode(bytes(json.dumps(dataValue[0], separators=(',', ':')), 'utf-8')).decode('utf-8')) + dataValue = [ + { + "storyId": storyid, + "unlockState1": ["t"] * 10, + "unlockState2": ["t"] * 10, + "unlockState3": ["t"] * 10, + "unlockState4": ["t"] * 10, + "unlockState5": ["t"] * 10, + "unlockState6": ["t"] * 10, + "unlockState7": ["t"] * 10, + "unlockState8": ["t"] * 10, + "unlockState9": ["t"] * 10, + "unlockState10": ["t"] * 10, + "unlockState11": ["t"] * 10, + "unlockState12": ["t"] * 10, + "unlockState13": ["t"] * 10, + "unlockState14": ["t"] * 10, + "unlockState15": ["t"] * 10, + "unlockState16": ["t"] * 10, + } + ] + data1.append( + b64encode( + bytes(json.dumps(dataValue[0], separators=(",", ":")), "utf-8") + ).decode("utf-8") + ) for song in songs: song_data = song["data"] songCode = [] - songCode.append({ - "mcode": song_data['mcode'], - "musicState": song_data['musicState'], - "playCount": song_data['playCount'], - "totalScore": song_data['totalScore'], - "highScore": song_data['highScore'], - "everHighScore": song_data['everHighScore'] if 'everHighScore' in song_data else ["0","0","0","0","0"], - "clearRate": song_data['clearRate'], - "rankPoint": song_data['rankPoint'], - "normalCR": song_data['normalCR'] if 'normalCR' in song_data else ["0","0","0","0","0"], - "survivalCR": song_data['survivalCR'] if 'survivalCR' in song_data else ["0","0","0","0","0"], - "ultimateCR": song_data['ultimateCR'] if 'ultimateCR' in song_data else ["0","0","0","0","0"], - "nohopeCR": song_data['nohopeCR'] if 'nohopeCR' in song_data else ["0","0","0","0","0"], - "combo": song_data['combo'], - "coupleUserId": song_data['coupleUserId'], - "difficulty": song_data['difficulty'], - "isFullCombo": song_data['isFullCombo'], - "clearGaugeType": song_data['clearGaugeType'], - "fieldType": song_data['fieldType'], - "gameType": song_data['gameType'], - "grade": song_data['grade'], - "unlockState": song_data['unlockState'], - "extraState": song_data['extraState'] - }) - index.append(song_data['index']) - data1.append(b64encode(bytes(json.dumps(songCode[0], separators=(',', ':')), 'utf-8')).decode('utf-8')) + songCode.append( + { + "mcode": song_data["mcode"], + "musicState": song_data["musicState"], + "playCount": song_data["playCount"], + "totalScore": song_data["totalScore"], + "highScore": song_data["highScore"], + "everHighScore": song_data["everHighScore"] + if "everHighScore" in song_data + else ["0", "0", "0", "0", "0"], + "clearRate": song_data["clearRate"], + "rankPoint": song_data["rankPoint"], + "normalCR": song_data["normalCR"] + if "normalCR" in song_data + else ["0", "0", "0", "0", "0"], + "survivalCR": song_data["survivalCR"] + if "survivalCR" in song_data + else ["0", "0", "0", "0", "0"], + "ultimateCR": song_data["ultimateCR"] + if "ultimateCR" in song_data + else ["0", "0", "0", "0", "0"], + "nohopeCR": song_data["nohopeCR"] + if "nohopeCR" in song_data + else ["0", "0", "0", "0", "0"], + "combo": song_data["combo"], + "coupleUserId": song_data["coupleUserId"], + "difficulty": song_data["difficulty"], + "isFullCombo": song_data["isFullCombo"], + "clearGaugeType": song_data["clearGaugeType"], + "fieldType": song_data["fieldType"], + "gameType": song_data["gameType"], + "grade": song_data["grade"], + "unlockState": song_data["unlockState"], + "extraState": song_data["extraState"], + } + ) + index.append(song_data["index"]) + data1.append( + b64encode( + bytes(json.dumps(songCode[0], separators=(",", ":")), "utf-8") + ).decode("utf-8") + ) for v in index: try: @@ -198,66 +246,81 @@ class CxbBase(): v_profile_data = v_profile["data"] versionindex.append(int(v_profile_data["appVersion"])) except: - versionindex.append('10400') + versionindex.append("10400") - return({"index":index, "data":data1, "version":versionindex}) + return {"index": index, "data": data1, "version": versionindex} def handle_action_saveindex_request(self, data: Dict) -> Dict: - save_data = data['saveindex'] - + save_data = data["saveindex"] + try: - #REV Omnimix Version Fetcher - gameversion = data['saveindex']['data'][0][2] + # REV Omnimix Version Fetcher + gameversion = data["saveindex"]["data"][0][2] self.logger.warning(f"Game Version is {gameversion}") except: pass - - if "10205" in gameversion: - self.logger.info(f"Saving CrossBeats REV profile for {data['saveindex']['uid']}") - #Alright.... time to bring the jank code - - for value in data['saveindex']['data']: - - if 'playedUserId' in value[1]: - self.data.profile.put_profile(data['saveindex']['uid'], self.version, value[0], value[1]) - if 'mcode' not in value[1]: - self.data.profile.put_profile(data['saveindex']['uid'], self.version, value[0], value[1]) - if 'shopId' in value: - continue - if 'mcode' in value[1] and 'musicState' in value[1]: - song_json = json.loads(value[1]) - - songCode = [] - songCode.append({ - "mcode": song_json['mcode'], - "musicState": song_json['musicState'], - "playCount": song_json['playCount'], - "totalScore": song_json['totalScore'], - "highScore": song_json['highScore'], - "clearRate": song_json['clearRate'], - "rankPoint": song_json['rankPoint'], - "combo": song_json['combo'], - "coupleUserId": song_json['coupleUserId'], - "difficulty": song_json['difficulty'], - "isFullCombo": song_json['isFullCombo'], - "clearGaugeType": song_json['clearGaugeType'], - "fieldType": song_json['fieldType'], - "gameType": song_json['gameType'], - "grade": song_json['grade'], - "unlockState": song_json['unlockState'], - "extraState": song_json['extraState'], - "index": value[0] - }) - self.data.score.put_best_score(data['saveindex']['uid'], song_json['mcode'], self.version, value[0], songCode[0]) - return({}) - else: - self.logger.info(f"Saving CrossBeats REV Sunrise profile for {data['saveindex']['uid']}") - #Sunrise + if "10205" in gameversion: + self.logger.info( + f"Saving CrossBeats REV profile for {data['saveindex']['uid']}" + ) + # Alright.... time to bring the jank code + + for value in data["saveindex"]["data"]: + if "playedUserId" in value[1]: + self.data.profile.put_profile( + data["saveindex"]["uid"], self.version, value[0], value[1] + ) + if "mcode" not in value[1]: + self.data.profile.put_profile( + data["saveindex"]["uid"], self.version, value[0], value[1] + ) + if "shopId" in value: + continue + if "mcode" in value[1] and "musicState" in value[1]: + song_json = json.loads(value[1]) + + songCode = [] + songCode.append( + { + "mcode": song_json["mcode"], + "musicState": song_json["musicState"], + "playCount": song_json["playCount"], + "totalScore": song_json["totalScore"], + "highScore": song_json["highScore"], + "clearRate": song_json["clearRate"], + "rankPoint": song_json["rankPoint"], + "combo": song_json["combo"], + "coupleUserId": song_json["coupleUserId"], + "difficulty": song_json["difficulty"], + "isFullCombo": song_json["isFullCombo"], + "clearGaugeType": song_json["clearGaugeType"], + "fieldType": song_json["fieldType"], + "gameType": song_json["gameType"], + "grade": song_json["grade"], + "unlockState": song_json["unlockState"], + "extraState": song_json["extraState"], + "index": value[0], + } + ) + self.data.score.put_best_score( + data["saveindex"]["uid"], + song_json["mcode"], + self.version, + value[0], + songCode[0], + ) + return {} + else: + self.logger.info( + f"Saving CrossBeats REV Sunrise profile for {data['saveindex']['uid']}" + ) + + # Sunrise try: - profileIndex = save_data['index'].index('0') + profileIndex = save_data["index"].index("0") except: - return({"data":""}) #Maybe + return {"data": ""} # Maybe profile = json.loads(save_data["data"][profileIndex]) aimeId = profile["aimeId"] @@ -265,65 +328,91 @@ class CxbBase(): for index, value in enumerate(data["saveindex"]["data"]): if int(data["saveindex"]["index"][index]) == 101: - 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: - 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: - self.data.profile.put_profile(aimeId, self.version, data["saveindex"]["index"][index], value) - if 'playedUserId' in value: - 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: - self.data.profile.put_profile(aimeId, self.version, data["saveindex"]["index"][index], json.loads(value)) - if 'shopId' in value: + 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 + ): + 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 + ): + self.data.profile.put_profile( + aimeId, self.version, data["saveindex"]["index"][index], value + ) + if "playedUserId" in value: + 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: + self.data.profile.put_profile( + aimeId, + self.version, + data["saveindex"]["index"][index], + json.loads(value), + ) + if "shopId" in value: continue # MusicList Index for the profile indexSongList = [] for value in data["saveindex"]["index"]: - if int(value) in range(100000,110000): + if int(value) in range(100000, 110000): indexSongList.append(value) - + for index, value in enumerate(data["saveindex"]["data"]): - if 'mcode' not in value: + if "mcode" not in value: continue - if 'playedUserId' in value: + if "playedUserId" in value: continue - + data1 = json.loads(value) songCode = [] - songCode.append({ - "mcode": data1['mcode'], - "musicState": data1['musicState'], - "playCount": data1['playCount'], - "totalScore": data1['totalScore'], - "highScore": data1['highScore'], - "everHighScore": data1['everHighScore'], - "clearRate": data1['clearRate'], - "rankPoint": data1['rankPoint'], - "normalCR": data1['normalCR'], - "survivalCR": data1['survivalCR'], - "ultimateCR": data1['ultimateCR'], - "nohopeCR": data1['nohopeCR'], - "combo": data1['combo'], - "coupleUserId": data1['coupleUserId'], - "difficulty": data1['difficulty'], - "isFullCombo": data1['isFullCombo'], - "clearGaugeType": data1['clearGaugeType'], - "fieldType": data1['fieldType'], - "gameType": data1['gameType'], - "grade": data1['grade'], - "unlockState": data1['unlockState'], - "extraState": data1['extraState'], - "index": indexSongList[i] - }) + songCode.append( + { + "mcode": data1["mcode"], + "musicState": data1["musicState"], + "playCount": data1["playCount"], + "totalScore": data1["totalScore"], + "highScore": data1["highScore"], + "everHighScore": data1["everHighScore"], + "clearRate": data1["clearRate"], + "rankPoint": data1["rankPoint"], + "normalCR": data1["normalCR"], + "survivalCR": data1["survivalCR"], + "ultimateCR": data1["ultimateCR"], + "nohopeCR": data1["nohopeCR"], + "combo": data1["combo"], + "coupleUserId": data1["coupleUserId"], + "difficulty": data1["difficulty"], + "isFullCombo": data1["isFullCombo"], + "clearGaugeType": data1["clearGaugeType"], + "fieldType": data1["fieldType"], + "gameType": data1["gameType"], + "grade": data1["grade"], + "unlockState": data1["unlockState"], + "extraState": data1["extraState"], + "index": indexSongList[i], + } + ) - self.data.score.put_best_score(aimeId, data1['mcode'], self.version, indexSongList[i], songCode[0]) + self.data.score.put_best_score( + aimeId, data1["mcode"], self.version, indexSongList[i], songCode[0] + ) i += 1 - return({}) - + return {} + def handle_action_sprankreq_request(self, data: Dict) -> Dict: - uid = data['sprankreq']['uid'] + uid = data["sprankreq"]["uid"] self.logger.info(f"Get best rankings for {uid}") p = self.data.score.get_best_rankings(uid) @@ -331,90 +420,122 @@ class CxbBase(): for rank in p: if rank["song_id"] is not None: - rankList.append({ - "sc": [rank["score"],rank["song_id"]], - "rid": rank["rev_id"], - "clear": rank["clear"] - }) + rankList.append( + { + "sc": [rank["score"], rank["song_id"]], + "rid": rank["rev_id"], + "clear": rank["clear"], + } + ) else: - rankList.append({ - "sc": [rank["score"]], - "rid": rank["rev_id"], - "clear": rank["clear"] - }) + rankList.append( + { + "sc": [rank["score"]], + "rid": rank["rev_id"], + "clear": rank["clear"], + } + ) - return({ + return { "uid": data["sprankreq"]["uid"], "aid": data["sprankreq"]["aid"], "rank": rankList, - "rankx":[1,1,1] - }) - + "rankx": [1, 1, 1], + } + def handle_action_getadv_request(self, data: Dict) -> Dict: - return({"data":[{"r":"1","i":"100300","c":"20"}]}) - + return {"data": [{"r": "1", "i": "100300", "c": "20"}]} + def handle_action_getmsg_request(self, data: Dict) -> Dict: - return({"msgs":[]}) - + return {"msgs": []} + def handle_auth_logout_request(self, data: Dict) -> Dict: - return({"auth":True}) - - def handle_action_rankreg_request(self, data: Dict) -> Dict: - uid = data['rankreg']['uid'] + return {"auth": True} + + 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}") - for rid in data['rankreg']['data']: - #REV S2 + for rid in data["rankreg"]["data"]: + # REV S2 if "clear" in rid: try: - self.data.score.put_ranking(user_id=uid, rev_id=int(rid["rid"]), song_id=int(rid["sc"][1]), score=int(rid["sc"][0]), clear=rid["clear"]) + self.data.score.put_ranking( + user_id=uid, + rev_id=int(rid["rid"]), + song_id=int(rid["sc"][1]), + score=int(rid["sc"][0]), + clear=rid["clear"], + ) except: - self.data.score.put_ranking(user_id=uid, rev_id=int(rid["rid"]), song_id=0, score=int(rid["sc"][0]), clear=rid["clear"]) - #REV + self.data.score.put_ranking( + user_id=uid, + rev_id=int(rid["rid"]), + song_id=0, + score=int(rid["sc"][0]), + clear=rid["clear"], + ) + # REV else: try: - self.data.score.put_ranking(user_id=uid, rev_id=int(rid["rid"]), song_id=int(rid["sc"][1]), score=int(rid["sc"][0]), clear=0) + self.data.score.put_ranking( + user_id=uid, + rev_id=int(rid["rid"]), + song_id=int(rid["sc"][1]), + score=int(rid["sc"][0]), + clear=0, + ) except: - self.data.score.put_ranking(user_id=uid, rev_id=int(rid["rid"]), song_id=0, score=int(rid["sc"][0]), clear=0) - return({}) - + self.data.score.put_ranking( + user_id=uid, + rev_id=int(rid["rid"]), + song_id=0, + score=int(rid["sc"][0]), + clear=0, + ) + return {} + def handle_action_addenergy_request(self, data: Dict) -> Dict: - uid = data['addenergy']['uid'] + uid = data["addenergy"]["uid"] self.logger.info(f"Add energy to user {uid}") profile = self.data.profile.get_profile_index(0, uid, self.version) data1 = profile["data"] p = self.data.item.get_energy(uid) energy = p["energy"] - - if not p: + + if not p: self.data.item.put_energy(uid, 5) - - return({ + + return { "class": data1["myClass"], "granted": "5", "total": "5", - "threshold": "1000" - }) + "threshold": "1000", + } array = [] - + newenergy = int(energy) + 5 self.data.item.put_energy(uid, newenergy) if int(energy) <= 995: - array.append({ - "class": data1["myClass"], - "granted": "5", - "total": str(energy), - "threshold": "1000" - }) + array.append( + { + "class": data1["myClass"], + "granted": "5", + "total": str(energy), + "threshold": "1000", + } + ) else: - array.append({ - "class": data1["myClass"], - "granted": "0", - "total": str(energy), - "threshold": "1000" - }) + array.append( + { + "class": data1["myClass"], + "granted": "0", + "total": str(energy), + "threshold": "1000", + } + ) return array[0] def handle_action_eventreq_request(self, data: Dict) -> Dict: diff --git a/titles/cxb/config.py b/titles/cxb/config.py index e83c1f1..00b7290 100644 --- a/titles/cxb/config.py +++ b/titles/cxb/config.py @@ -1,40 +1,60 @@ from core.config import CoreConfig -class CxbServerConfig(): + +class CxbServerConfig: def __init__(self, parent_config: "CxbConfig"): self.__config = parent_config - + @property def enable(self) -> bool: - return CoreConfig.get_config_field(self.__config, 'cxb', 'server', 'enable', default=True) + return CoreConfig.get_config_field( + self.__config, "cxb", "server", "enable", default=True + ) @property def loglevel(self) -> int: - return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'cxb', 'server', 'loglevel', default="info")) + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "cxb", "server", "loglevel", default="info" + ) + ) @property def hostname(self) -> str: - return CoreConfig.get_config_field(self.__config, 'cxb', 'server', 'hostname', default="localhost") + return CoreConfig.get_config_field( + self.__config, "cxb", "server", "hostname", default="localhost" + ) @property def ssl_enable(self) -> bool: - return CoreConfig.get_config_field(self.__config, 'cxb', 'server', 'ssl_enable', default=False) + return CoreConfig.get_config_field( + self.__config, "cxb", "server", "ssl_enable", default=False + ) @property def port(self) -> int: - return CoreConfig.get_config_field(self.__config, 'cxb', 'server', 'port', default=8082) + return CoreConfig.get_config_field( + self.__config, "cxb", "server", "port", default=8082 + ) @property def port_secure(self) -> int: - return CoreConfig.get_config_field(self.__config, 'cxb', 'server', 'port_secure', default=443) - + return CoreConfig.get_config_field( + self.__config, "cxb", "server", "port_secure", default=443 + ) + @property def ssl_cert(self) -> str: - return CoreConfig.get_config_field(self.__config, 'cxb', 'server', 'ssl_cert', default="cert/title.crt") + return CoreConfig.get_config_field( + self.__config, "cxb", "server", "ssl_cert", default="cert/title.crt" + ) @property def ssl_key(self) -> str: - return CoreConfig.get_config_field(self.__config, 'cxb', 'server', 'ssl_key', default="cert/title.key") + return CoreConfig.get_config_field( + self.__config, "cxb", "server", "ssl_key", default="cert/title.key" + ) + class CxbConfig(dict): def __init__(self) -> None: diff --git a/titles/cxb/const.py b/titles/cxb/const.py index 338b8a5..8ce5480 100644 --- a/titles/cxb/const.py +++ b/titles/cxb/const.py @@ -1,4 +1,4 @@ -class CxbConstants(): +class CxbConstants: GAME_CODE = "SDCA" CONFIG_NAME = "cxb.yaml" @@ -8,8 +8,13 @@ class CxbConstants(): VER_CROSSBEATS_REV_SUNRISE_S2 = 2 VER_CROSSBEATS_REV_SUNRISE_S2_OMNI = 3 - VERSION_NAMES = ("crossbeats REV.", "crossbeats REV. SUNRISE", "crossbeats REV. SUNRISE S2", "crossbeats REV. SUNRISE S2 Omnimix") + VERSION_NAMES = ( + "crossbeats REV.", + "crossbeats REV. SUNRISE", + "crossbeats REV. SUNRISE S2", + "crossbeats REV. SUNRISE S2 Omnimix", + ) @classmethod def game_ver_to_string(cls, ver: int): - return cls.VERSION_NAMES[ver] \ No newline at end of file + return cls.VERSION_NAMES[ver] diff --git a/titles/cxb/database.py b/titles/cxb/database.py index 8fed1dc..081e2bd 100644 --- a/titles/cxb/database.py +++ b/titles/cxb/database.py @@ -1,8 +1,8 @@ - from core.data import Data from core.config import CoreConfig from titles.cxb.schema import CxbProfileData, CxbScoreData, CxbItemData, CxbStaticData + class CxbData(Data): def __init__(self, cfg: CoreConfig) -> None: super().__init__(cfg) diff --git a/titles/cxb/index.py b/titles/cxb/index.py index ad852a8..d6ab74b 100644 --- a/titles/cxb/index.py +++ b/titles/cxb/index.py @@ -17,6 +17,7 @@ from titles.cxb.rev import CxbRev from titles.cxb.rss1 import CxbRevSunriseS1 from titles.cxb.rss2 import CxbRevSunriseS2 + class CxbServlet(resource.Resource): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: self.isLeaf = True @@ -24,62 +25,85 @@ class CxbServlet(resource.Resource): self.core_cfg = core_cfg self.game_cfg = CxbConfig() if path.exists(f"{cfg_dir}/{CxbConstants.CONFIG_NAME}"): - self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{CxbConstants.CONFIG_NAME}"))) + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{CxbConstants.CONFIG_NAME}")) + ) self.logger = logging.getLogger("cxb") if not hasattr(self.logger, "inited"): log_fmt_str = "[%(asctime)s] CXB | %(levelname)s | %(message)s" log_fmt = logging.Formatter(log_fmt_str) - fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.core_cfg.server.log_dir, "cxb"), encoding='utf8', - when="d", backupCount=10) + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.core_cfg.server.log_dir, "cxb"), + encoding="utf8", + when="d", + backupCount=10, + ) fileHandler.setFormatter(log_fmt) - + consoleHandler = logging.StreamHandler() consoleHandler.setFormatter(log_fmt) self.logger.addHandler(fileHandler) self.logger.addHandler(consoleHandler) - + self.logger.setLevel(self.game_cfg.server.loglevel) - coloredlogs.install(level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str) + coloredlogs.install( + level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str + ) self.logger.inited = True - + self.versions = [ CxbRev(core_cfg, self.game_cfg), CxbRevSunriseS1(core_cfg, self.game_cfg), CxbRevSunriseS2(core_cfg, self.game_cfg), ] - + @classmethod - def get_allnet_info(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str, str]: + def get_allnet_info( + cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str + ) -> Tuple[bool, str, str]: game_cfg = CxbConfig() if path.exists(f"{cfg_dir}/{CxbConstants.CONFIG_NAME}"): - game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{CxbConstants.CONFIG_NAME}"))) + game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{CxbConstants.CONFIG_NAME}")) + ) if not game_cfg.server.enable: return (False, "", "") - + if core_cfg.server.is_develop: - return (True, f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/", "") - + return ( + True, + f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/", + "", + ) + return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", "") def setup(self): if self.game_cfg.server.enable: - endpoints.serverFromString(reactor, f"tcp:{self.game_cfg.server.port}:interface={self.core_cfg.server.listen_address}")\ - .listen(server.Site(CxbServlet(self.core_cfg, self.cfg_dir))) - - if self.core_cfg.server.is_develop and self.game_cfg.server.ssl_enable: - endpoints.serverFromString(reactor, f"ssl:{self.game_cfg.server.port_secure}"\ - f":interface={self.core_cfg.server.listen_address}:privateKey={self.game_cfg.server.ssl_key}:"\ - f"certKey={self.game_cfg.server.ssl_cert}")\ - .listen(server.Site(CxbServlet(self.core_cfg, self.cfg_dir))) + endpoints.serverFromString( + reactor, + f"tcp:{self.game_cfg.server.port}:interface={self.core_cfg.server.listen_address}", + ).listen(server.Site(CxbServlet(self.core_cfg, self.cfg_dir))) - self.logger.info(f"Crossbeats title server ready on port {self.game_cfg.server.port} & {self.game_cfg.server.port_secure}") + if self.core_cfg.server.is_develop and self.game_cfg.server.ssl_enable: + endpoints.serverFromString( + reactor, + f"ssl:{self.game_cfg.server.port_secure}" + f":interface={self.core_cfg.server.listen_address}:privateKey={self.game_cfg.server.ssl_key}:" + f"certKey={self.game_cfg.server.ssl_cert}", + ).listen(server.Site(CxbServlet(self.core_cfg, self.cfg_dir))) + + self.logger.info( + f"Crossbeats title server ready on port {self.game_cfg.server.port} & {self.game_cfg.server.port_secure}" + ) else: - self.logger.info(f"Crossbeats title server ready on port {self.game_cfg.server.port}") - + self.logger.info( + f"Crossbeats title server ready on port {self.game_cfg.server.port}" + ) def render_POST(self, request: Request): version = 0 @@ -96,21 +120,28 @@ class CxbServlet(resource.Resource): except Exception as e: try: - req_json: Dict = json.loads(req_bytes.decode().replace('"', '\\"').replace("'", '"')) + req_json: Dict = json.loads( + req_bytes.decode().replace('"', '\\"').replace("'", '"') + ) except Exception as f: - self.logger.warn(f"Error decoding json: {e} / {f} - {req_url} - {req_bytes}") + self.logger.warn( + f"Error decoding json: {e} / {f} - {req_url} - {req_bytes}" + ) return b"" - + if req_json == {}: self.logger.warn(f"Empty json request to {req_url}") return b"" - + cmd = url_split[len(url_split) - 1] subcmd = list(req_json.keys())[0] if subcmd == "dldate": - if not type(req_json["dldate"]) is dict or "filetype" not in req_json["dldate"]: + if ( + not type(req_json["dldate"]) is dict + or "filetype" not in req_json["dldate"] + ): self.logger.warn(f"Malformed dldate request: {req_url} {req_json}") return b"" @@ -119,7 +150,9 @@ class CxbServlet(resource.Resource): version = int(filetype_split[0]) filetype_inflect_split = inflection.underscore(filetype).split("/") - match = re.match("^([A-Za-z]*)(\d\d\d\d)$", filetype_split[len(filetype_split) - 1]) + match = re.match( + "^([A-Za-z]*)(\d\d\d\d)$", filetype_split[len(filetype_split) - 1] + ) if match: subcmd = f"{inflection.underscore(match.group(1))}xxxx" else: @@ -128,7 +161,7 @@ class CxbServlet(resource.Resource): filetype = subcmd func_to_find = f"handle_{cmd}_{subcmd}_request" - + if version <= 10102: version_string = "Rev" internal_ver = CxbConstants.VER_CROSSBEATS_REV @@ -136,28 +169,28 @@ class CxbServlet(resource.Resource): elif version == 10113 or version == 10103: version_string = "Rev SunriseS1" internal_ver = CxbConstants.VER_CROSSBEATS_REV_SUNRISE_S1 - + elif version >= 10114 or version == 10104: version_string = "Rev SunriseS2" internal_ver = CxbConstants.VER_CROSSBEATS_REV_SUNRISE_S2 - + else: version_string = "Base" - + self.logger.info(f"{version_string} Request {req_url} -> {filetype}") self.logger.debug(req_json) try: handler = getattr(self.versions[internal_ver], func_to_find) resp = handler(req_json) - - except AttributeError as e: + + except AttributeError as e: self.logger.warning(f"Unhandled {version_string} request {req_url} - {e}") resp = {} except Exception as e: self.logger.error(f"Error handling {version_string} method {req_url} - {e}") raise - - self.logger.debug(f"{version_string} Response {resp}") + + self.logger.debug(f"{version_string} Response {resp}") return json.dumps(resp, ensure_ascii=False).encode("utf-8") diff --git a/titles/cxb/read.py b/titles/cxb/read.py index 6117f4e..cf2d8e1 100644 --- a/titles/cxb/read.py +++ b/titles/cxb/read.py @@ -8,13 +8,23 @@ from core.config import CoreConfig from titles.cxb.database import CxbData from titles.cxb.const import CxbConstants + class CxbReader(BaseReader): - def __init__(self, config: CoreConfig, version: int, bin_arg: Optional[str], opt_arg: Optional[str], extra: Optional[str]) -> None: + def __init__( + self, + config: CoreConfig, + version: int, + bin_arg: Optional[str], + opt_arg: Optional[str], + extra: Optional[str], + ) -> None: super().__init__(config, version, bin_arg, opt_arg, extra) self.data = CxbData(config) try: - self.logger.info(f"Start importer for {CxbConstants.game_ver_to_string(version)}") + self.logger.info( + f"Start importer for {CxbConstants.game_ver_to_string(version)}" + ) except IndexError: self.logger.error(f"Invalid project cxb version {version}") exit(1) @@ -28,7 +38,7 @@ class CxbReader(BaseReader): 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}") @@ -45,18 +55,73 @@ class CxbReader(BaseReader): if not "N/A" in row["standard"]: self.logger.info(f"Added song {song_id} chart 0") - self.data.static.put_music(self.version, song_id, index, 0, title, artist, genre, int(row["standard"].replace("Standard ","").replace("N/A","0"))) + self.data.static.put_music( + self.version, + song_id, + index, + 0, + title, + artist, + genre, + int( + row["standard"] + .replace("Standard ", "") + .replace("N/A", "0") + ), + ) if not "N/A" in row["hard"]: self.logger.info(f"Added song {song_id} chart 1") - self.data.static.put_music(self.version, song_id, index, 1, title, artist, genre, int(row["hard"].replace("Hard ","").replace("N/A","0"))) + self.data.static.put_music( + self.version, + song_id, + index, + 1, + title, + artist, + genre, + int(row["hard"].replace("Hard ", "").replace("N/A", "0")), + ) if not "N/A" in row["master"]: self.logger.info(f"Added song {song_id} chart 2") - self.data.static.put_music(self.version, song_id, index, 2, title, artist, genre, int(row["master"].replace("Master ","").replace("N/A","0"))) + self.data.static.put_music( + self.version, + song_id, + index, + 2, + title, + artist, + genre, + int( + row["master"].replace("Master ", "").replace("N/A", "0") + ), + ) if not "N/A" in row["unlimited"]: self.logger.info(f"Added song {song_id} chart 3") - self.data.static.put_music(self.version, song_id, index, 3, title, artist, genre, int(row["unlimited"].replace("Unlimited ","").replace("N/A","0"))) + self.data.static.put_music( + self.version, + song_id, + index, + 3, + title, + artist, + genre, + int( + row["unlimited"] + .replace("Unlimited ", "") + .replace("N/A", "0") + ), + ) if not "N/A" in row["easy"]: self.logger.info(f"Added song {song_id} chart 4") - self.data.static.put_music(self.version, song_id, index, 4, title, artist, genre, int(row["easy"].replace("Easy ","").replace("N/A","0"))) + self.data.static.put_music( + self.version, + song_id, + index, + 4, + title, + artist, + genre, + int(row["easy"].replace("Easy ", "").replace("N/A", "0")), + ) except: self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping") diff --git a/titles/cxb/rev.py b/titles/cxb/rev.py index 9a24c17..c78e622 100644 --- a/titles/cxb/rev.py +++ b/titles/cxb/rev.py @@ -11,155 +11,191 @@ from titles.cxb.config import CxbConfig from titles.cxb.base import CxbBase from titles.cxb.const import CxbConstants + class CxbRev(CxbBase): def __init__(self, cfg: CoreConfig, game_cfg: CxbConfig) -> None: super().__init__(cfg, game_cfg) self.version = CxbConstants.VER_CROSSBEATS_REV - + def handle_data_path_list_request(self, data: Dict) -> Dict: - return { "data": "" } - + return {"data": ""} + 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'] + userid = score_data["usid"] - self.data.score.put_playlog(userid, score_data['mcode'], score_data['difficulty'], score_data["score"], int(Decimal(score_data["clearrate"]) * 100), score_data["flawless"], score_data["super"], score_data["cool"], score_data["fast"], score_data["fast2"], score_data["slow"], score_data["slow2"], score_data["fail"], score_data["combo"]) - return({"data":True}) - return {"data": True } + self.data.score.put_playlog( + userid, + score_data["mcode"], + score_data["difficulty"], + score_data["score"], + int(Decimal(score_data["clearrate"]) * 100), + score_data["flawless"], + score_data["super"], + score_data["cool"], + score_data["fast"], + score_data["fast2"], + score_data["slow"], + score_data["slow2"], + score_data["fail"], + score_data["combo"], + ) + return {"data": True} + return {"data": True} - @cached(lifetime=86400) + @cached(lifetime=86400) def handle_data_music_list_request(self, data: Dict) -> Dict: ret_str = "" with open(r"titles/cxb/rev_data/MusicArchiveList.csv") as music: lines = music.readlines() for line in lines: - line_split = line.split(',') + line_split = line.split(",") ret_str += f"{line_split[0]},{line_split[1]},{line_split[2]},{line_split[3]},{line_split[4]},{line_split[5]},{line_split[6]},{line_split[7]},{line_split[8]},{line_split[9]},{line_split[10]},{line_split[11]},{line_split[12]},{line_split[13]},{line_split[14]},\r\n" - - return({"data":ret_str}) - + + return {"data": ret_str} + @cached(lifetime=86400) def handle_data_item_list_icon_request(self, data: Dict) -> Dict: ret_str = "\r\n#ItemListIcon\r\n" - with open(r"titles/cxb/rev_data/Item/ItemArchiveList_Icon.csv", encoding="utf-8") as item: + with open( + r"titles/cxb/rev_data/Item/ItemArchiveList_Icon.csv", encoding="utf-8" + ) as item: lines = item.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" - return({"data":ret_str}) + return {"data": ret_str} - @cached(lifetime=86400) + @cached(lifetime=86400) def handle_data_item_list_skin_notes_request(self, data: Dict) -> Dict: ret_str = "\r\n#ItemListSkinNotes\r\n" - with open(r"titles/cxb/rev_data/Item/ItemArchiveList_SkinNotes.csv", encoding="utf-8") as item: + with open( + r"titles/cxb/rev_data/Item/ItemArchiveList_SkinNotes.csv", encoding="utf-8" + ) as item: lines = item.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" - return({"data":ret_str}) + return {"data": ret_str} - @cached(lifetime=86400) + @cached(lifetime=86400) def handle_data_item_list_skin_effect_request(self, data: Dict) -> Dict: ret_str = "\r\n#ItemListSkinEffect\r\n" - with open(r"titles/cxb/rev_data/Item/ItemArchiveList_SkinEffect.csv", encoding="utf-8") as item: + with open( + r"titles/cxb/rev_data/Item/ItemArchiveList_SkinEffect.csv", encoding="utf-8" + ) as item: lines = item.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" - return({"data":ret_str}) + return {"data": ret_str} - @cached(lifetime=86400) + @cached(lifetime=86400) def handle_data_item_list_skin_bg_request(self, data: Dict) -> Dict: ret_str = "\r\n#ItemListSkinBg\r\n" - with open(r"titles/cxb/rev_data/Item/ItemArchiveList_SkinBg.csv", encoding="utf-8") as item: + with open( + r"titles/cxb/rev_data/Item/ItemArchiveList_SkinBg.csv", encoding="utf-8" + ) as item: lines = item.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" - return({"data":ret_str}) + return {"data": ret_str} - @cached(lifetime=86400) + @cached(lifetime=86400) def handle_data_item_list_title_request(self, data: Dict) -> Dict: ret_str = "\r\n#ItemListTitle\r\n" - with open(r"titles/cxb/rev_data/Item/ItemList_Title.csv", encoding="shift-jis") as item: + with open( + r"titles/cxb/rev_data/Item/ItemList_Title.csv", encoding="shift-jis" + ) as item: lines = item.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" - return({"data":ret_str}) + return {"data": ret_str} - @cached(lifetime=86400) + @cached(lifetime=86400) def handle_data_shop_list_music_request(self, data: Dict) -> Dict: ret_str = "\r\n#ShopListMusic\r\n" - with open(r"titles/cxb/rev_data/Shop/ShopList_Music.csv", encoding="shift-jis") as shop: + with open( + r"titles/cxb/rev_data/Shop/ShopList_Music.csv", encoding="shift-jis" + ) as shop: lines = shop.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" - return({"data":ret_str}) + return {"data": ret_str} - @cached(lifetime=86400) + @cached(lifetime=86400) def handle_data_shop_list_icon_request(self, data: Dict) -> Dict: ret_str = "\r\n#ShopListIcon\r\n" - with open(r"titles/cxb/rev_data/Shop/ShopList_Icon.csv", encoding="shift-jis") as shop: + with open( + r"titles/cxb/rev_data/Shop/ShopList_Icon.csv", encoding="shift-jis" + ) as shop: lines = shop.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" - return({"data":ret_str}) + return {"data": ret_str} - @cached(lifetime=86400) + @cached(lifetime=86400) def handle_data_shop_list_title_request(self, data: Dict) -> Dict: ret_str = "\r\n#ShopListTitle\r\n" - with open(r"titles/cxb/rev_data/Shop/ShopList_Title.csv", encoding="shift-jis") as shop: + with open( + r"titles/cxb/rev_data/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}) - - def handle_data_shop_list_skin_hud_request(self, data: Dict) -> Dict: - return({"data":""}) - - def handle_data_shop_list_skin_arrow_request(self, data: Dict) -> Dict: - return({"data":""}) - - def handle_data_shop_list_skin_hit_request(self, data: Dict) -> Dict: - return({"data":""}) + return {"data": ret_str} - @cached(lifetime=86400) + def handle_data_shop_list_skin_hud_request(self, data: Dict) -> Dict: + return {"data": ""} + + def handle_data_shop_list_skin_arrow_request(self, data: Dict) -> Dict: + return {"data": ""} + + def handle_data_shop_list_skin_hit_request(self, data: Dict) -> Dict: + return {"data": ""} + + @cached(lifetime=86400) def handle_data_shop_list_sale_request(self, data: Dict) -> Dict: ret_str = "\r\n#ShopListSale\r\n" - with open(r"titles/cxb/rev_data/Shop/ShopList_Sale.csv", encoding="shift-jis") as shop: + with open( + r"titles/cxb/rev_data/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}) + return {"data": ret_str} def handle_data_extra_stage_list_request(self, data: Dict) -> Dict: - return({"data":""}) + return {"data": ""} - @cached(lifetime=86400) + @cached(lifetime=86400) def handle_data_exxxxx_request(self, data: Dict) -> Dict: extra_num = int(data["dldate"]["filetype"][-4:]) ret_str = "" - with open(fr"titles/cxb/rev_data/Ex000{extra_num}.csv", encoding="shift-jis") as stage: + with open( + rf"titles/cxb/rev_data/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}) - - def handle_data_bonus_list10100_request(self, data: Dict) -> Dict: - return({"data": ""}) - - def handle_data_free_coupon_request(self, data: Dict) -> Dict: - return({"data": ""}) + return {"data": ret_str} - @cached(lifetime=86400) + def handle_data_bonus_list10100_request(self, data: Dict) -> Dict: + return {"data": ""} + + def handle_data_free_coupon_request(self, data: Dict) -> Dict: + return {"data": ""} + + @cached(lifetime=86400) def handle_data_news_list_request(self, data: Dict) -> Dict: ret_str = "" with open(r"titles/cxb/rev_data/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}) - + return {"data": ret_str} + def handle_data_tips_request(self, data: Dict) -> Dict: - return({"data":""}) - + return {"data": ""} + @cached(lifetime=86400) def handle_data_license_request(self, data: Dict) -> Dict: ret_str = "" @@ -167,90 +203,104 @@ class CxbRev(CxbBase): lines = lic.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" - return({"data":ret_str}) + return {"data": ret_str} - @cached(lifetime=86400) + @cached(lifetime=86400) def handle_data_course_list_request(self, data: Dict) -> Dict: ret_str = "" - with open(r"titles/cxb/rev_data/Course/CourseList.csv", encoding="UTF-8") as course: + with open( + r"titles/cxb/rev_data/Course/CourseList.csv", encoding="UTF-8" + ) as course: lines = course.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" - return({"data":ret_str}) + return {"data": ret_str} - @cached(lifetime=86400) + @cached(lifetime=86400) 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(fr"titles/cxb/rev_data/Course/Cs000{extra_num}.csv", encoding="shift-jis") as course: + with open( + rf"titles/cxb/rev_data/Course/Cs000{extra_num}.csv", encoding="shift-jis" + ) as course: lines = course.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" - return({"data":ret_str}) + return {"data": ret_str} - @cached(lifetime=86400) + @cached(lifetime=86400) def handle_data_mission_list_request(self, data: Dict) -> Dict: ret_str = "" - with open(r"titles/cxb/rev_data/MissionList.csv", encoding="shift-jis") as mission: + with open( + r"titles/cxb/rev_data/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}) - - def handle_data_mission_bonus_request(self, data: Dict) -> Dict: - return({"data": ""}) - - def handle_data_unlimited_mission_request(self, data: Dict) -> Dict: - return({"data": ""}) + return {"data": ret_str} - @cached(lifetime=86400) + def handle_data_mission_bonus_request(self, data: Dict) -> Dict: + return {"data": ""} + + def handle_data_unlimited_mission_request(self, data: Dict) -> Dict: + return {"data": ""} + + @cached(lifetime=86400) def handle_data_event_list_request(self, data: Dict) -> Dict: ret_str = "" - with open(r"titles/cxb/rev_data/Event/EventArchiveList.csv", encoding="shift-jis") as mission: + with open( + r"titles/cxb/rev_data/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}) + return {"data": ret_str} def handle_data_event_music_list_request(self, data: Dict) -> Dict: - return({"data": ""}) - - def handle_data_event_mission_list_request(self, data: Dict) -> Dict: - return({"data": ""}) - - def handle_data_event_achievement_single_high_score_list_request(self, data: Dict) -> Dict: - return({"data": ""}) - - def handle_data_event_achievement_single_accumulation_request(self, data: Dict) -> Dict: - return({"data": ""}) - - def handle_data_event_ranking_high_score_list_request(self, data: Dict) -> Dict: - return({"data": ""}) - - def handle_data_event_ranking_accumulation_list_request(self, data: Dict) -> Dict: - return({"data": ""}) - - def handle_data_event_ranking_stamp_list_request(self, data: Dict) -> Dict: - return({"data": ""}) - - def handle_data_event_ranking_store_list_request(self, data: Dict) -> Dict: - return({"data": ""}) - - def handle_data_event_ranking_area_list_request(self, data: Dict) -> Dict: - return({"data": ""}) + return {"data": ""} - @cached(lifetime=86400) + def handle_data_event_mission_list_request(self, data: Dict) -> Dict: + return {"data": ""} + + def handle_data_event_achievement_single_high_score_list_request( + self, data: Dict + ) -> Dict: + return {"data": ""} + + def handle_data_event_achievement_single_accumulation_request( + self, data: Dict + ) -> Dict: + return {"data": ""} + + def handle_data_event_ranking_high_score_list_request(self, data: Dict) -> Dict: + return {"data": ""} + + def handle_data_event_ranking_accumulation_list_request(self, data: Dict) -> Dict: + return {"data": ""} + + def handle_data_event_ranking_stamp_list_request(self, data: Dict) -> Dict: + return {"data": ""} + + def handle_data_event_ranking_store_list_request(self, data: Dict) -> Dict: + return {"data": ""} + + def handle_data_event_ranking_area_list_request(self, data: Dict) -> Dict: + return {"data": ""} + + @cached(lifetime=86400) def handle_data_event_stamp_list_request(self, data: Dict) -> Dict: ret_str = "" - with open(r"titles/cxb/rev_data/Event/EventStampList.csv", encoding="shift-jis") as event: + with open( + r"titles/cxb/rev_data/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}) - + return {"data": ret_str} + 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"}) - + return {"data": "1,2,1,1,2,3,9,5,6,7,8,9,10,\r\n"} + def handle_data_server_state_request(self, data: Dict) -> Dict: - return({"data": True}) + return {"data": True} diff --git a/titles/cxb/rss1.py b/titles/cxb/rss1.py index e480238..ba428f7 100644 --- a/titles/cxb/rss1.py +++ b/titles/cxb/rss1.py @@ -11,128 +11,147 @@ from titles.cxb.config import CxbConfig from titles.cxb.base import CxbBase from titles.cxb.const import CxbConstants + class CxbRevSunriseS1(CxbBase): def __init__(self, cfg: CoreConfig, game_cfg: CxbConfig) -> None: super().__init__(cfg, game_cfg) self.version = CxbConstants.VER_CROSSBEATS_REV_SUNRISE_S1 - - def handle_data_path_list_request(self, data: Dict) -> Dict: - return { "data": "" } - @cached(lifetime=86400) + def handle_data_path_list_request(self, data: Dict) -> Dict: + return {"data": ""} + + @cached(lifetime=86400) def handle_data_music_list_request(self, data: Dict) -> Dict: ret_str = "" with open(r"titles/cxb/rss1_data/MusicArchiveList.csv") as music: lines = music.readlines() for line in lines: - line_split = line.split(',') + line_split = line.split(",") ret_str += f"{line_split[0]},{line_split[1]},{line_split[2]},{line_split[3]},{line_split[4]},{line_split[5]},{line_split[6]},{line_split[7]},{line_split[8]},{line_split[9]},{line_split[10]},{line_split[11]},{line_split[12]},{line_split[13]},{line_split[14]},\r\n" - - return({"data":ret_str}) - @cached(lifetime=86400) + return {"data": ret_str} + + @cached(lifetime=86400) def handle_data_item_list_detail_request(self, data: Dict) -> Dict: - #ItemListIcon load + # ItemListIcon load ret_str = "#ItemListIcon\r\n" - with open(r"titles/cxb/rss1_data/Item/ItemList_Icon.csv", encoding="shift-jis") as item: + with open( + r"titles/cxb/rss1_data/Item/ItemList_Icon.csv", encoding="shift-jis" + ) as item: lines = item.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" - #ItemListTitle load + # ItemListTitle load ret_str += "\r\n#ItemListTitle\r\n" - with open(r"titles/cxb/rss1_data/Item/ItemList_Title.csv", encoding="shift-jis") as item: + with open( + r"titles/cxb/rss1_data/Item/ItemList_Title.csv", encoding="shift-jis" + ) as item: lines = item.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" - return({"data":ret_str}) + return {"data": ret_str} - @cached(lifetime=86400) + @cached(lifetime=86400) def handle_data_shop_list_detail_request(self, data: Dict) -> Dict: - #ShopListIcon load + # ShopListIcon load ret_str = "#ShopListIcon\r\n" - with open(r"titles/cxb/rss1_data/Shop/ShopList_Icon.csv", encoding="utf-8") as shop: + with open( + r"titles/cxb/rss1_data/Shop/ShopList_Icon.csv", encoding="utf-8" + ) as shop: lines = shop.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" - - #ShopListMusic load - ret_str += "\r\n#ShopListMusic\r\n" - with open(r"titles/cxb/rss1_data/Shop/ShopList_Music.csv", encoding="utf-8") as shop: - lines = shop.readlines() - 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/rss1_data/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/rss1_data/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/rss1_data/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/rss1_data/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(r"titles/cxb/rss1_data/Shop/ShopList_Title.csv", encoding="utf-8") as shop: - lines = shop.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":""}) - - def handle_data_ex0001_request(self, data: Dict) -> Dict: - return({"data":""}) - - def handle_data_one_more_extra_list_request(self, data: Dict) -> Dict: - return({"data":""}) - - def handle_data_bonus_list10100_request(self, data: Dict) -> Dict: - return({"data":""}) - - def handle_data_oe0001_request(self, data: Dict) -> Dict: - return({"data":""}) - - def handle_data_free_coupon_request(self, data: Dict) -> Dict: - return({"data":""}) - @cached(lifetime=86400) + # ShopListMusic load + ret_str += "\r\n#ShopListMusic\r\n" + with open( + r"titles/cxb/rss1_data/Shop/ShopList_Music.csv", encoding="utf-8" + ) as shop: + lines = shop.readlines() + 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/rss1_data/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/rss1_data/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/rss1_data/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/rss1_data/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( + r"titles/cxb/rss1_data/Shop/ShopList_Title.csv", encoding="utf-8" + ) as shop: + lines = shop.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": ""} + + def handle_data_ex0001_request(self, data: Dict) -> Dict: + return {"data": ""} + + def handle_data_one_more_extra_list_request(self, data: Dict) -> Dict: + return {"data": ""} + + def handle_data_bonus_list10100_request(self, data: Dict) -> Dict: + return {"data": ""} + + def handle_data_oe0001_request(self, data: Dict) -> Dict: + return {"data": ""} + + def handle_data_free_coupon_request(self, data: Dict) -> Dict: + return {"data": ""} + + @cached(lifetime=86400) def handle_data_news_list_request(self, data: Dict) -> Dict: ret_str = "" with open(r"titles/cxb/rss1_data/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}) + return {"data": ret_str} def handle_data_tips_request(self, data: Dict) -> Dict: - return({"data":""}) - + return {"data": ""} + def handle_data_release_info_list_request(self, data: Dict) -> Dict: - return({"data":""}) - + return {"data": ""} + @cached(lifetime=86400) def handle_data_random_music_list_request(self, data: Dict) -> Dict: ret_str = "" @@ -141,10 +160,12 @@ class CxbRevSunriseS1(CxbBase): count = 0 for line in lines: line_split = line.split(",") - ret_str += str(count) + "," + line_split[0] + "," + line_split[0] + ",\r\n" + ret_str += ( + str(count) + "," + line_split[0] + "," + line_split[0] + ",\r\n" + ) + + return {"data": ret_str} - return({"data":ret_str}) - @cached(lifetime=86400) def handle_data_license_request(self, data: Dict) -> Dict: ret_str = "" @@ -152,54 +173,58 @@ class CxbRevSunriseS1(CxbBase): lines = licenses.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" - return({"data":ret_str}) - + return {"data": ret_str} + @cached(lifetime=86400) def handle_data_course_list_request(self, data: Dict) -> Dict: ret_str = "" - with open(r"titles/cxb/rss1_data/Course/CourseList.csv", encoding="UTF-8") as course: + with open( + r"titles/cxb/rss1_data/Course/CourseList.csv", encoding="UTF-8" + ) as course: lines = course.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" - return({"data":ret_str}) + return {"data": ret_str} @cached(lifetime=86400) def handle_data_csxxxx_request(self, data: Dict) -> Dict: extra_num = int(data["dldate"]["filetype"][-4:]) ret_str = "" - with open(fr"titles/cxb/rss1_data/Course/Cs{extra_num}.csv", encoding="shift-jis") as course: + with open( + rf"titles/cxb/rss1_data/Course/Cs{extra_num}.csv", encoding="shift-jis" + ) as course: lines = course.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" - return({"data":ret_str}) - + return {"data": ret_str} + def handle_data_mission_list_request(self, data: Dict) -> Dict: - return({"data":""}) - + return {"data": ""} + def handle_data_mission_bonus_request(self, data: Dict) -> Dict: - return({"data":""}) - + return {"data": ""} + def handle_data_unlimited_mission_request(self, data: Dict) -> Dict: - return({"data":""}) - + return {"data": ""} + 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): + for i in range(0, 10): ret_str += f"80000{i},{i},{i},0,10000,,\r\n" ret_str += f"80000{i},{i},{i},1,10500,,\r\n" ret_str += f"80000{i},{i},{i},2,10500,,\r\n" - for i in range(10,13): + for i in range(10, 13): ret_str += f"8000{i},{i},{i},0,10000,,\r\n" ret_str += f"8000{i},{i},{i},1,10500,,\r\n" ret_str += f"8000{i},{i},{i},2,10500,,\r\n" - ret_str +="\r\n---\r\n0,150,100,100,100,100,\r\n" - for i in range(1,130): - ret_str +=f"{i},100,100,100,100,100,\r\n" - + ret_str += "\r\n---\r\n0,150,100,100,100,100,\r\n" + for i in range(1, 130): + ret_str += f"{i},100,100,100,100,100,\r\n" + ret_str += "---\r\n" - return({"data": ret_str}) - + return {"data": ret_str} + @cached(lifetime=86400) def handle_data_partnerxxxx_request(self, data: Dict) -> Dict: partner_num = int(data["dldate"]["filetype"][-4:]) @@ -208,50 +233,54 @@ class CxbRevSunriseS1(CxbBase): lines = partner.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" - return({"data": ret_str}) - + return {"data": ret_str} + def handle_data_server_state_request(self, data: Dict) -> Dict: - return({"data": True}) - + return {"data": True} + def handle_data_settings_request(self, data: Dict) -> Dict: - return({"data": "2,\r\n"}) + return {"data": "2,\r\n"} 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 + # story id, story name, game version, start time, end time, course arc, unlock flag, song mcode for menu ret_str = "\r\n" - ret_str += f"st0000,RISING PURPLE,10104,1464370990,4096483201,Cs1000,-1,purple,\r\n" - ret_str += f"st0001,REBEL YELL,10104,1467999790,4096483201,Cs1000,-1,chaset,\r\n" + ret_str += ( + f"st0000,RISING PURPLE,10104,1464370990,4096483201,Cs1000,-1,purple,\r\n" + ) + ret_str += ( + f"st0001,REBEL YELL,10104,1467999790,4096483201,Cs1000,-1,chaset,\r\n" + ) ret_str += f"st0002,REMNANT,10104,1502127790,4096483201,Cs1000,-1,overcl,\r\n" - return({"data": ret_str}) - + return {"data": ret_str} + 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}) + for i in range(1, 11): + ret_str += f"{i},st000{story_num}_{i-1},,,,,,,,,,,,,,,,1,,-1,1,\r\n" + return {"data": ret_str} 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"}) + return {"data": "Cs1032,1,1,1,1,1,1,1,1,1,1,\r\n"} def handle_data_premium_list_request(self, data: Dict) -> Dict: - return({"data": "1,,,,10,,,,,99,,,,,,,,,100,,\r\n"}) + return {"data": "1,,,,10,,,,,99,,,,,,,,,100,,\r\n"} def handle_data_event_list_request(self, data: Dict) -> Dict: - return({"data":""}) + return {"data": ""} 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"}) + return {"data": "1,2,1,1,2,3,9,5,6,7,8,9,10,\r\n"} elif "EventStampList" in event_id: - return({"data":"Cs1002,1,1,1,1,1,1,1,1,1,1,\r\n"}) + return {"data": "Cs1002,1,1,1,1,1,1,1,1,1,1,\r\n"} else: - return({"data":""}) - + return {"data": ""} + 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"}) + return {"data": "1,2,1,1,2,3,9,5,6,7,8,9,10,\r\n"} else: - return({"data":""}) + return {"data": ""} diff --git a/titles/cxb/rss2.py b/titles/cxb/rss2.py index 5ae98f4..e32e762 100644 --- a/titles/cxb/rss2.py +++ b/titles/cxb/rss2.py @@ -11,128 +11,147 @@ from titles.cxb.config import CxbConfig from titles.cxb.base import CxbBase from titles.cxb.const import CxbConstants + class CxbRevSunriseS2(CxbBase): def __init__(self, cfg: CoreConfig, game_cfg: CxbConfig) -> None: super().__init__(cfg, game_cfg) self.version = CxbConstants.VER_CROSSBEATS_REV_SUNRISE_S2_OMNI - - def handle_data_path_list_request(self, data: Dict) -> Dict: - return { "data": "" } - @cached(lifetime=86400) + def handle_data_path_list_request(self, data: Dict) -> Dict: + return {"data": ""} + + @cached(lifetime=86400) def handle_data_music_list_request(self, data: Dict) -> Dict: ret_str = "" with open(r"titles/cxb/rss2_data/MusicArchiveList.csv") as music: lines = music.readlines() for line in lines: - line_split = line.split(',') + line_split = line.split(",") ret_str += f"{line_split[0]},{line_split[1]},{line_split[2]},{line_split[3]},{line_split[4]},{line_split[5]},{line_split[6]},{line_split[7]},{line_split[8]},{line_split[9]},{line_split[10]},{line_split[11]},{line_split[12]},{line_split[13]},{line_split[14]},\r\n" - - return({"data":ret_str}) - @cached(lifetime=86400) + return {"data": ret_str} + + @cached(lifetime=86400) def handle_data_item_list_detail_request(self, data: Dict) -> Dict: - #ItemListIcon load + # ItemListIcon load ret_str = "#ItemListIcon\r\n" - with open(r"titles/cxb/rss2_data/Item/ItemList_Icon.csv", encoding="utf-8") as item: + with open( + r"titles/cxb/rss2_data/Item/ItemList_Icon.csv", encoding="utf-8" + ) as item: lines = item.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" - - #ItemListTitle load + + # ItemListTitle load ret_str += "\r\n#ItemListTitle\r\n" - with open(r"titles/cxb/rss2_data/Item/ItemList_Title.csv", encoding="utf-8") as item: + with open( + r"titles/cxb/rss2_data/Item/ItemList_Title.csv", encoding="utf-8" + ) as item: lines = item.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" - return({"data":ret_str}) + return {"data": ret_str} - @cached(lifetime=86400) + @cached(lifetime=86400) def handle_data_shop_list_detail_request(self, data: Dict) -> Dict: - #ShopListIcon load + # ShopListIcon load ret_str = "#ShopListIcon\r\n" - with open(r"titles/cxb/rss2_data/Shop/ShopList_Icon.csv", encoding="utf-8") as shop: + with open( + r"titles/cxb/rss2_data/Shop/ShopList_Icon.csv", encoding="utf-8" + ) as shop: lines = shop.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" - - #ShopListMusic load - ret_str += "\r\n#ShopListMusic\r\n" - with open(r"titles/cxb/rss2_data/Shop/ShopList_Music.csv", encoding="utf-8") as shop: - lines = shop.readlines() - 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/rss2_data/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/rss2_data/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/rss2_data/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/rss2_data/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(r"titles/cxb/rss2_data/Shop/ShopList_Title.csv", encoding="utf-8") as shop: - lines = shop.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":""}) - - def handle_data_ex0001_request(self, data: Dict) -> Dict: - return({"data":""}) - - def handle_data_one_more_extra_list_request(self, data: Dict) -> Dict: - return({"data":""}) - - def handle_data_bonus_list10100_request(self, data: Dict) -> Dict: - return({"data":""}) - - def handle_data_oe0001_request(self, data: Dict) -> Dict: - return({"data":""}) - - def handle_data_free_coupon_request(self, data: Dict) -> Dict: - return({"data":""}) - @cached(lifetime=86400) + # ShopListMusic load + ret_str += "\r\n#ShopListMusic\r\n" + with open( + r"titles/cxb/rss2_data/Shop/ShopList_Music.csv", encoding="utf-8" + ) as shop: + lines = shop.readlines() + 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/rss2_data/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/rss2_data/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/rss2_data/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/rss2_data/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( + r"titles/cxb/rss2_data/Shop/ShopList_Title.csv", encoding="utf-8" + ) as shop: + lines = shop.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": ""} + + def handle_data_ex0001_request(self, data: Dict) -> Dict: + return {"data": ""} + + def handle_data_one_more_extra_list_request(self, data: Dict) -> Dict: + return {"data": ""} + + def handle_data_bonus_list10100_request(self, data: Dict) -> Dict: + return {"data": ""} + + def handle_data_oe0001_request(self, data: Dict) -> Dict: + return {"data": ""} + + def handle_data_free_coupon_request(self, data: Dict) -> Dict: + return {"data": ""} + + @cached(lifetime=86400) def handle_data_news_list_request(self, data: Dict) -> Dict: ret_str = "" with open(r"titles/cxb/rss2_data/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}) + return {"data": ret_str} def handle_data_tips_request(self, data: Dict) -> Dict: - return({"data":""}) - + return {"data": ""} + def handle_data_release_info_list_request(self, data: Dict) -> Dict: - return({"data":""}) - + return {"data": ""} + @cached(lifetime=86400) def handle_data_random_music_list_request(self, data: Dict) -> Dict: ret_str = "" @@ -141,10 +160,12 @@ class CxbRevSunriseS2(CxbBase): count = 0 for line in lines: line_split = line.split(",") - ret_str += str(count) + "," + line_split[0] + "," + line_split[0] + ",\r\n" + ret_str += ( + str(count) + "," + line_split[0] + "," + line_split[0] + ",\r\n" + ) + + return {"data": ret_str} - return({"data":ret_str}) - @cached(lifetime=86400) def handle_data_license_request(self, data: Dict) -> Dict: ret_str = "" @@ -152,54 +173,58 @@ class CxbRevSunriseS2(CxbBase): lines = licenses.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" - return({"data":ret_str}) - + return {"data": ret_str} + @cached(lifetime=86400) def handle_data_course_list_request(self, data: Dict) -> Dict: ret_str = "" - with open(r"titles/cxb/rss2_data/Course/CourseList.csv", encoding="UTF-8") as course: + with open( + r"titles/cxb/rss2_data/Course/CourseList.csv", encoding="UTF-8" + ) as course: lines = course.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" - return({"data":ret_str}) + return {"data": ret_str} @cached(lifetime=86400) def handle_data_csxxxx_request(self, data: Dict) -> Dict: extra_num = int(data["dldate"]["filetype"][-4:]) ret_str = "" - with open(fr"titles/cxb/rss2_data/Course/Cs{extra_num}.csv", encoding="shift-jis") as course: + with open( + rf"titles/cxb/rss2_data/Course/Cs{extra_num}.csv", encoding="shift-jis" + ) as course: lines = course.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" - return({"data":ret_str}) - + return {"data": ret_str} + def handle_data_mission_list_request(self, data: Dict) -> Dict: - return({"data":""}) - + return {"data": ""} + def handle_data_mission_bonus_request(self, data: Dict) -> Dict: - return({"data":""}) - + return {"data": ""} + def handle_data_unlimited_mission_request(self, data: Dict) -> Dict: - return({"data":""}) - + return {"data": ""} + 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): + for i in range(0, 10): ret_str += f"80000{i},{i},{i},0,10000,,\r\n" ret_str += f"80000{i},{i},{i},1,10500,,\r\n" ret_str += f"80000{i},{i},{i},2,10500,,\r\n" - for i in range(10,13): + for i in range(10, 13): ret_str += f"8000{i},{i},{i},0,10000,,\r\n" ret_str += f"8000{i},{i},{i},1,10500,,\r\n" ret_str += f"8000{i},{i},{i},2,10500,,\r\n" - ret_str +="\r\n---\r\n0,150,100,100,100,100,\r\n" - for i in range(1,130): - ret_str +=f"{i},100,100,100,100,100,\r\n" - + ret_str += "\r\n---\r\n0,150,100,100,100,100,\r\n" + for i in range(1, 130): + ret_str += f"{i},100,100,100,100,100,\r\n" + ret_str += "---\r\n" - return({"data": ret_str}) - + return {"data": ret_str} + @cached(lifetime=86400) def handle_data_partnerxxxx_request(self, data: Dict) -> Dict: partner_num = int(data["dldate"]["filetype"][-4:]) @@ -208,55 +233,65 @@ class CxbRevSunriseS2(CxbBase): lines = partner.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" - return({"data": ret_str}) - + return {"data": ret_str} + def handle_data_server_state_request(self, data: Dict) -> Dict: - return({"data": True}) - + return {"data": True} + def handle_data_settings_request(self, data: Dict) -> Dict: - return({"data": "2,\r\n"}) + return {"data": "2,\r\n"} 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 + # story id, story name, game version, start time, end time, course arc, unlock flag, song mcode for menu ret_str = "\r\n" - ret_str += f"st0000,RISING PURPLE,10104,1464370990,4096483201,Cs1000,-1,purple,\r\n" - ret_str += f"st0001,REBEL YELL,10104,1467999790,4096483201,Cs1000,-1,chaset,\r\n" + ret_str += ( + f"st0000,RISING PURPLE,10104,1464370990,4096483201,Cs1000,-1,purple,\r\n" + ) + ret_str += ( + f"st0001,REBEL YELL,10104,1467999790,4096483201,Cs1000,-1,chaset,\r\n" + ) ret_str += f"st0002,REMNANT,10104,1502127790,4096483201,Cs1000,-1,overcl,\r\n" - return({"data": ret_str}) - + return {"data": ret_str} + 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 - for i in range(1,11): - ret_str +=f"{i},st000{story_num}_{i-1},,,,,,,,,,,,,,,,1,,-1,1,\r\n" - return({"data": 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} 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"}) + return {"data": "Cs1002,1,1,1,1,1,1,1,1,1,1,\r\n"} def handle_data_premium_list_request(self, data: Dict) -> Dict: - return({"data": "1,,,,10,,,,,99,,,,,,,,,100,,\r\n"}) + return {"data": "1,,,,10,,,,,99,,,,,,,,,100,,\r\n"} 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"}) + return { + "data": "Cs4001,0,10000,1601510400,1604188799,1,nv2006,1,\r\nCs4005,0,10000,1609459200,1615766399,1,nv2006,1,\r\n" + } def handle_data_event_detail_list_request(self, data: Dict) -> Dict: event_id = data["dldate"]["filetype"].split("/")[2] if "Cs4001" in event_id: - return({"data":"#EventMusicList\r\n1,zonzon2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,\r\n2,moonki,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,\r\n3,tricko,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,\r\n"}) + return { + "data": "#EventMusicList\r\n1,zonzon2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,\r\n2,moonki,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,\r\n3,tricko,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,\r\n" + } elif "Cs4005" in event_id: - return({"data":"#EventMusicList\r\n2,firstl,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,\r\n2,valent,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,\r\n2,dazzli2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,\r\n"}) + return { + "data": "#EventMusicList\r\n2,firstl,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,\r\n2,valent,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,\r\n2,dazzli2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,\r\n" + } elif "EventStampMapListCs1002" in event_id: - return({"data":"1,2,1,1,2,3,9,5,6,7,8,9,10,\r\n"}) + return {"data": "1,2,1,1,2,3,9,5,6,7,8,9,10,\r\n"} elif "EventStampList" in event_id: - return({"data":"Cs1002,1,1,1,1,1,1,1,1,1,1,\r\n"}) + return {"data": "Cs1002,1,1,1,1,1,1,1,1,1,1,\r\n"} else: - return({"data":""}) - + return {"data": ""} + 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"}) + return {"data": "1,2,1,1,2,3,9,5,6,7,8,9,10,\r\n"} else: - return({"data":""}) + return {"data": ""} diff --git a/titles/cxb/schema/item.py b/titles/cxb/schema/item.py index 80d8427..022a036 100644 --- a/titles/cxb/schema/item.py +++ b/titles/cxb/schema/item.py @@ -14,32 +14,29 @@ energy = Table( Column("user", ForeignKey("aime_user.id", ondelete="cascade"), nullable=False), Column("energy", Integer, nullable=False, server_default="0"), UniqueConstraint("user", name="cxb_rev_energy_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) -class CxbItemData(BaseData): - 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 - ) +class CxbItemData(BaseData): + 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 = self.execute(conflict) if result is None: - self.logger.error(f"{__name__} failed to insert item! user: {user_id}, energy: {rev_energy}") + self.logger.error( + f"{__name__} failed to insert item! user: {user_id}, energy: {rev_energy}" + ) return None - + return result.lastrowid - + def get_energy(self, user_id: int) -> Optional[Dict]: - sql = energy.select( - and_(energy.c.user == user_id) - ) + sql = energy.select(and_(energy.c.user == user_id)) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() diff --git a/titles/cxb/schema/profile.py b/titles/cxb/schema/profile.py index 1f731b4..5c62f76 100644 --- a/titles/cxb/schema/profile.py +++ b/titles/cxb/schema/profile.py @@ -16,57 +16,63 @@ profile = Table( Column("index", Integer, nullable=False), Column("data", JSON, nullable=False), UniqueConstraint("user", "index", name="cxb_profile_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) + class CxbProfileData(BaseData): - def put_profile(self, user_id: int, version: int, index: int, data: JSON) -> Optional[int]: + def put_profile( + self, user_id: int, version: int, index: int, data: JSON + ) -> Optional[int]: sql = insert(profile).values( - user = user_id, - version = version, - index = index, - data = data + user=user_id, version=version, index=index, data=data ) - conflict = sql.on_duplicate_key_update( - index = index, - data = data - ) + conflict = sql.on_duplicate_key_update(index=index, data=data) result = self.execute(conflict) if result is None: - self.logger.error(f"{__name__} failed to update! user: {user_id}, index: {index}, data: {data}") + self.logger.error( + f"{__name__} failed to update! user: {user_id}, index: {index}, data: {data}" + ) return None - + return result.lastrowid 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 """ - sql = profile.select(and_( - profile.c.version == version, - profile.c.user == aime_id - )) - + sql = profile.select( + and_(profile.c.version == version, profile.c.user == aime_id) + ) + result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - def get_profile_index(self, index: int, aime_id: int = None, version: int = None) -> Optional[Dict]: + def get_profile_index( + self, index: int, aime_id: int = None, version: int = None + ) -> Optional[Dict]: """ Given a game version and either a profile or aime id, return the profile """ if aime_id is not None and version is not None and index is not None: - sql = profile.select(and_( + sql = profile.select( + and_( profile.c.version == version, profile.c.user == aime_id, - profile.c.index == index - )) + profile.c.index == index, + ) + ) else: - self.logger.error(f"get_profile: Bad arguments!! aime_id {aime_id} version {version}") + self.logger.error( + f"get_profile: Bad arguments!! aime_id {aime_id} version {version}" + ) return None - + result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() diff --git a/titles/cxb/schema/score.py b/titles/cxb/schema/score.py index 014e535..b6f4f16 100644 --- a/titles/cxb/schema/score.py +++ b/titles/cxb/schema/score.py @@ -18,7 +18,7 @@ score = Table( Column("song_index", Integer), Column("data", JSON), UniqueConstraint("user", "song_mcode", "song_index", name="cxb_score_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) playlog = Table( @@ -40,7 +40,7 @@ playlog = Table( Column("fail", Integer), Column("combo", Integer), Column("date_scored", TIMESTAMP, server_default=func.now()), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) ranking = Table( @@ -53,11 +53,19 @@ ranking = Table( Column("score", Integer), Column("clear", Integer), UniqueConstraint("user", "rev_id", name="cxb_ranking_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) + class CxbScoreData(BaseData): - def put_best_score(self, user_id: int, song_mcode: str, game_version: int, song_index: int, data: JSON) -> Optional[int]: + def put_best_score( + self, + user_id: int, + song_mcode: str, + game_version: int, + song_index: int, + data: JSON, + ) -> Optional[int]: """ Update the user's best score for a chart """ @@ -66,22 +74,37 @@ class CxbScoreData(BaseData): song_mcode=song_mcode, game_version=game_version, song_index=song_index, - data=data + data=data, ) - conflict = sql.on_duplicate_key_update( - data = sql.inserted.data - ) + conflict = sql.on_duplicate_key_update(data=sql.inserted.data) 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}") + self.logger.error( + f"{__name__} failed to insert best score! profile: {user_id}, song: {song_mcode}, data: {data}" + ) return None - + return result.lastrowid - - def put_playlog(self, user_id: int, song_mcode: str, chart_id: int, score: int, clear: int, flawless: int, this_super: int, - cool: int, this_fast: int, this_fast2: int, this_slow: int, this_slow2: int, fail: int, combo: int) -> Optional[int]: + + def put_playlog( + self, + user_id: int, + song_mcode: str, + chart_id: int, + score: int, + clear: int, + flawless: int, + this_super: int, + cool: int, + this_fast: int, + this_fast2: int, + this_slow: int, + this_slow2: int, + fail: int, + combo: int, + ) -> Optional[int]: """ Add an entry to the user's play log """ @@ -99,45 +122,42 @@ class CxbScoreData(BaseData): slow=this_slow, slow2=this_slow2, fail=fail, - combo=combo + combo=combo, ) 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}") + self.logger.error( + f"{__name__} failed to insert playlog! profile: {user_id}, song: {song_mcode}, chart: {chart_id}" + ) return None - + return result.lastrowid - def put_ranking(self, user_id: int, rev_id: int, song_id: int, score: int, clear: int) -> Optional[int]: + def put_ranking( + self, user_id: int, rev_id: int, song_id: int, score: int, clear: int + ) -> Optional[int]: """ Add an entry to the user's ranking logs """ if song_id == 0: sql = insert(ranking).values( - user=user_id, - rev_id=rev_id, - score=score, - clear=clear + user=user_id, rev_id=rev_id, score=score, clear=clear ) else: sql = insert(ranking).values( - user=user_id, - rev_id=rev_id, - song_id=song_id, - score=score, - clear=clear + user=user_id, rev_id=rev_id, song_id=song_id, score=score, clear=clear ) - - conflict = sql.on_duplicate_key_update( - score = score - ) + + conflict = sql.on_duplicate_key_update(score=score) 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}") + self.logger.error( + f"{__name__} failed to insert ranking log! profile: {user_id}, score: {score}, clear: {clear}" + ) return None - + return result.lastrowid def get_best_score(self, user_id: int, song_mcode: int) -> Optional[Dict]: @@ -146,21 +166,22 @@ class CxbScoreData(BaseData): ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() def get_best_scores(self, user_id: int) -> Optional[Dict]: sql = score.select(score.c.user == user_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() def get_best_rankings(self, user_id: int) -> Optional[List[Dict]]: - sql = ranking.select( - ranking.c.user == user_id - ) + sql = ranking.select(ranking.c.user == user_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() diff --git a/titles/cxb/schema/static.py b/titles/cxb/schema/static.py index 6b16ac4..6459e99 100644 --- a/titles/cxb/schema/static.py +++ b/titles/cxb/schema/static.py @@ -21,54 +21,75 @@ music = Table( Column("artist", String(255)), Column("category", String(255)), Column("level", Float), - UniqueConstraint("version", "songId", "chartId", "index", name="cxb_static_music_uk"), - mysql_charset='utf8mb4' + UniqueConstraint( + "version", "songId", "chartId", "index", name="cxb_static_music_uk" + ), + mysql_charset="utf8mb4", ) + class CxbStaticData(BaseData): - def put_music(self, version: int, mcode: str, index: int, chart: int, title: str, artist: str, category: str, level: float ) -> Optional[int]: + def put_music( + self, + version: int, + mcode: str, + index: int, + chart: int, + title: str, + artist: str, + category: str, + level: float, + ) -> Optional[int]: sql = insert(music).values( - version = version, - songId = mcode, - index = index, - chartId = chart, - title = title, - artist = artist, - category = category, - level = level + version=version, + songId=mcode, + index=index, + chartId=chart, + title=title, + artist=artist, + category=category, + level=level, ) conflict = sql.on_duplicate_key_update( - title = title, - artist = artist, - category = category, - level = level + title=title, artist=artist, category=category, level=level ) result = self.execute(conflict) - if result is None: return None + if result is None: + return None return result.lastrowid - - def get_music(self, version: int, song_id: Optional[int] = None) -> Optional[List[Row]]: + + def get_music( + self, version: int, song_id: Optional[int] = None + ) -> Optional[List[Row]]: if song_id is None: sql = select(music).where(music.c.version == version) else: - sql = select(music).where(and_( - music.c.version == version, - music.c.songId == song_id, - )) + sql = select(music).where( + and_( + music.c.version == version, + music.c.songId == song_id, + ) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - - def get_music_chart(self, version: int, song_id: int, chart_id: int) -> Optional[List[Row]]: - sql = select(music).where(and_( - music.c.version == version, - music.c.songId == song_id, - music.c.chartId == chart_id - )) + + def get_music_chart( + self, version: int, song_id: int, chart_id: int + ) -> Optional[List[Row]]: + sql = select(music).where( + and_( + music.c.version == version, + music.c.songId == song_id, + music.c.chartId == chart_id, + ) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() diff --git a/titles/diva/__init__.py b/titles/diva/__init__.py index e14aee2..9d93468 100644 --- a/titles/diva/__init__.py +++ b/titles/diva/__init__.py @@ -7,4 +7,4 @@ index = DivaServlet database = DivaData reader = DivaReader game_codes = [DivaConstants.GAME_CODE] -current_schema_version = 1 \ No newline at end of file +current_schema_version = 1 diff --git a/titles/diva/base.py b/titles/diva/base.py index 2e788af..9e58269 100644 --- a/titles/diva/base.py +++ b/titles/diva/base.py @@ -1,6 +1,6 @@ import datetime from typing import Any, List, Dict -import logging +import logging import json import urllib @@ -9,34 +9,35 @@ from titles.diva.config import DivaConfig from titles.diva.const import DivaConstants from titles.diva.database import DivaData -class DivaBase(): + +class DivaBase: def __init__(self, cfg: CoreConfig, game_cfg: DivaConfig) -> None: - self.core_cfg = cfg # Config file + self.core_cfg = cfg # Config file self.game_config = game_cfg - self.data = DivaData(cfg) # Database + self.data = DivaData(cfg) # Database self.date_time_format = "%Y-%m-%d %H:%M:%S" self.logger = logging.getLogger("diva") self.game = DivaConstants.GAME_CODE self.version = DivaConstants.VER_PROJECT_DIVA_ARCADE_FUTURE_TONE dt = datetime.datetime.now() - self.time_lut=urllib.parse.quote(dt.strftime("%Y-%m-%d %H:%M:%S:16.0")) - + self.time_lut = urllib.parse.quote(dt.strftime("%Y-%m-%d %H:%M:%S:16.0")) + def handle_test_request(self, data: Dict) -> Dict: return "" def handle_game_init_request(self, data: Dict) -> Dict: - return ( f'' ) + return f"" 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', - 'atnd_prm2': '30,10,100,4,1,50,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,1,1,1,1,1,1,1,1', - 'atnd_prm3': '100,0,1,1,1,1,1,1,1,1,2,3,4,1,1,1,3,4,5,1,1,1,4,5,6,1,1,1,5,6,7,4,4,4,9,10,14,5,10,10,25,20,50,30,90,5,10,10,25,20,50,30,90,5,10,10,25,20,50,30,90,5,10,10,25,20,50,30,90,5,10,10,25,20,50,30,90,10,30,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0', - 'atnd_lut': f'{self.time_lut}', + "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", + "atnd_prm2": "30,10,100,4,1,50,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,1,1,1,1,1,1,1,1", + "atnd_prm3": "100,0,1,1,1,1,1,1,1,1,2,3,4,1,1,1,3,4,5,1,1,1,4,5,6,1,1,1,5,6,7,4,4,4,9,10,14,5,10,10,25,20,50,30,90,5,10,10,25,20,50,30,90,5,10,10,25,20,50,30,90,5,10,10,25,20,50,30,90,5,10,10,25,20,50,30,90,10,30,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0", + "atnd_lut": f"{self.time_lut}", } - + encoded += urllib.parse.urlencode(params) encoded = encoded.replace("%2C", ",") @@ -45,42 +46,42 @@ class DivaBase(): def handle_ping_request(self, data: Dict) -> Dict: encoded = "&" params = { - 'ping_b_msg': f'Welcome to {self.core_cfg.server.name} network!', - 'ping_m_msg': 'xxx', - 'atnd_lut': f'{self.time_lut}', - 'fi_lut': f'{self.time_lut}', - 'ci_lut': f'{self.time_lut}', - 'qi_lut': f'{self.time_lut}', - 'pvl_lut': '2021-05-22 12:08:16.0', - 'shp_ctlg_lut': '2020-06-10 19:44:16.0', - 'cstmz_itm_ctlg_lut': '2019-10-08 20:23:12.0', - 'ngwl_lut': '2019-10-08 20:23:12.0', - 'rnk_nv_lut': '2020-06-10 19:51:30.0', - 'rnk_ps_lut': f'{self.time_lut}', - 'bi_lut': '2020-09-18 10:00:00.0', - 'cpi_lut': '2020-10-25 09:25:10.0', - 'bdlol_lut': '2020-09-18 10:00:00.0', - 'p_std_hc_lut': '2019-08-01 04:00:36.0', - 'p_std_i_n_lut': '2019-08-01 04:00:36.0', - 'pdcl_lut': '2019-08-01 04:00:36.0', - 'pnml_lut': '2019-08-01 04:00:36.0', - 'cinml_lut': '2019-08-01 04:00:36.0', - 'rwl_lut': '2019-08-01 04:00:36.0', - 'req_inv_cmd_num': '-1,-1,-1,-1,-1,-1,-1,-1,-1,-1', - 'req_inv_cmd_prm1': '-1,-1,-1,-1,-1,-1,-1,-1,-1,-1', - 'req_inv_cmd_prm2': '-1,-1,-1,-1,-1,-1,-1,-1,-1,-1', - 'req_inv_cmd_prm3': '-1,-1,-1,-1,-1,-1,-1,-1,-1,-1', - 'req_inv_cmd_prm4': '-1,-1,-1,-1,-1,-1,-1,-1,-1,-1', - 'pow_save_flg': 0, - 'nblss_dnt_p': 100, - 'nblss_ltt_rl_vp': 1500, - 'nblss_ex_ltt_flg': 1, - 'nblss_dnt_st_tm': "2019-07-15 12:00:00.0", - 'nblss_dnt_ed_tm': "2019-09-17 12:00:00.0", - 'nblss_ltt_st_tm': "2019-09-18 12:00:00.0", - 'nblss_ltt_ed_tm': "2019-09-22 12:00:00.0", + "ping_b_msg": f"Welcome to {self.core_cfg.server.name} network!", + "ping_m_msg": "xxx", + "atnd_lut": f"{self.time_lut}", + "fi_lut": f"{self.time_lut}", + "ci_lut": f"{self.time_lut}", + "qi_lut": f"{self.time_lut}", + "pvl_lut": "2021-05-22 12:08:16.0", + "shp_ctlg_lut": "2020-06-10 19:44:16.0", + "cstmz_itm_ctlg_lut": "2019-10-08 20:23:12.0", + "ngwl_lut": "2019-10-08 20:23:12.0", + "rnk_nv_lut": "2020-06-10 19:51:30.0", + "rnk_ps_lut": f"{self.time_lut}", + "bi_lut": "2020-09-18 10:00:00.0", + "cpi_lut": "2020-10-25 09:25:10.0", + "bdlol_lut": "2020-09-18 10:00:00.0", + "p_std_hc_lut": "2019-08-01 04:00:36.0", + "p_std_i_n_lut": "2019-08-01 04:00:36.0", + "pdcl_lut": "2019-08-01 04:00:36.0", + "pnml_lut": "2019-08-01 04:00:36.0", + "cinml_lut": "2019-08-01 04:00:36.0", + "rwl_lut": "2019-08-01 04:00:36.0", + "req_inv_cmd_num": "-1,-1,-1,-1,-1,-1,-1,-1,-1,-1", + "req_inv_cmd_prm1": "-1,-1,-1,-1,-1,-1,-1,-1,-1,-1", + "req_inv_cmd_prm2": "-1,-1,-1,-1,-1,-1,-1,-1,-1,-1", + "req_inv_cmd_prm3": "-1,-1,-1,-1,-1,-1,-1,-1,-1,-1", + "req_inv_cmd_prm4": "-1,-1,-1,-1,-1,-1,-1,-1,-1,-1", + "pow_save_flg": 0, + "nblss_dnt_p": 100, + "nblss_ltt_rl_vp": 1500, + "nblss_ex_ltt_flg": 1, + "nblss_dnt_st_tm": "2019-07-15 12:00:00.0", + "nblss_dnt_ed_tm": "2019-09-17 12:00:00.0", + "nblss_ltt_st_tm": "2019-09-18 12:00:00.0", + "nblss_ltt_ed_tm": "2019-09-22 12:00:00.0", } - + encoded += urllib.parse.urlencode(params) encoded = encoded.replace("+", "%20") encoded = encoded.replace("%2C", ",") @@ -122,7 +123,7 @@ class DivaBase(): response += f"&pvl_lut={self.time_lut}" response += f"&pv_lst={pvlist}" - return ( response ) + return response def handle_shop_catalog_request(self, data: Dict) -> Dict: catalog = "" @@ -137,7 +138,21 @@ class DivaBase(): else: for shop in shopList: - line = str(shop["shopId"]) + "," + str(shop['unknown_0']) + "," + shop['name'] + "," + str(shop['points']) + "," + shop['start_date'] + "," + shop['end_date'] + "," + str(shop["type"]) + line = ( + str(shop["shopId"]) + + "," + + str(shop["unknown_0"]) + + "," + + shop["name"] + + "," + + str(shop["points"]) + + "," + + shop["start_date"] + + "," + + shop["end_date"] + + "," + + str(shop["type"]) + ) line = urllib.parse.quote(line) + "," catalog += f"{urllib.parse.quote(line)}" @@ -146,7 +161,7 @@ class DivaBase(): response = f"&shp_ctlg_lut={self.time_lut}" response += f"&shp_ctlg={catalog[:-3]}" - return ( response ) + return response def handle_buy_module_request(self, data: Dict) -> Dict: profile = self.data.profile.get_profile(data["pd_id"], self.version) @@ -162,10 +177,7 @@ class DivaBase(): new_vcld_pts = profile["vcld_pts"] - int(data["mdl_price"]) - self.data.profile.update_profile( - profile["user"], - vcld_pts=new_vcld_pts - ) + 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 @@ -191,7 +203,21 @@ class DivaBase(): else: for item in itemList: - line = str(item["itemId"]) + "," + str(item['unknown_0']) + "," + item['name'] + "," + str(item['points']) + "," + item['start_date'] + "," + item['end_date'] + "," + str(item["type"]) + line = ( + str(item["itemId"]) + + "," + + str(item["unknown_0"]) + + "," + + item["name"] + + "," + + str(item["points"]) + + "," + + item["start_date"] + + "," + + item["end_date"] + + "," + + str(item["type"]) + ) line = urllib.parse.quote(line) + "," catalog += f"{urllib.parse.quote(line)}" @@ -200,11 +226,13 @@ class DivaBase(): response = f"&cstmz_itm_ctlg_lut={self.time_lut}" response += f"&cstmz_itm_ctlg={catalog[:-3]}" - return ( response ) + return response 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"])) + item = self.data.static.get_enabled_item( + self.version, int(data["cstmz_itm_id"]) + ) # make sure module is available to purchase if not item: @@ -217,15 +245,16 @@ class DivaBase(): new_vcld_pts = profile["vcld_pts"] - int(data["cstmz_itm_price"]) # save new Vocaloid Points balance - self.data.profile.update_profile( - profile["user"], - vcld_pts=new_vcld_pts + self.data.profile.update_profile(profile["user"], vcld_pts=new_vcld_pts) + + self.data.customize.put_customize_item( + data["pd_id"], self.version, data["cstmz_itm_id"] ) - self.data.customize.put_customize_item(data["pd_id"], self.version, data["cstmz_itm_id"]) - # generate the cstmz_itm_have string - cstmz_itm_have = self.data.customize.get_customize_items_have_string(data["pd_id"], self.version) + cstmz_itm_have = self.data.customize.get_customize_items_have_string( + data["pd_id"], self.version + ) response = "&shp_rslt=1" response += f"&cstmz_itm_id={data['cstmz_itm_id']}" @@ -237,33 +266,33 @@ class DivaBase(): def handle_festa_info_request(self, data: Dict) -> Dict: encoded = "&" params = { - 'fi_id': '1,-1', - 'fi_name': f'{self.core_cfg.server.name} Opening,xxx', - 'fi_kind': '0,0', - 'fi_difficulty': '-1,-1', - 'fi_pv_id_lst': 'ALL,ALL', - 'fi_attr': '7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', - 'fi_add_vp': '20,0', - 'fi_mul_vp': '1,1', - 'fi_st': '2022-06-17 17:00:00.0,2014-07-08 18:10:11.0', - 'fi_et': '2029-01-01 10:00:00.0,2014-07-08 18:10:11.0', - 'fi_lut': '{self.time_lut}', + "fi_id": "1,-1", + "fi_name": f"{self.core_cfg.server.name} Opening,xxx", + "fi_kind": "0,0", + "fi_difficulty": "-1,-1", + "fi_pv_id_lst": "ALL,ALL", + "fi_attr": "7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + "fi_add_vp": "20,0", + "fi_mul_vp": "1,1", + "fi_st": "2022-06-17 17:00:00.0,2014-07-08 18:10:11.0", + "fi_et": "2029-01-01 10:00:00.0,2014-07-08 18:10:11.0", + "fi_lut": "{self.time_lut}", } - + encoded += urllib.parse.urlencode(params) encoded = encoded.replace("+", "%20") encoded = encoded.replace("%2C", ",") return encoded - + def handle_contest_info_request(self, data: Dict) -> Dict: response = "" response += f"&ci_lut={self.time_lut}" response += "&ci_str=%2A%2A%2A,%2A%2A%2A,%2A%2A%2A,%2A%2A%2A,%2A%2A%2A,%2A%2A%2A,%2A%2A%2A,%2A%2A%2A" - - return ( response ) - + + return response + def handle_qst_inf_request(self, data: Dict) -> Dict: quest = "" @@ -279,11 +308,31 @@ class DivaBase(): response += f"&qhi_str={quest[:-1]}" else: for quests in questList: - line = str(quests["questId"]) + "," + str(quests['quest_order']) + "," + str(quests['kind']) + "," + str(quests['unknown_0']) + "," + quests['start_datetime'] + "," + quests['end_datetime'] + "," + quests["name"] + "," + str(quests["unknown_1"]) + "," + str(quests["unknown_2"]) + "," + str(quests["quest_enable"]) + line = ( + str(quests["questId"]) + + "," + + str(quests["quest_order"]) + + "," + + str(quests["kind"]) + + "," + + str(quests["unknown_0"]) + + "," + + quests["start_datetime"] + + "," + + quests["end_datetime"] + + "," + + quests["name"] + + "," + + str(quests["unknown_1"]) + + "," + + str(quests["unknown_2"]) + + "," + + str(quests["quest_enable"]) + ) quest += f"{urllib.parse.quote(line)}%0A," responseline = f"{quest[:-1]}," - for i in range(len(questList),59): + for i in range(len(questList), 59): responseline += "%2A%2A%2A%0A," response = "" @@ -292,44 +341,44 @@ class DivaBase(): response += "&qrai_str=%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1,%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1,%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1" - return ( response ) - + return response + def handle_nv_ranking_request(self, data: Dict) -> Dict: - return ( f'' ) - + return f"" + def handle_ps_ranking_request(self, data: Dict) -> Dict: - return ( f'' ) - + return f"" + def handle_ng_word_request(self, data: Dict) -> Dict: - return ( f'' ) - + return f"" + def handle_rmt_wp_list_request(self, data: Dict) -> Dict: - return ( f'' ) - + return f"" + def handle_pv_def_chr_list_request(self, data: Dict) -> Dict: - return ( f'' ) - + return f"" + def handle_pv_ng_mdl_list_request(self, data: Dict) -> Dict: - return ( f'' ) - + return f"" + def handle_cstmz_itm_ng_mdl_lst_request(self, data: Dict) -> Dict: - return ( f'' ) - + return f"" + def handle_banner_info_request(self, data: Dict) -> Dict: - return ( f'' ) - + return f"" + def handle_banner_data_request(self, data: Dict) -> Dict: - return ( f'' ) - + return f"" + def handle_cm_ply_info_request(self, data: Dict) -> Dict: - return ( f'' ) - + return f"" + def handle_pstd_h_ctrl_request(self, data: Dict) -> Dict: - return ( f'' ) - + return f"" + def handle_pstd_item_ng_lst_request(self, data: Dict) -> Dict: - return ( f'' ) - + return f"" + 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) @@ -372,8 +421,10 @@ class DivaBase(): return response 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']}") + self.data.profile.create_profile( + self.version, data["aime_id"], data["player_name"] + ) + return f"&cd_adm_result=1&pd_id={data['aime_id']}" def handle_start_request(self, data: Dict) -> Dict: profile = self.data.profile.get_profile(data["pd_id"], self.version) @@ -384,12 +435,16 @@ class DivaBase(): 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 = 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 + ) 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 = self.data.customize.get_customize_items_have_string(data["pd_id"], self.version) + cstmz_itm_have = self.data.customize.get_customize_items_have_string( + data["pd_id"], self.version + ) response = f"&pd_id={data['pd_id']}" response += "&start_result=1" @@ -452,15 +507,16 @@ class DivaBase(): response += f"&mdl_eqp_ary={mdl_eqp_ary}" response += f"&c_itm_eqp_ary={c_itm_eqp_ary}" response += f"&ms_itm_flg_ary={ms_itm_flg_ary}" - - return ( response ) + + return response def handle_pd_unlock_request(self, data: Dict) -> Dict: - return ( f'' ) - + return f"" + 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 + if profile is None: + return response = "" @@ -471,10 +527,16 @@ class DivaBase(): response += f"&lv_efct_id={profile['lv_efct_id']}" response += f"&lv_plt_id={profile['lv_plt_id']}" - return ( response ) + return response - def _get_pv_pd_result(self, song: int, pd_db_song: Dict, pd_db_ranking: Dict, - pd_db_customize: Dict, edition: int) -> str: + def _get_pv_pd_result( + self, + song: int, + pd_db_song: Dict, + pd_db_ranking: Dict, + pd_db_customize: Dict, + edition: int, + ) -> str: """ Helper function to generate the pv_result string for every song, ranking and edition """ @@ -483,7 +545,7 @@ class DivaBase(): # make sure there are enough max scores to calculate a ranking if pd_db_ranking["ranking"] != 0: global_ranking = pd_db_ranking["ranking"] - + # pv_no pv_result = f"{song}," # edition @@ -513,7 +575,7 @@ class DivaBase(): f"{pd_db_customize['chsld_se']}," f"{pd_db_customize['sldtch_se']}" ) - + pv_result += f"{module_eqp}," pv_result += f"{customize_eqp}," pv_result += f"{customize_flag}," @@ -537,21 +599,35 @@ class DivaBase(): 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 = self.data.score.get_best_user_score(data["pd_id"], int(song), data["difficulty"], edition=0) - pd_db_song_1 = self.data.score.get_best_user_score(data["pd_id"], int(song), data["difficulty"], edition=1) - + pd_db_song_0 = self.data.score.get_best_user_score( + data["pd_id"], int(song), data["difficulty"], edition=0 + ) + 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 = self.data.score.get_global_ranking(data["pd_id"], int(song), data["difficulty"], edition=0) + 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 = self.data.score.get_global_ranking(data["pd_id"], int(song), data["difficulty"], edition=1) + pd_db_ranking_1 = self.data.score.get_global_ranking( + data["pd_id"], int(song), data["difficulty"], edition=1 + ) + + pd_db_customize = self.data.pv_customize.get_pv_customize( + data["pd_id"], int(song) + ) - pd_db_customize = self.data.pv_customize.get_pv_customize(data["pd_id"], int(song)) - # generate the pv_result string with the ORIGINAL edition and the EXTRA edition appended - pv_result = self._get_pv_pd_result(int(song), pd_db_song_0, pd_db_ranking_0, pd_db_customize, edition=0) - pv_result += "," + self._get_pv_pd_result(int(song), pd_db_song_1, pd_db_ranking_1, pd_db_customize, edition=1) + pv_result = self._get_pv_pd_result( + int(song), pd_db_song_0, pd_db_ranking_0, pd_db_customize, edition=0 + ) + pv_result += "," + self._get_pv_pd_result( + int(song), pd_db_song_1, pd_db_ranking_1, pd_db_customize, edition=1 + ) self.logger.debug(f"pv_result = {pv_result}") @@ -565,13 +641,12 @@ class DivaBase(): response += "&pdddt_flg=0" response += f"&pdddt_tm={self.time_lut}" - return ( response ) + return response def handle_stage_start_request(self, data: Dict) -> Dict: - return ( f'' ) + return f"" 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(",") @@ -590,15 +665,100 @@ class DivaBase(): for index, value in enumerate(pd_song_list): if "-1" not in pd_song_list[index]: - 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]) + 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: - self.data.score.put_best_score(data["pd_id"], self.version, pd_song_list[index], pd_song_difficulty[index], pd_song_edition[index], pd_song_max_score[index], pd_song_max_atn_pnt[index], pd_song_ranking[index], pd_song_sort_kind, pd_song_cool_cnt[index], pd_song_fine_cnt[index], pd_song_safe_cnt[index], pd_song_sad_cnt[index], pd_song_worst_cnt[index], pd_song_max_combo[index]) - self.data.score.put_playlog(data["pd_id"], self.version, pd_song_list[index], pd_song_difficulty[index], pd_song_edition[index], pd_song_max_score[index], pd_song_max_atn_pnt[index], pd_song_ranking[index], pd_song_sort_kind, pd_song_cool_cnt[index], pd_song_fine_cnt[index], pd_song_safe_cnt[index], pd_song_sad_cnt[index], pd_song_worst_cnt[index], pd_song_max_combo[index]) + self.data.score.put_best_score( + data["pd_id"], + self.version, + pd_song_list[index], + pd_song_difficulty[index], + pd_song_edition[index], + pd_song_max_score[index], + pd_song_max_atn_pnt[index], + pd_song_ranking[index], + pd_song_sort_kind, + pd_song_cool_cnt[index], + pd_song_fine_cnt[index], + pd_song_safe_cnt[index], + pd_song_sad_cnt[index], + pd_song_worst_cnt[index], + pd_song_max_combo[index], + ) + self.data.score.put_playlog( + data["pd_id"], + self.version, + pd_song_list[index], + pd_song_difficulty[index], + pd_song_edition[index], + pd_song_max_score[index], + pd_song_max_atn_pnt[index], + pd_song_ranking[index], + pd_song_sort_kind, + pd_song_cool_cnt[index], + pd_song_fine_cnt[index], + pd_song_safe_cnt[index], + pd_song_sad_cnt[index], + pd_song_worst_cnt[index], + pd_song_max_combo[index], + ) elif int(pd_song_max_score[index]) >= int(profile_pd_db_song["score"]): - self.data.score.put_best_score(data["pd_id"], self.version, pd_song_list[index], pd_song_difficulty[index], pd_song_edition[index], pd_song_max_score[index], pd_song_max_atn_pnt[index], pd_song_ranking[index], pd_song_sort_kind, pd_song_cool_cnt[index], pd_song_fine_cnt[index], pd_song_safe_cnt[index], pd_song_sad_cnt[index], pd_song_worst_cnt[index], pd_song_max_combo[index]) - self.data.score.put_playlog(data["pd_id"], self.version, pd_song_list[index], pd_song_difficulty[index], pd_song_edition[index], pd_song_max_score[index], pd_song_max_atn_pnt[index], pd_song_ranking[index], pd_song_sort_kind, pd_song_cool_cnt[index], pd_song_fine_cnt[index], pd_song_safe_cnt[index], pd_song_sad_cnt[index], pd_song_worst_cnt[index], pd_song_max_combo[index]) + self.data.score.put_best_score( + data["pd_id"], + self.version, + pd_song_list[index], + pd_song_difficulty[index], + pd_song_edition[index], + pd_song_max_score[index], + pd_song_max_atn_pnt[index], + pd_song_ranking[index], + pd_song_sort_kind, + pd_song_cool_cnt[index], + pd_song_fine_cnt[index], + pd_song_safe_cnt[index], + pd_song_sad_cnt[index], + pd_song_worst_cnt[index], + pd_song_max_combo[index], + ) + self.data.score.put_playlog( + data["pd_id"], + self.version, + pd_song_list[index], + pd_song_difficulty[index], + pd_song_edition[index], + pd_song_max_score[index], + pd_song_max_atn_pnt[index], + pd_song_ranking[index], + pd_song_sort_kind, + pd_song_cool_cnt[index], + pd_song_fine_cnt[index], + pd_song_safe_cnt[index], + pd_song_sad_cnt[index], + pd_song_worst_cnt[index], + pd_song_max_combo[index], + ) elif int(pd_song_max_score[index]) != int(profile_pd_db_song["score"]): - self.data.score.put_playlog(data["pd_id"], self.version, pd_song_list[index], pd_song_difficulty[index], pd_song_edition[index], pd_song_max_score[index], pd_song_max_atn_pnt[index], pd_song_ranking[index], pd_song_sort_kind, pd_song_cool_cnt[index], pd_song_fine_cnt[index], pd_song_safe_cnt[index], pd_song_sad_cnt[index], pd_song_worst_cnt[index], pd_song_max_combo[index]) + self.data.score.put_playlog( + data["pd_id"], + self.version, + pd_song_list[index], + pd_song_difficulty[index], + pd_song_edition[index], + pd_song_max_score[index], + pd_song_max_atn_pnt[index], + pd_song_ranking[index], + pd_song_sort_kind, + pd_song_cool_cnt[index], + pd_song_fine_cnt[index], + pd_song_safe_cnt[index], + pd_song_sad_cnt[index], + pd_song_worst_cnt[index], + pd_song_max_combo[index], + ) # Profile saving based on registration list @@ -608,7 +768,7 @@ class DivaBase(): total_atn_pnt = 0 for best_score in best_scores: total_atn_pnt += best_score["atn_pnt"] - + new_level = (total_atn_pnt // 13979) + 1 new_level_pnt = round((total_atn_pnt % 13979) / 13979 * 100) @@ -630,7 +790,7 @@ class DivaBase(): nxt_dffclty=int(data["nxt_dffclty"]), nxt_edtn=int(data["nxt_edtn"]), my_qst_id=data["my_qst_id"], - my_qst_sts=data["my_qst_sts"] + my_qst_sts=data["my_qst_sts"], ) response += f"&lv_num={new_level}" @@ -663,35 +823,51 @@ class DivaBase(): response += "&my_ccd_r_hnd=-1,-1,-1,-1,-1" response += "&my_ccd_r_vp=-1,-1,-1,-1,-1" - return ( response ) + return response def handle_end_request(self, data: Dict) -> Dict: profile = self.data.profile.get_profile(data["pd_id"], self.version) self.data.profile.update_profile( - profile["user"], - my_qst_id=data["my_qst_id"], - my_qst_sts=data["my_qst_sts"] + profile["user"], my_qst_id=data["my_qst_id"], my_qst_sts=data["my_qst_sts"] ) - return (f'') + return f"" def handle_shop_exit_request(self, data: Dict) -> Dict: - self.data.item.put_shop(data["pd_id"], self.version, data["mdl_eqp_cmn_ary"], data["c_itm_eqp_cmn_ary"], data["ms_itm_flg_cmn_ary"]) + self.data.item.put_shop( + data["pd_id"], + self.version, + data["mdl_eqp_cmn_ary"], + data["c_itm_eqp_cmn_ary"], + data["ms_itm_flg_cmn_ary"], + ) if int(data["use_pv_mdl_eqp"]) == 1: - self.data.pv_customize.put_pv_customize(data["pd_id"], self.version, data["ply_pv_id"], - data["mdl_eqp_pv_ary"], data["c_itm_eqp_pv_ary"], data["ms_itm_flg_pv_ary"]) + self.data.pv_customize.put_pv_customize( + data["pd_id"], + self.version, + data["ply_pv_id"], + data["mdl_eqp_pv_ary"], + data["c_itm_eqp_pv_ary"], + data["ms_itm_flg_pv_ary"], + ) else: - self.data.pv_customize.put_pv_customize(data["pd_id"], self.version, data["ply_pv_id"], - "-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") + self.data.pv_customize.put_pv_customize( + data["pd_id"], + self.version, + data["ply_pv_id"], + "-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", + ) response = "&shp_rslt=1" - return ( response ) + return response 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" - + response = "&cd_adm_result=1" response += "&chg_name_price=100" response += "&accept_idx=100" @@ -706,20 +882,18 @@ class DivaBase(): response += f"&passwd_stat={profile['passwd_stat']}" return response - + 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"]): return "&cd_adm_result=0" - + # update the vocaloid points and player name new_vcld_pts = profile["vcld_pts"] - int(data["chg_name_price"]) self.data.profile.update_profile( - profile["user"], - player_name=data["player_name"], - vcld_pts=new_vcld_pts + profile["user"], player_name=data["player_name"], vcld_pts=new_vcld_pts ) response = "&cd_adm_result=1" @@ -728,19 +902,17 @@ class DivaBase(): response += f"&player_name={data['player_name']}" return response - + 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"]): + if data["passwd"] != profile["passwd"]: return "&cd_adm_result=0" # set password to true and update the saved password self.data.profile.update_profile( - profile["user"], - passwd_stat=1, - passwd=data["new_passwd"] + profile["user"], passwd_stat=1, passwd=data["new_passwd"] ) response = "&cd_adm_result=1" diff --git a/titles/diva/config.py b/titles/diva/config.py index af4d626..efa327e 100644 --- a/titles/diva/config.py +++ b/titles/diva/config.py @@ -1,30 +1,40 @@ from core.config import CoreConfig -class DivaServerConfig(): +class DivaServerConfig: def __init__(self, parent_config: "DivaConfig") -> None: self.__config = parent_config @property def enable(self) -> bool: - return CoreConfig.get_config_field(self.__config, 'diva', 'server', 'enable', default=True) + return CoreConfig.get_config_field( + self.__config, "diva", "server", "enable", default=True + ) @property def loglevel(self) -> int: - return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'diva', 'server', 'loglevel', default="info")) + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "diva", "server", "loglevel", default="info" + ) + ) -class DivaModsConfig(): +class DivaModsConfig: def __init__(self, parent_config: "DivaConfig") -> None: self.__config = parent_config @property def unlock_all_modules(self) -> bool: - return CoreConfig.get_config_field(self.__config, 'diva', 'mods', 'unlock_all_modules', default=True) + return CoreConfig.get_config_field( + self.__config, "diva", "mods", "unlock_all_modules", default=True + ) @property def unlock_all_items(self) -> bool: - return CoreConfig.get_config_field(self.__config, 'diva', 'mods', 'unlock_all_items', default=True) + return CoreConfig.get_config_field( + self.__config, "diva", "mods", "unlock_all_items", default=True + ) class DivaConfig(dict): diff --git a/titles/diva/const.py b/titles/diva/const.py index 2ea7024..08597c4 100644 --- a/titles/diva/const.py +++ b/titles/diva/const.py @@ -1,4 +1,4 @@ -class DivaConstants(): +class DivaConstants: GAME_CODE = "SBZV" CONFIG_NAME = "diva.yaml" @@ -10,4 +10,4 @@ class DivaConstants(): @classmethod def game_ver_to_string(cls, ver: int): - return cls.VERSION_NAMES[ver] \ No newline at end of file + return cls.VERSION_NAMES[ver] diff --git a/titles/diva/database.py b/titles/diva/database.py index a7e4193..cf36af9 100644 --- a/titles/diva/database.py +++ b/titles/diva/database.py @@ -1,6 +1,14 @@ from core.data import Data from core.config import CoreConfig -from titles.diva.schema import DivaProfileData, DivaScoreData, DivaModuleData, DivaCustomizeItemData, DivaPvCustomizeData, DivaItemData, DivaStaticData +from titles.diva.schema import ( + DivaProfileData, + DivaScoreData, + DivaModuleData, + DivaCustomizeItemData, + DivaPvCustomizeData, + DivaItemData, + DivaStaticData, +) class DivaData(Data): diff --git a/titles/diva/index.py b/titles/diva/index.py index b049fef..609e640 100644 --- a/titles/diva/index.py +++ b/titles/diva/index.py @@ -14,85 +14,112 @@ from titles.diva.config import DivaConfig from titles.diva.const import DivaConstants from titles.diva.base import DivaBase -class DivaServlet(): + +class DivaServlet: def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: self.core_cfg = core_cfg self.game_cfg = DivaConfig() if path.exists(f"{cfg_dir}/{DivaConstants.CONFIG_NAME}"): - self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{DivaConstants.CONFIG_NAME}"))) + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{DivaConstants.CONFIG_NAME}")) + ) self.base = DivaBase(core_cfg, self.game_cfg) self.logger = logging.getLogger("diva") log_fmt_str = "[%(asctime)s] Diva | %(levelname)s | %(message)s" log_fmt = logging.Formatter(log_fmt_str) - fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.core_cfg.server.log_dir, "diva"), encoding='utf8', - when="d", backupCount=10) + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.core_cfg.server.log_dir, "diva"), + encoding="utf8", + when="d", + backupCount=10, + ) fileHandler.setFormatter(log_fmt) - + consoleHandler = logging.StreamHandler() consoleHandler.setFormatter(log_fmt) self.logger.addHandler(fileHandler) self.logger.addHandler(consoleHandler) - + self.logger.setLevel(self.game_cfg.server.loglevel) - coloredlogs.install(level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str) - + coloredlogs.install( + level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str + ) + @classmethod - def get_allnet_info(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str, str]: + def get_allnet_info( + cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str + ) -> Tuple[bool, str, str]: game_cfg = DivaConfig() if path.exists(f"{cfg_dir}/{DivaConstants.CONFIG_NAME}"): - game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{DivaConstants.CONFIG_NAME}"))) + game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{DivaConstants.CONFIG_NAME}")) + ) if not game_cfg.server.enable: return (False, "", "") - + if core_cfg.server.is_develop: - return (True, f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/", "") - + return ( + True, + f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/", + "", + ) + return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", "") def render_POST(self, req: Request, version: int, url_path: str) -> bytes: req_raw = req.content.getvalue() url_header = req.getAllHeaders() - #Ping Dispatch - if "THIS_STRING_SEPARATES"in str(url_header): + # Ping Dispatch + if "THIS_STRING_SEPARATES" in str(url_header): binary_request = req_raw.splitlines() binary_cmd_decoded = binary_request[3].decode("utf-8") - binary_array = binary_cmd_decoded.split('&') + binary_array = binary_cmd_decoded.split("&") bin_req_data = {} for kvp in binary_array: split_bin = kvp.split("=") bin_req_data[split_bin[0]] = split_bin[1] - + self.logger.info(f"Binary {bin_req_data['cmd']} Request") self.logger.debug(bin_req_data) handler = getattr(self.base, f"handle_{bin_req_data['cmd']}_request") resp = handler(bin_req_data) - self.logger.debug(f"Response 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') + self.logger.debug( + f"Response 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 + json_string = json.dumps( + req_raw.decode("utf-8") + ) # Take the response and decode as UTF-8 and dump + b64string = json_string.replace( + r"\n", "\n" + ) # Remove all \n and separate them as new lines + gz_string = base64.b64decode(b64string) # Decompressing the base64 string - #Main Dispatch - json_string = json.dumps(req_raw.decode("utf-8")) #Take the response and decode as UTF-8 and dump - b64string = json_string.replace(r'\n', '\n') # Remove all \n and separate them as new lines - gz_string = base64.b64decode(b64string) # Decompressing the base64 string - try: - url_data = zlib.decompress( gz_string ).decode("utf-8") # Decompressing the gzip + url_data = zlib.decompress(gz_string).decode( + "utf-8" + ) # Decompressing the gzip except zlib.error as e: self.logger.error(f"Failed to defalte! {e} -> {gz_string}") return "stat=0" req_kvp = urllib.parse.unquote(url_data) req_data = {} - + # We then need to split each parts with & so we can reuse them to fill out the requests splitted_request = str.split(req_kvp, "&") for kvp in splitted_request: @@ -109,15 +136,25 @@ class DivaServlet(): handler = getattr(self.base, func_to_find) resp = handler(req_data) - except AttributeError as e: + except AttributeError as e: self.logger.warning(f"Unhandled {req_data['cmd']} request {e}") - return f"cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok".encode('utf-8') + 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 f"cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok".encode('utf-8') + return f"cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok".encode( + "utf-8" + ) req.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}") + self.logger.debug( + f"Response 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') + 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 c597315..9c409ef 100644 --- a/titles/diva/read.py +++ b/titles/diva/read.py @@ -7,13 +7,23 @@ from core.config import CoreConfig from titles.diva.database import DivaData from titles.diva.const import DivaConstants + class DivaReader(BaseReader): - def __init__(self, config: CoreConfig, version: int, bin_dir: Optional[str], opt_dir: Optional[str], extra: Optional[str]) -> None: + def __init__( + self, + config: CoreConfig, + version: int, + bin_dir: Optional[str], + opt_dir: Optional[str], + extra: Optional[str], + ) -> None: super().__init__(config, version, bin_dir, opt_dir, extra) self.data = DivaData(config) try: - self.logger.info(f"Start importer for {DivaConstants.game_ver_to_string(version)}") + self.logger.info( + f"Start importer for {DivaConstants.game_ver_to_string(version)}" + ) except IndexError: self.logger.error(f"Invalid project diva version {version}") exit(1) @@ -30,7 +40,7 @@ class DivaReader(BaseReader): if not path.exists(f"{self.bin_dir}/rom"): self.logger.warn(f"Couldn't find rom folder in {self.bin_dir}, skipping") pull_bin_rom = False - + if self.opt_dir is not None: opt_dirs = self.get_data_directories(self.opt_dir) else: @@ -44,18 +54,25 @@ class DivaReader(BaseReader): if pull_opt_rom: for dir in opt_dirs: self.read_rom(f"{dir}/rom") - + 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"): for root, dirs, files in walk(f"{ram_root_dir}/databank"): for file in files: - if file.startswith("ShopCatalog_") or file.startswith("CustomizeItemCatalog_") or \ - (file.startswith("QuestInfo") and not file.startswith("QuestInfoTm")): - + if ( + file.startswith("ShopCatalog_") + or file.startswith("CustomizeItemCatalog_") + or ( + file.startswith("QuestInfo") + and not file.startswith("QuestInfoTm") + ) + ): with open(f"{root}/{file}", "r") as f: - file_data: str = urllib.parse.unquote(urllib.parse.unquote(f.read())) + file_data: str = urllib.parse.unquote( + urllib.parse.unquote(f.read()) + ) if file_data == "***": self.logger.info(f"{file} is empty, skipping") continue @@ -70,23 +87,54 @@ class DivaReader(BaseReader): if file.startswith("ShopCatalog_"): for x in range(0, len(split), 7): - self.logger.info(f"Added shop item {split[x+0]}") + self.logger.info( + f"Added shop item {split[x+0]}" + ) - self.data.static.put_shop(self.version, split[x+0], split[x+2], split[x+6], split[x+3], - split[x+1], split[x+4], split[x+5]) + self.data.static.put_shop( + self.version, + split[x + 0], + split[x + 2], + split[x + 6], + split[x + 3], + split[x + 1], + split[x + 4], + split[x + 5], + ) - elif file.startswith("CustomizeItemCatalog_") and len(split) >= 7: + elif ( + file.startswith("CustomizeItemCatalog_") + and len(split) >= 7 + ): for x in range(0, len(split), 7): self.logger.info(f"Added item {split[x+0]}") - self.data.static.put_items(self.version, split[x+0], split[x+2], split[x+6], split[x+3], - split[x+1], split[x+4], split[x+5]) + self.data.static.put_items( + self.version, + split[x + 0], + split[x + 2], + split[x + 6], + split[x + 3], + split[x + 1], + split[x + 4], + split[x + 5], + ) elif file.startswith("QuestInfo") and len(split) >= 9: self.logger.info(f"Added quest {split[0]}") - - self.data.static.put_quests(self.version, split[0], split[6], split[2], split[3], - split[7], split[8], split[1], split[4], split[5]) + + self.data.static.put_quests( + self.version, + split[0], + split[6], + split[2], + split[3], + split[7], + split[8], + split[1], + split[4], + split[5], + ) else: continue @@ -102,13 +150,13 @@ class DivaReader(BaseReader): elif path.exists(f"{rom_root_dir}/pv_db.txt"): file_path = f"{rom_root_dir}/pv_db.txt" else: - self.logger.warn(f"Cannot find pv_db.txt or mdata_pv_db.txt in {rom_root_dir}, skipping") + self.logger.warn( + f"Cannot find pv_db.txt or mdata_pv_db.txt in {rom_root_dir}, skipping" + ) return with open(file_path, "r", encoding="utf-8") as f: - for line in f.readlines(): - if line.startswith("#") or not line: continue @@ -127,14 +175,13 @@ class DivaReader(BaseReader): for x in range(1, len(key_split)): key_args.append(key_split[x]) - + try: pv_list[pv_id] = self.add_branch(pv_list[pv_id], key_args, val) except KeyError: pv_list[pv_id] = {} pv_list[pv_id] = self.add_branch(pv_list[pv_id], key_args, val) - for pv_id, pv_data in pv_list.items(): song_id = int(pv_id.split("_")[1]) if "songinfo" not in pv_data: @@ -148,46 +195,99 @@ class DivaReader(BaseReader): if "music" not in pv_data["songinfo"]: pv_data["songinfo"]["music"] = "-" - if "easy" in pv_data['difficulty'] and '0' in pv_data['difficulty']['easy']: - diff = pv_data['difficulty']['easy']['0']['level'].split('_') + if "easy" in pv_data["difficulty"] and "0" in pv_data["difficulty"]["easy"]: + diff = pv_data["difficulty"]["easy"]["0"]["level"].split("_") self.logger.info(f"Added song {song_id} chart 0") - self.data.static.put_music(self.version, song_id, 0, pv_data["song_name"], pv_data["songinfo"]["arranger"], - pv_data["songinfo"]["illustrator"], pv_data["songinfo"]["lyrics"], pv_data["songinfo"]["music"], - float(f"{diff[2]}.{diff[3]}"), pv_data["bpm"], pv_data["date"]) - - if "normal" in pv_data['difficulty'] and '0' in pv_data['difficulty']['normal']: - diff = pv_data['difficulty']['normal']['0']['level'].split('_') + self.data.static.put_music( + self.version, + song_id, + 0, + pv_data["song_name"], + pv_data["songinfo"]["arranger"], + pv_data["songinfo"]["illustrator"], + pv_data["songinfo"]["lyrics"], + pv_data["songinfo"]["music"], + float(f"{diff[2]}.{diff[3]}"), + pv_data["bpm"], + pv_data["date"], + ) + + if ( + "normal" in pv_data["difficulty"] + and "0" in pv_data["difficulty"]["normal"] + ): + diff = pv_data["difficulty"]["normal"]["0"]["level"].split("_") self.logger.info(f"Added song {song_id} chart 1") - self.data.static.put_music(self.version, song_id, 1, pv_data["song_name"], pv_data["songinfo"]["arranger"], - pv_data["songinfo"]["illustrator"], pv_data["songinfo"]["lyrics"], pv_data["songinfo"]["music"], - float(f"{diff[2]}.{diff[3]}"), pv_data["bpm"], pv_data["date"]) - - if "hard" in pv_data['difficulty'] and '0' in pv_data['difficulty']['hard']: - diff = pv_data['difficulty']['hard']['0']['level'].split('_') + self.data.static.put_music( + self.version, + song_id, + 1, + pv_data["song_name"], + pv_data["songinfo"]["arranger"], + pv_data["songinfo"]["illustrator"], + pv_data["songinfo"]["lyrics"], + pv_data["songinfo"]["music"], + float(f"{diff[2]}.{diff[3]}"), + pv_data["bpm"], + pv_data["date"], + ) + + if "hard" in pv_data["difficulty"] and "0" in pv_data["difficulty"]["hard"]: + diff = pv_data["difficulty"]["hard"]["0"]["level"].split("_") self.logger.info(f"Added song {song_id} chart 2") - self.data.static.put_music(self.version, song_id, 2, pv_data["song_name"], pv_data["songinfo"]["arranger"], - pv_data["songinfo"]["illustrator"], pv_data["songinfo"]["lyrics"], pv_data["songinfo"]["music"], - float(f"{diff[2]}.{diff[3]}"), pv_data["bpm"], pv_data["date"]) - - if "extreme" in pv_data['difficulty']: - if "0" in pv_data['difficulty']['extreme']: - diff = pv_data['difficulty']['extreme']['0']['level'].split('_') + self.data.static.put_music( + self.version, + song_id, + 2, + pv_data["song_name"], + pv_data["songinfo"]["arranger"], + pv_data["songinfo"]["illustrator"], + pv_data["songinfo"]["lyrics"], + pv_data["songinfo"]["music"], + float(f"{diff[2]}.{diff[3]}"), + pv_data["bpm"], + pv_data["date"], + ) + + if "extreme" in pv_data["difficulty"]: + if "0" in pv_data["difficulty"]["extreme"]: + diff = pv_data["difficulty"]["extreme"]["0"]["level"].split("_") self.logger.info(f"Added song {song_id} chart 3") - self.data.static.put_music(self.version, song_id, 3, pv_data["song_name"], pv_data["songinfo"]["arranger"], - pv_data["songinfo"]["illustrator"], pv_data["songinfo"]["lyrics"], pv_data["songinfo"]["music"], - float(f"{diff[2]}.{diff[3]}"), pv_data["bpm"], pv_data["date"]) + self.data.static.put_music( + self.version, + song_id, + 3, + pv_data["song_name"], + pv_data["songinfo"]["arranger"], + pv_data["songinfo"]["illustrator"], + pv_data["songinfo"]["lyrics"], + pv_data["songinfo"]["music"], + float(f"{diff[2]}.{diff[3]}"), + pv_data["bpm"], + pv_data["date"], + ) - if "1" in pv_data['difficulty']['extreme']: - diff = pv_data['difficulty']['extreme']['1']['level'].split('_') + if "1" in pv_data["difficulty"]["extreme"]: + diff = pv_data["difficulty"]["extreme"]["1"]["level"].split("_") self.logger.info(f"Added song {song_id} chart 4") - self.data.static.put_music(self.version, song_id, 4, pv_data["song_name"], pv_data["songinfo"]["arranger"], - pv_data["songinfo"]["illustrator"], pv_data["songinfo"]["lyrics"], pv_data["songinfo"]["music"], - float(f"{diff[2]}.{diff[3]}"), pv_data["bpm"], pv_data["date"]) + self.data.static.put_music( + self.version, + song_id, + 4, + pv_data["song_name"], + pv_data["songinfo"]["arranger"], + pv_data["songinfo"]["illustrator"], + pv_data["songinfo"]["lyrics"], + pv_data["songinfo"]["music"], + float(f"{diff[2]}.{diff[3]}"), + pv_data["bpm"], + pv_data["date"], + ) def add_branch(self, tree: Dict, vector: List, value: str): """ @@ -195,9 +295,9 @@ class DivaReader(BaseReader): Author: iJames on StackOverflow """ key = vector[0] - tree[key] = value \ - if len(vector) == 1 \ - else self.add_branch(tree[key] if key in tree else {}, - vector[1:], - value) - return tree \ No newline at end of file + tree[key] = ( + value + if len(vector) == 1 + else self.add_branch(tree[key] if key in tree else {}, vector[1:], value) + ) + return tree diff --git a/titles/diva/schema/__init__.py b/titles/diva/schema/__init__.py index 72cd97f..e149e6d 100644 --- a/titles/diva/schema/__init__.py +++ b/titles/diva/schema/__init__.py @@ -6,6 +6,12 @@ from titles.diva.schema.pv_customize import DivaPvCustomizeData from titles.diva.schema.item import DivaItemData from titles.diva.schema.static import DivaStaticData -__all__ = [DivaProfileData, DivaScoreData, DivaModuleData, - DivaCustomizeItemData, DivaPvCustomizeData, DivaItemData, - DivaStaticData] +__all__ = [ + DivaProfileData, + DivaScoreData, + DivaModuleData, + DivaCustomizeItemData, + DivaPvCustomizeData, + DivaItemData, + DivaStaticData, +] diff --git a/titles/diva/schema/customize.py b/titles/diva/schema/customize.py index f372349..91480f5 100644 --- a/titles/diva/schema/customize.py +++ b/titles/diva/schema/customize.py @@ -10,25 +10,29 @@ customize = Table( "diva_profile_customize_item", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("version", Integer, nullable=False), Column("item_id", Integer, nullable=False), - UniqueConstraint("user", "version", "item_id", name="diva_profile_customize_item_uk"), - mysql_charset='utf8mb4' + UniqueConstraint( + "user", "version", "item_id", name="diva_profile_customize_item_uk" + ), + mysql_charset="utf8mb4", ) class DivaCustomizeItemData(BaseData): 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 - ) + sql = insert(customize).values(version=version, user=aime_id, item_id=item_id) 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}") + self.logger.error( + f"{__name__} Failed to insert diva profile customize item! aime id: {aime_id} item: {item_id}" + ) return None return result.lastrowid @@ -36,10 +40,9 @@ class DivaCustomizeItemData(BaseData): """ Given a game version and an aime id, return all the customize items, not used directly """ - sql = customize.select(and_( - customize.c.version == version, - customize.c.user == aime_id - )) + sql = customize.select( + and_(customize.c.version == version, customize.c.user == aime_id) + ) result = self.execute(sql) if result is None: diff --git a/titles/diva/schema/item.py b/titles/diva/schema/item.py index ce7d910..4d484ae 100644 --- a/titles/diva/schema/item.py +++ b/titles/diva/schema/item.py @@ -11,37 +11,48 @@ shop = Table( "diva_profile_shop", 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("mdl_eqp_ary", String(32)), Column("c_itm_eqp_ary", String(59)), Column("ms_itm_flg_ary", String(59)), UniqueConstraint("user", "version", name="diva_profile_shop_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) class DivaItemData(BaseData): - def put_shop(self, aime_id: int, version: int, mdl_eqp_ary: str, - c_itm_eqp_ary: str, ms_itm_flg_ary: str) -> None: - + def put_shop( + self, + aime_id: int, + version: int, + mdl_eqp_ary: str, + c_itm_eqp_ary: str, + ms_itm_flg_ary: str, + ) -> None: sql = insert(shop).values( version=version, user=aime_id, mdl_eqp_ary=mdl_eqp_ary, c_itm_eqp_ary=c_itm_eqp_ary, - ms_itm_flg_ary=ms_itm_flg_ary + ms_itm_flg_ary=ms_itm_flg_ary, ) conflict = sql.on_duplicate_key_update( mdl_eqp_ary=mdl_eqp_ary, c_itm_eqp_ary=c_itm_eqp_ary, - ms_itm_flg_ary=ms_itm_flg_ary + ms_itm_flg_ary=ms_itm_flg_ary, ) 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}") + self.logger.error( + f"{__name__} Failed to insert diva profile! aime id: {aime_id} array: {mdl_eqp_ary}" + ) return None return result.lastrowid @@ -49,10 +60,7 @@ class DivaItemData(BaseData): """ 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 - )) + sql = shop.select(and_(shop.c.version == version, shop.c.user == aime_id)) result = self.execute(sql) if result is None: diff --git a/titles/diva/schema/module.py b/titles/diva/schema/module.py index f9c930c..5872d68 100644 --- a/titles/diva/schema/module.py +++ b/titles/diva/schema/module.py @@ -10,25 +10,27 @@ module = Table( "diva_profile_module", 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("module_id", Integer, nullable=False), UniqueConstraint("user", "version", "module_id", name="diva_profile_module_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) class DivaModuleData(BaseData): 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 - ) + sql = insert(module).values(version=version, user=aime_id, module_id=module_id) 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}") + self.logger.error( + f"{__name__} Failed to insert diva profile module! aime id: {aime_id} module: {module_id}" + ) return None return result.lastrowid @@ -36,10 +38,7 @@ class DivaModuleData(BaseData): """ 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 - )) + sql = module.select(and_(module.c.version == version, module.c.user == aime_id)) result = self.execute(sql) if result is None: diff --git a/titles/diva/schema/profile.py b/titles/diva/schema/profile.py index 993b03c..1a498e2 100644 --- a/titles/diva/schema/profile.py +++ b/titles/diva/schema/profile.py @@ -11,8 +11,11 @@ profile = Table( "diva_profile", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", - onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("version", Integer, nullable=False), Column("player_name", String(10), nullable=False), Column("lv_str", String(24), nullable=False, server_default="Dab on 'em"), @@ -29,10 +32,8 @@ profile = Table( Column("use_pv_skn_eqp", Boolean, nullable=False, server_default="0"), Column("use_pv_btn_se_eqp", Boolean, nullable=False, server_default="1"), Column("use_pv_sld_se_eqp", Boolean, nullable=False, server_default="0"), - Column("use_pv_chn_sld_se_eqp", Boolean, - nullable=False, server_default="0"), - Column("use_pv_sldr_tch_se_eqp", Boolean, - nullable=False, server_default="0"), + Column("use_pv_chn_sld_se_eqp", Boolean, nullable=False, server_default="0"), + Column("use_pv_sldr_tch_se_eqp", Boolean, nullable=False, server_default="0"), Column("nxt_pv_id", Integer, nullable=False, server_default="708"), Column("nxt_dffclty", Integer, nullable=False, server_default="2"), Column("nxt_edtn", Integer, nullable=False, server_default="0"), @@ -44,35 +45,39 @@ profile = Table( Column("lv_plt_id", Integer, nullable=False, server_default="1"), Column("passwd_stat", Integer, nullable=False, server_default="0"), Column("passwd", String(12), nullable=False, server_default="**********"), - Column("my_qst_id", String( - 128), server_default="-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"), - Column("my_qst_sts", String( - 128), server_default="-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"), + Column( + "my_qst_id", + String(128), + server_default="-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", + ), + Column( + "my_qst_sts", + String(128), + server_default="-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", + ), UniqueConstraint("user", "version", name="diva_profile_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) class DivaProfileData(BaseData): - def create_profile(self, version: int, aime_id: int, - player_name: str) -> Optional[int]: + def create_profile( + self, version: int, aime_id: int, player_name: str + ) -> Optional[int]: """ Given a game version, aime id, and player_name, create a profile and return it's ID """ sql = insert(profile).values( - version=version, - user=aime_id, - player_name=player_name + version=version, user=aime_id, player_name=player_name ) - conflict = sql.on_duplicate_key_update( - player_name=sql.inserted.player_name - ) + conflict = sql.on_duplicate_key_update(player_name=sql.inserted.player_name) 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}") + f"{__name__} Failed to insert diva profile! aime id: {aime_id} username: {player_name}" + ) return None return result.lastrowid @@ -86,17 +91,17 @@ class DivaProfileData(BaseData): result = self.execute(sql) if result is None: self.logger.error( - f"update_profile: failed to update profile! profile: {aime_id}") + f"update_profile: failed to update profile! profile: {aime_id}" + ) return None 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 """ - sql = profile.select(and_( - profile.c.version == version, - profile.c.user == aime_id - )) + sql = profile.select( + and_(profile.c.version == version, profile.c.user == aime_id) + ) result = self.execute(sql) if result is None: diff --git a/titles/diva/schema/pv_customize.py b/titles/diva/schema/pv_customize.py index e456e06..1ca8909 100644 --- a/titles/diva/schema/pv_customize.py +++ b/titles/diva/schema/pv_customize.py @@ -10,27 +10,44 @@ pv_customize = Table( "diva_profile_pv_customize", 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("pv_id", Integer, nullable=False), Column("mdl_eqp_ary", String(14), server_default="-999,-999,-999"), - Column("c_itm_eqp_ary", String(59), server_default="-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999"), - Column("ms_itm_flg_ary", String(59), server_default="-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1"), + Column( + "c_itm_eqp_ary", + String(59), + server_default="-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999", + ), + Column( + "ms_itm_flg_ary", + String(59), + server_default="-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1", + ), Column("skin", Integer, server_default="-1"), Column("btn_se", Integer, server_default="-1"), Column("sld_se", Integer, server_default="-1"), Column("chsld_se", Integer, server_default="-1"), Column("sldtch_se", Integer, server_default="-1"), UniqueConstraint("user", "version", "pv_id", name="diva_profile_pv_customize_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) class DivaPvCustomizeData(BaseData): - def put_pv_customize(self, aime_id: int, version: int, pv_id: int, - mdl_eqp_ary: str, c_itm_eqp_ary: str, - ms_itm_flg_ary: str) -> Optional[int]: - + def put_pv_customize( + self, + aime_id: int, + version: int, + pv_id: int, + mdl_eqp_ary: str, + c_itm_eqp_ary: str, + ms_itm_flg_ary: str, + ) -> Optional[int]: sql = insert(pv_customize).values( version=version, user=aime_id, @@ -49,19 +66,19 @@ class DivaPvCustomizeData(BaseData): result = self.execute(conflict) if result is None: - self.logger.error(f"{__name__} Failed to insert diva pv customize! aime id: {aime_id}") + self.logger.error( + f"{__name__} Failed to insert diva pv customize! aime id: {aime_id}" + ) return None return result.lastrowid - 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 """ - sql = pv_customize.select(and_( - pv_customize.c.user == aime_id, - pv_customize.c.pv_id == pv_id - )) + sql = pv_customize.select( + and_(pv_customize.c.user == aime_id, pv_customize.c.pv_id == pv_id) + ) result = self.execute(sql) if result is None: diff --git a/titles/diva/schema/score.py b/titles/diva/schema/score.py index 638c25a..2d86925 100644 --- a/titles/diva/schema/score.py +++ b/titles/diva/schema/score.py @@ -28,7 +28,7 @@ score = Table( Column("worst", Integer), Column("max_combo", Integer), UniqueConstraint("user", "pv_id", "difficulty", "edition", name="diva_score_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) playlog = Table( @@ -51,16 +51,29 @@ playlog = Table( Column("worst", Integer), Column("max_combo", Integer), Column("date_scored", TIMESTAMP, server_default=func.now()), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) class DivaScoreData(BaseData): - def put_best_score(self, user_id: int, game_version: int, song_id: int, - difficulty: int, edition: int, song_score: int, - atn_pnt: int, clr_kind: int, sort_kind: int, - cool: int, fine: int, safe: int, sad: int, - worst: int, max_combo: int) -> Optional[int]: + def put_best_score( + self, + user_id: int, + game_version: int, + song_id: int, + difficulty: int, + edition: int, + song_score: int, + atn_pnt: int, + clr_kind: int, + sort_kind: int, + cool: int, + fine: int, + safe: int, + sad: int, + worst: int, + max_combo: int, + ) -> Optional[int]: """ Update the user's best score for a chart """ @@ -98,16 +111,30 @@ class DivaScoreData(BaseData): result = self.execute(conflict) if result is None: self.logger.error( - f"{__name__} failed to insert best score! profile: {user_id}, song: {song_id}") + f"{__name__} failed to insert best score! profile: {user_id}, song: {song_id}" + ) return None return result.lastrowid - def put_playlog(self, user_id: int, game_version: int, song_id: int, - difficulty: int, edition: int, song_score: int, - atn_pnt: int, clr_kind: int, sort_kind: int, - cool: int, fine: int, safe: int, sad: int, - worst: int, max_combo: int) -> Optional[int]: + def put_playlog( + self, + user_id: int, + game_version: int, + song_id: int, + difficulty: int, + edition: int, + song_score: int, + atn_pnt: int, + clr_kind: int, + sort_kind: int, + cool: int, + fine: int, + safe: int, + sad: int, + worst: int, + max_combo: int, + ) -> Optional[int]: """ Add an entry to the user's play log """ @@ -126,24 +153,28 @@ class DivaScoreData(BaseData): safe=safe, sad=sad, worst=worst, - max_combo=max_combo + max_combo=max_combo, ) 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}") + f"{__name__} failed to insert playlog! profile: {user_id}, song: {song_id}, chart: {difficulty}" + ) return None return result.lastrowid - def get_best_user_score(self, user_id: int, pv_id: int, difficulty: int, - edition: int) -> Optional[Dict]: + def get_best_user_score( + self, user_id: int, pv_id: int, difficulty: int, edition: int + ) -> Optional[Dict]: sql = score.select( - and_(score.c.user == user_id, - score.c.pv_id == pv_id, - score.c.difficulty == difficulty, - score.c.edition == edition) + and_( + score.c.user == user_id, + score.c.pv_id == pv_id, + score.c.difficulty == difficulty, + score.c.edition == edition, + ) ) result = self.execute(sql) @@ -151,36 +182,48 @@ class DivaScoreData(BaseData): return None return result.fetchone() - def get_top3_scores(self, pv_id: int, difficulty: int, - edition: int) -> Optional[List[Dict]]: - sql = score.select( - and_(score.c.pv_id == pv_id, - score.c.difficulty == difficulty, - score.c.edition == edition) - ).order_by(score.c.score.desc()).limit(3) + def get_top3_scores( + self, pv_id: int, difficulty: int, edition: int + ) -> Optional[List[Dict]]: + sql = ( + score.select( + and_( + score.c.pv_id == pv_id, + score.c.difficulty == difficulty, + score.c.edition == edition, + ) + ) + .order_by(score.c.score.desc()) + .limit(3) + ) result = self.execute(sql) if result is None: return None return result.fetchall() - def get_global_ranking(self, user_id: int, pv_id: int, difficulty: int, - edition: int) -> Optional[List]: + def get_global_ranking( + self, user_id: int, pv_id: int, difficulty: int, edition: int + ) -> Optional[List]: # get the subquery max score of a user with pv_id, difficulty and # edition - sql_sub = select([score.c.score]).filter( - score.c.user == user_id, - score.c.pv_id == pv_id, - score.c.difficulty == difficulty, - score.c.edition == edition - ).scalar_subquery() + sql_sub = ( + select([score.c.score]) + .filter( + score.c.user == user_id, + score.c.pv_id == pv_id, + score.c.difficulty == difficulty, + score.c.edition == edition, + ) + .scalar_subquery() + ) # Perform the main query, also rename the resulting column to ranking sql = select(func.count(score.c.id).label("ranking")).filter( score.c.score >= sql_sub, score.c.pv_id == pv_id, score.c.difficulty == difficulty, - score.c.edition == edition + score.c.edition == edition, ) result = self.execute(sql) diff --git a/titles/diva/schema/static.py b/titles/diva/schema/static.py index c8d83bd..02ee0ec 100644 --- a/titles/diva/schema/static.py +++ b/titles/diva/schema/static.py @@ -25,7 +25,7 @@ music = Table( Column("bpm", Integer), Column("date", String(255)), UniqueConstraint("version", "songId", "chartId", name="diva_static_music_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) quests = Table( @@ -43,9 +43,8 @@ quests = Table( Column("quest_order", Integer), Column("start_datetime", String(255)), Column("end_datetime", String(255)), - UniqueConstraint("version", "questId", name="diva_static_quests_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) shop = Table( @@ -62,7 +61,7 @@ shop = Table( Column("end_date", String(255)), Column("enabled", Boolean, server_default="1"), UniqueConstraint("version", "shopId", name="diva_static_shop_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) items = Table( @@ -79,64 +78,91 @@ items = Table( Column("end_date", String(255)), Column("enabled", Boolean, server_default="1"), UniqueConstraint("version", "itemId", name="diva_static_items_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) + class DivaStaticData(BaseData): - def put_quests(self, version: int, questId: int, name: str, kind: int, unknown_0: int, unknown_1: int, unknown_2: int, quest_order: int, start_datetime: str, end_datetime: str) -> Optional[int]: + def put_quests( + self, + version: int, + questId: int, + name: str, + kind: int, + unknown_0: int, + unknown_1: int, + unknown_2: int, + quest_order: int, + start_datetime: str, + end_datetime: str, + ) -> Optional[int]: sql = insert(quests).values( - version = version, - questId = questId, - name = name, - kind = kind, - unknown_0 = unknown_0, - unknown_1 = unknown_1, - unknown_2 = unknown_2, - quest_order = quest_order, - start_datetime = start_datetime, - end_datetime = end_datetime + version=version, + questId=questId, + name=name, + kind=kind, + unknown_0=unknown_0, + unknown_1=unknown_1, + unknown_2=unknown_2, + quest_order=quest_order, + start_datetime=start_datetime, + end_datetime=end_datetime, ) - conflict = sql.on_duplicate_key_update( - name = name - ) + conflict = sql.on_duplicate_key_update(name=name) result = self.execute(conflict) - if result is None: return None + if result is None: + return None return result.lastrowid - + def get_enabled_quests(self, version: int) -> Optional[List[Row]]: - sql = select(quests).where(and_(quests.c.version == version, quests.c.quest_enable == True)) + sql = select(quests).where( + and_(quests.c.version == version, quests.c.quest_enable == True) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - def put_shop(self, version: int, shopId: int, name: str, type: int, points: int, unknown_0: int, start_date: str, end_date: str) -> Optional[int]: + def put_shop( + self, + version: int, + shopId: int, + name: str, + type: int, + points: int, + unknown_0: int, + start_date: str, + end_date: str, + ) -> Optional[int]: sql = insert(shop).values( - version = version, - shopId = shopId, - name = name, - type = type, - points = points, - unknown_0 = unknown_0, - start_date = start_date, - end_date = end_date + version=version, + shopId=shopId, + name=name, + type=type, + points=points, + unknown_0=unknown_0, + start_date=start_date, + end_date=end_date, ) - conflict = sql.on_duplicate_key_update( - name = name - ) + conflict = sql.on_duplicate_key_update(name=name) result = self.execute(conflict) - if result is None: return None + if result is None: + return None return result.lastrowid def get_enabled_shop(self, version: int, shopId: int) -> Optional[Row]: - sql = select(shop).where(and_( - shop.c.version == version, - shop.c.shopId == shopId, - shop.c.enabled == True)) + sql = select(shop).where( + and_( + shop.c.version == version, + shop.c.shopId == shopId, + shop.c.enabled == True, + ) + ) result = self.execute(sql) if result is None: @@ -144,40 +170,52 @@ class DivaStaticData(BaseData): return result.fetchone() def get_enabled_shops(self, version: int) -> Optional[List[Row]]: - sql = select(shop).where(and_( - shop.c.version == version, - shop.c.enabled == True)) + sql = select(shop).where( + and_(shop.c.version == version, shop.c.enabled == True) + ) result = self.execute(sql) if result is None: return None return result.fetchall() - def put_items(self, version: int, itemId: int, name: str, type: int, points: int, unknown_0: int, start_date: str, end_date: str) -> Optional[int]: + def put_items( + self, + version: int, + itemId: int, + name: str, + type: int, + points: int, + unknown_0: int, + start_date: str, + end_date: str, + ) -> Optional[int]: sql = insert(items).values( - version = version, - itemId = itemId, - name = name, - type = type, - points = points, - unknown_0 = unknown_0, - start_date = start_date, - end_date = end_date + version=version, + itemId=itemId, + name=name, + type=type, + points=points, + unknown_0=unknown_0, + start_date=start_date, + end_date=end_date, ) - conflict = sql.on_duplicate_key_update( - name = name - ) + conflict = sql.on_duplicate_key_update(name=name) result = self.execute(conflict) - if result is None: return None + if result is None: + return None return result.lastrowid def get_enabled_item(self, version: int, itemId: int) -> Optional[Row]: - sql = select(items).where(and_( - items.c.version == version, - items.c.itemId == itemId, - items.c.enabled == True)) + sql = select(items).where( + and_( + items.c.version == version, + items.c.itemId == itemId, + items.c.enabled == True, + ) + ) result = self.execute(sql) if result is None: @@ -185,66 +223,89 @@ class DivaStaticData(BaseData): return result.fetchone() def get_enabled_items(self, version: int) -> Optional[List[Row]]: - sql = select(items).where(and_( - items.c.version == version, - items.c.enabled == True)) + sql = select(items).where( + and_(items.c.version == version, items.c.enabled == True) + ) result = self.execute(sql) if result is None: return None return result.fetchall() - def put_music(self, version: int, song: int, chart: int, title: str, arranger: str, illustrator: str, - lyrics: str, music_comp: str, level: float, bpm: int, date: str) -> Optional[int]: + def put_music( + self, + version: int, + song: int, + chart: int, + title: str, + arranger: str, + illustrator: str, + lyrics: str, + music_comp: str, + level: float, + bpm: int, + date: str, + ) -> Optional[int]: sql = insert(music).values( - version = version, - songId = song, - chartId = chart, - title = title, - vocaloid_arranger = arranger, - pv_illustrator = illustrator, - lyrics = lyrics, - bg_music = music_comp, - level = level, - bpm = bpm, - date = date + version=version, + songId=song, + chartId=chart, + title=title, + vocaloid_arranger=arranger, + pv_illustrator=illustrator, + lyrics=lyrics, + bg_music=music_comp, + level=level, + bpm=bpm, + date=date, ) conflict = sql.on_duplicate_key_update( - title = title, - vocaloid_arranger = arranger, - pv_illustrator = illustrator, - lyrics = lyrics, - bg_music = music_comp, - level = level, - bpm = bpm, - date = date + title=title, + vocaloid_arranger=arranger, + pv_illustrator=illustrator, + lyrics=lyrics, + bg_music=music_comp, + level=level, + bpm=bpm, + date=date, ) result = self.execute(conflict) - if result is None: return None + if result is None: + return None return result.lastrowid - - def get_music(self, version: int, song_id: Optional[int] = None) -> Optional[List[Row]]: + + def get_music( + self, version: int, song_id: Optional[int] = None + ) -> Optional[List[Row]]: if song_id is None: sql = select(music).where(music.c.version == version) else: - sql = select(music).where(and_( - music.c.version == version, - music.c.songId == song_id, - )) + sql = select(music).where( + and_( + music.c.version == version, + music.c.songId == song_id, + ) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - - def get_music_chart(self, version: int, song_id: int, chart_id: int) -> Optional[List[Row]]: - sql = select(music).where(and_( - music.c.version == version, - music.c.songId == song_id, - music.c.chartId == chart_id - )) + + def get_music_chart( + self, version: int, song_id: int, chart_id: int + ) -> Optional[List[Row]]: + sql = select(music).where( + and_( + music.c.version == version, + music.c.songId == song_id, + music.c.chartId == chart_id, + ) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() diff --git a/titles/mai2/__init__.py b/titles/mai2/__init__.py index 71dbd5e..0d9ea89 100644 --- a/titles/mai2/__init__.py +++ b/titles/mai2/__init__.py @@ -7,4 +7,4 @@ index = Mai2Servlet database = Mai2Data reader = Mai2Reader game_codes = [Mai2Constants.GAME_CODE] -current_schema_version = 2 \ No newline at end of file +current_schema_version = 2 diff --git a/titles/mai2/base.py b/titles/mai2/base.py index ea98681..8a48d8b 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -7,7 +7,8 @@ from titles.mai2.const import Mai2Constants from titles.mai2.config import Mai2Config from titles.mai2.database import Mai2Data -class Mai2Base(): + +class Mai2Base: def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: self.core_config = cfg self.game_config = game_cfg @@ -17,23 +18,27 @@ class Mai2Base(): self.logger = logging.getLogger("mai2") def handle_get_game_setting_api_request(self, data: Dict): - reboot_start = date.strftime(datetime.now() + timedelta(hours=3), Mai2Constants.DATE_TIME_FORMAT) - reboot_end = date.strftime(datetime.now() + timedelta(hours=4), Mai2Constants.DATE_TIME_FORMAT) + reboot_start = date.strftime( + datetime.now() + timedelta(hours=3), Mai2Constants.DATE_TIME_FORMAT + ) + reboot_end = date.strftime( + datetime.now() + timedelta(hours=4), Mai2Constants.DATE_TIME_FORMAT + ) return { - "gameSetting": { - "isMaintenance": "false", - "requestInterval": 10, - "rebootStartTime": reboot_start, - "rebootEndTime": reboot_end, - "movieUploadLimit": 10000, - "movieStatus": 0, - "movieServerUri": "", - "deliverServerUri": "", - "oldServerUri": "", - "usbDlServerUri": "", - "rebootInterval": 0 + "gameSetting": { + "isMaintenance": "false", + "requestInterval": 10, + "rebootStartTime": reboot_start, + "rebootEndTime": reboot_end, + "movieUploadLimit": 10000, + "movieStatus": 0, + "movieServerUri": "", + "deliverServerUri": "", + "oldServerUri": "", + "usbDlServerUri": "", + "rebootInterval": 0, }, - "isAouAccession": "true", + "isAouAccession": "true", } def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: @@ -46,34 +51,44 @@ class Mai2Base(): def handle_get_game_event_api_request(self, data: Dict) -> Dict: events = self.data.static.get_enabled_events(self.version) events_lst = [] - if events is None: return {"type": data["type"], "length": 0, "gameEventList": []} + if events is None: + return {"type": data["type"], "length": 0, "gameEventList": []} for event in events: - events_lst.append({ - "type": event["type"], - "id": event["eventId"], - "startDate": "2017-12-05 07:00:00.0", - "endDate": "2099-12-31 00:00:00.0" - }) + events_lst.append( + { + "type": event["type"], + "id": event["eventId"], + "startDate": "2017-12-05 07:00:00.0", + "endDate": "2099-12-31 00:00:00.0", + } + ) - return {"type": data["type"], "length": len(events_lst), "gameEventList": events_lst} + return { + "type": data["type"], + "length": len(events_lst), + "gameEventList": events_lst, + } def handle_get_game_ng_music_id_api_request(self, data: Dict) -> Dict: return {"length": 0, "musicIdList": []} def handle_get_game_charge_api_request(self, data: Dict) -> Dict: game_charge_list = self.data.static.get_enabled_tickets(self.version, 1) - if game_charge_list is None: return {"length": 0, "gameChargeList": []} + if game_charge_list is None: + return {"length": 0, "gameChargeList": []} charge_list = [] for x in range(len(game_charge_list)): - charge_list.append({ - "orderId": x, - "chargeId": game_charge_list[x]["ticketId"], - "price": game_charge_list[x]["price"], - "startDate": "2017-12-05 07:00:00.0", - "endDate": "2099-12-31 00:00:00.0" - }) + charge_list.append( + { + "orderId": x, + "chargeId": game_charge_list[x]["ticketId"], + "price": game_charge_list[x]["price"], + "startDate": "2017-12-05 07:00:00.0", + "endDate": "2099-12-31 00:00:00.0", + } + ) return {"length": len(charge_list), "gameChargeList": charge_list} @@ -92,7 +107,8 @@ class Mai2Base(): 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 + if p is None or o is None: + return {} # Register profile = p._asdict() option = o._asdict() @@ -106,20 +122,24 @@ class Mai2Base(): "lastLoginDate": profile["lastLoginDate"], "lastPlayDate": profile["lastPlayDate"], "playerRating": profile["playerRating"], - "nameplateId": 0, # Unused + "nameplateId": 0, # Unused "iconId": profile["iconId"], - "trophyId": 0, # Unused + "trophyId": 0, # Unused "partnerId": profile["partnerId"], "frameId": profile["frameId"], - "dispRate": option["dispRate"], # 0: all/begin, 1: disprate, 2: dispDan, 3: hide, 4: end + "dispRate": option[ + "dispRate" + ], # 0: all/begin, 1: disprate, 2: dispDan, 3: hide, 4: end "totalAwake": profile["totalAwake"], "isNetMember": profile["isNetMember"], "dailyBonusDate": profile["dailyBonusDate"], "headPhoneVolume": option["headPhoneVolume"], - "isInherit": False, # Not sure what this is or does?? - "banState": profile["banState"] if profile["banState"] is not None else 0 # New with uni+ + "isInherit": False, # Not sure what this is or does?? + "banState": profile["banState"] + if profile["banState"] is not None + else 0, # New with uni+ } - + def handle_user_login_api_request(self, data: Dict) -> Dict: profile = self.data.profile.get_profile_detail(data["userId"], self.version) @@ -137,10 +157,10 @@ class Mai2Base(): "returnCode": 1, "lastLoginDate": lastLoginDate, "loginCount": loginCt, - "consecutiveLoginCount": 0, # We don't really have a way to track this... - "loginId": loginCt # Used with the playlog! + "consecutiveLoginCount": 0, # We don't really have a way to track this... + "loginId": loginCt, # Used with the playlog! } - + def handle_upload_user_playlog_api_request(self, data: Dict) -> Dict: user_id = data["userId"] playlog = data["userPlaylog"] @@ -154,50 +174,83 @@ class Mai2Base(): if "userData" in upsert and len(upsert["userData"]) > 0: upsert["userData"][0]["isNetMember"] = 1 upsert["userData"][0].pop("accessCode") - self.data.profile.put_profile_detail(user_id, self.version, upsert["userData"][0]) - + self.data.profile.put_profile_detail( + user_id, self.version, upsert["userData"][0] + ) + if "userExtend" in upsert and len(upsert["userExtend"]) > 0: - self.data.profile.put_profile_extend(user_id, self.version, upsert["userExtend"][0]) + self.data.profile.put_profile_extend( + user_id, self.version, upsert["userExtend"][0] + ) if "userGhost" in upsert: for ghost in upsert["userGhost"]: self.data.profile.put_profile_extend(user_id, self.version, ghost) - + if "userOption" in upsert and len(upsert["userOption"]) > 0: - self.data.profile.put_profile_option(user_id, self.version, upsert["userOption"][0]) + self.data.profile.put_profile_option( + user_id, self.version, upsert["userOption"][0] + ) if "userRatingList" in upsert and len(upsert["userRatingList"]) > 0: - self.data.profile.put_profile_rating(user_id, self.version, upsert["userRatingList"][0]) + 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 k, v in upsert["userActivityList"][0].items(): for act in v: self.data.profile.put_profile_activity(user_id, act) if upsert["isNewCharacterList"] and int(upsert["isNewCharacterList"]) > 0: for char in upsert["userCharacterList"]: - self.data.item.put_character(user_id, char["characterId"], char["level"], char["awakening"], char["useCount"]) + self.data.item.put_character( + user_id, + char["characterId"], + char["level"], + char["awakening"], + char["useCount"], + ) if upsert["isNewItemList"] and int(upsert["isNewItemList"]) > 0: for item in upsert["userItemList"]: - self.data.item.put_item(user_id, int(item["itemKind"]), item["itemId"], item["stock"], item["isValid"]) + self.data.item.put_item( + user_id, + int(item["itemKind"]), + item["itemId"], + item["stock"], + item["isValid"], + ) if upsert["isNewLoginBonusList"] and int(upsert["isNewLoginBonusList"]) > 0: for login_bonus in upsert["userLoginBonusList"]: - self.data.item.put_login_bonus(user_id, login_bonus["bonusId"], login_bonus["point"], login_bonus["isCurrent"], login_bonus["isComplete"]) + self.data.item.put_login_bonus( + user_id, + login_bonus["bonusId"], + login_bonus["point"], + login_bonus["isCurrent"], + login_bonus["isComplete"], + ) if upsert["isNewMapList"] and int(upsert["isNewMapList"]) > 0: for map in upsert["userMapList"]: - self.data.item.put_map(user_id, map["mapId"], map["distance"], map["isLock"], map["isClear"], map["isComplete"]) - + self.data.item.put_map( + user_id, + map["mapId"], + map["distance"], + map["isLock"], + map["isClear"], + map["isComplete"], + ) + if upsert["isNewMusicDetailList"] and int(upsert["isNewMusicDetailList"]) > 0: for music in upsert["userMusicDetailList"]: self.data.score.put_best_score(user_id, music) - + if upsert["isNewCourseList"] and int(upsert["isNewCourseList"]) > 0: for course in upsert["userCourseList"]: self.data.score.put_course(user_id, course) - + if upsert["isNewFavoriteList"] and int(upsert["isNewFavoriteList"]) > 0: for fav in upsert["userFavoriteList"]: self.data.item.put_favorite(user_id, fav["kind"], fav["itemIdList"]) @@ -211,45 +264,39 @@ class Mai2Base(): 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 + if profile is None: + return profile_dict = profile._asdict() profile_dict.pop("id") profile_dict.pop("user") profile_dict.pop("version") - return { - "userId": data["userId"], - "userData": profile_dict - } + return {"userId": data["userId"], "userData": profile_dict} def handle_get_user_extend_api_request(self, data: Dict) -> Dict: extend = self.data.profile.get_profile_extend(data["userId"], self.version) - if extend is None: return + if extend is None: + return extend_dict = extend._asdict() extend_dict.pop("id") extend_dict.pop("user") extend_dict.pop("version") - return { - "userId": data["userId"], - "userExtend": extend_dict - } + return {"userId": data["userId"], "userExtend": extend_dict} def handle_get_user_option_api_request(self, data: Dict) -> Dict: options = self.data.profile.get_profile_option(data["userId"], self.version) - if options is None: return + if options is None: + return options_dict = options._asdict() options_dict.pop("id") options_dict.pop("user") options_dict.pop("version") - return { - "userId": data["userId"], - "userOption": options_dict - } + return {"userId": data["userId"], "userOption": options_dict} def handle_get_user_card_api_request(self, data: Dict) -> Dict: return {"userId": data["userId"], "nextIndex": 0, "userCardList": []} @@ -266,73 +313,83 @@ class Mai2Base(): for x in range(next_idx, data["maxCount"]): try: - user_item_list.append({"item_kind": user_items[x]["item_kind"], "item_id": user_items[x]["item_id"], - "stock": user_items[x]["stock"], "isValid": user_items[x]["is_valid"]}) - except: break - + user_item_list.append( + { + "item_kind": user_items[x]["item_kind"], + "item_id": user_items[x]["item_id"], + "stock": user_items[x]["stock"], + "isValid": user_items[x]["is_valid"], + } + ) + except: + break + if len(user_item_list) == data["maxCount"]: next_idx = data["nextIndex"] + data["maxCount"] + 1 break - return {"userId": data["userId"], "nextIndex": next_idx, "itemKind": kind, "userItemList": user_item_list} + return { + "userId": data["userId"], + "nextIndex": next_idx, + "itemKind": kind, + "userItemList": user_item_list, + } def handle_get_user_character_api_request(self, data: Dict) -> Dict: characters = self.data.item.get_characters(data["userId"]) chara_list = [] for chara in characters: - chara_list.append({ - "characterId": chara["character_id"], - "level": chara["level"], - "awakening": chara["awakening"], - "useCount": chara["use_count"], - }) + chara_list.append( + { + "characterId": chara["character_id"], + "level": chara["level"], + "awakening": chara["awakening"], + "useCount": chara["use_count"], + } + ) return {"userId": data["userId"], "userCharacterList": chara_list} - + def handle_get_user_favorite_api_request(self, data: Dict) -> Dict: favorites = self.data.item.get_favorites(data["userId"], data["itemKind"]) - if favorites is None: return + if favorites is None: + return userFavs = [] for fav in favorites: - userFavs.append({ - "userId": data["userId"], - "itemKind": fav["itemKind"], - "itemIdList": fav["itemIdList"] - }) + userFavs.append( + { + "userId": data["userId"], + "itemKind": fav["itemKind"], + "itemIdList": fav["itemIdList"], + } + ) - return { - "userId": data["userId"], - "userFavoriteData": userFavs - } + return {"userId": data["userId"], "userFavoriteData": userFavs} def handle_get_user_ghost_api_request(self, data: Dict) -> Dict: ghost = self.data.profile.get_profile_ghost(data["userId"], self.version) - if ghost is None: return + if ghost is None: + return ghost_dict = ghost._asdict() ghost_dict.pop("user") ghost_dict.pop("id") ghost_dict.pop("version_int") - return { - "userId": data["userId"], - "userGhost": ghost_dict - } + return {"userId": data["userId"], "userGhost": ghost_dict} def handle_get_user_rating_api_request(self, data: Dict) -> Dict: rating = self.data.profile.get_profile_rating(data["userId"], self.version) - if rating is None: return + if rating is None: + return rating_dict = rating._asdict() rating_dict.pop("user") rating_dict.pop("id") rating_dict.pop("version") - return { - "userId": data["userId"], - "userRating": rating_dict - } + return {"userId": data["userId"], "userRating": rating_dict} def handle_get_user_activity_api_request(self, data: Dict) -> Dict: """ @@ -340,31 +397,27 @@ class Mai2Base(): """ playlist = self.data.profile.get_profile_activity(data["userId"], 1) musiclist = self.data.profile.get_profile_activity(data["userId"], 2) - if playlist is None or musiclist is None: return + if playlist is None or musiclist is None: + return plst = [] mlst = [] for play in playlist: - tmp = play._asdict() + tmp = play._asdict() tmp["id"] = tmp["activityId"] tmp.pop("activityId") tmp.pop("user") plst.append(tmp) for music in musiclist: - tmp = music._asdict() + tmp = music._asdict() tmp["id"] = tmp["activityId"] tmp.pop("activityId") tmp.pop("user") mlst.append(tmp) - return { - "userActivity": { - "playList": plst, - "musicList": mlst - } - } + return {"userActivity": {"playList": plst, "musicList": mlst}} def handle_get_user_course_api_request(self, data: Dict) -> Dict: user_courses = self.data.score.get_courses(data["userId"]) @@ -389,21 +442,30 @@ class Mai2Base(): for x in range(data["nextIndex"], data["maxCount"] + data["nextIndex"]): try: - friend_season_ranking_list.append({ - "mapId": friend_season_ranking_list[x]["map_id"], - "distance": friend_season_ranking_list[x]["distance"], - "isLock": friend_season_ranking_list[x]["is_lock"], - "isClear": friend_season_ranking_list[x]["is_clear"], - "isComplete": friend_season_ranking_list[x]["is_complete"], - }) + friend_season_ranking_list.append( + { + "mapId": friend_season_ranking_list[x]["map_id"], + "distance": friend_season_ranking_list[x]["distance"], + "isLock": friend_season_ranking_list[x]["is_lock"], + "isClear": friend_season_ranking_list[x]["is_clear"], + "isComplete": friend_season_ranking_list[x]["is_complete"], + } + ) except: break # We're capped and still have some left to go - if len(friend_season_ranking_list) == data["maxCount"] and len(friend_season_ranking) > data["maxCount"] + data["nextIndex"]: + if ( + len(friend_season_ranking_list) == data["maxCount"] + and len(friend_season_ranking) > data["maxCount"] + data["nextIndex"] + ): next_index = data["maxCount"] + data["nextIndex"] - return {"userId": data["userId"], "nextIndex": next_index, "userFriendSeasonRankingList": friend_season_ranking_list} + return { + "userId": data["userId"], + "nextIndex": next_index, + "userFriendSeasonRankingList": friend_season_ranking_list, + } def handle_get_user_map_api_request(self, data: Dict) -> Dict: maps = self.data.item.get_maps(data["userId"]) @@ -412,21 +474,30 @@ class Mai2Base(): for x in range(data["nextIndex"], data["maxCount"] + data["nextIndex"]): try: - map_list.append({ - "mapId": maps[x]["map_id"], - "distance": maps[x]["distance"], - "isLock": maps[x]["is_lock"], - "isClear": maps[x]["is_clear"], - "isComplete": maps[x]["is_complete"], - }) + map_list.append( + { + "mapId": maps[x]["map_id"], + "distance": maps[x]["distance"], + "isLock": maps[x]["is_lock"], + "isClear": maps[x]["is_clear"], + "isComplete": maps[x]["is_complete"], + } + ) except: break # We're capped and still have some left to go - if len(map_list) == data["maxCount"] and len(maps) > data["maxCount"] + data["nextIndex"]: + if ( + len(map_list) == data["maxCount"] + and len(maps) > data["maxCount"] + data["nextIndex"] + ): next_index = data["maxCount"] + data["nextIndex"] - return {"userId": data["userId"], "nextIndex": next_index, "userMapList": map_list} + return { + "userId": data["userId"], + "nextIndex": next_index, + "userMapList": map_list, + } def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: login_bonuses = self.data.item.get_login_bonuses(data["userId"]) @@ -435,20 +506,29 @@ class Mai2Base(): for x in range(data["nextIndex"], data["maxCount"] + data["nextIndex"]): try: - login_bonus_list.append({ - "bonusId": login_bonuses[x]["bonus_id"], - "point": login_bonuses[x]["point"], - "isCurrent": login_bonuses[x]["is_current"], - "isComplete": login_bonuses[x]["is_complete"], - }) + login_bonus_list.append( + { + "bonusId": login_bonuses[x]["bonus_id"], + "point": login_bonuses[x]["point"], + "isCurrent": login_bonuses[x]["is_current"], + "isComplete": login_bonuses[x]["is_complete"], + } + ) except: break # We're capped and still have some left to go - if len(login_bonus_list) == data["maxCount"] and len(login_bonuses) > data["maxCount"] + data["nextIndex"]: + if ( + len(login_bonus_list) == data["maxCount"] + and len(login_bonuses) > data["maxCount"] + data["nextIndex"] + ): next_index = data["maxCount"] + data["nextIndex"] - return {"userId": data["userId"], "nextIndex": next_index, "userLoginBonusList": login_bonus_list} + return { + "userId": data["userId"], + "nextIndex": next_index, + "userLoginBonusList": login_bonus_list, + } def handle_get_user_region_api_request(self, data: Dict) -> Dict: return {"userId": data["userId"], "length": 0, "userRegionList": []} @@ -460,18 +540,24 @@ class Mai2Base(): if songs is not None: for song in songs: - music_detail_list.append({ - "musicId": song["song_id"], - "level": song["chart_id"], - "playCount": song["play_count"], - "achievement": song["achievement"], - "comboStatus": song["combo_status"], - "syncStatus": song["sync_status"], - "deluxscoreMax": song["dx_score"], - "scoreRank": song["score_rank"], - }) + music_detail_list.append( + { + "musicId": song["song_id"], + "level": song["chart_id"], + "playCount": song["play_count"], + "achievement": song["achievement"], + "comboStatus": song["combo_status"], + "syncStatus": song["sync_status"], + "deluxscoreMax": song["dx_score"], + "scoreRank": song["score_rank"], + } + ) if len(music_detail_list) == data["maxCount"]: next_index = data["maxCount"] + data["nextIndex"] break - return {"userId": data["userId"], "nextIndex": next_index, "userMusicList": [{"userMusicDetailList": music_detail_list}]} + return { + "userId": data["userId"], + "nextIndex": next_index, + "userMusicList": [{"userMusicDetailList": music_detail_list}], + } diff --git a/titles/mai2/config.py b/titles/mai2/config.py index 7fe9a33..3a20065 100644 --- a/titles/mai2/config.py +++ b/titles/mai2/config.py @@ -1,17 +1,25 @@ from core.config import CoreConfig -class Mai2ServerConfig(): + +class Mai2ServerConfig: def __init__(self, parent: "Mai2Config") -> None: self.__config = parent @property def enable(self) -> bool: - return CoreConfig.get_config_field(self.__config, 'mai2', 'server', 'enable', default=True) - + return CoreConfig.get_config_field( + self.__config, "mai2", "server", "enable", default=True + ) + @property def loglevel(self) -> int: - return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'mai2', 'server', 'loglevel', default="info")) + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "mai2", "server", "loglevel", default="info" + ) + ) + class Mai2Config(dict): def __init__(self) -> None: - self.server = Mai2ServerConfig(self) \ No newline at end of file + self.server = Mai2ServerConfig(self) diff --git a/titles/mai2/const.py b/titles/mai2/const.py index c6ae129..dd1dca0 100644 --- a/titles/mai2/const.py +++ b/titles/mai2/const.py @@ -1,4 +1,4 @@ -class Mai2Constants(): +class Mai2Constants: GRADE = { "D": 0, "C": 1, @@ -13,22 +13,10 @@ class Mai2Constants(): "SS": 10, "SS+": 11, "SSS": 12, - "SSS+": 13 - } - FC = { - "None": 0, - "FC": 1, - "FC+": 2, - "AP": 3, - "AP+": 4 - } - SYNC = { - "None": 0, - "FS": 1, - "FS+": 2, - "FDX": 3, - "FDX+": 4 + "SSS+": 13, } + FC = {"None": 0, "FC": 1, "FC+": 2, "AP": 3, "AP+": 4} + SYNC = {"None": 0, "FS": 1, "FS+": 2, "FDX": 3, "FDX+": 4} DATE_TIME_FORMAT = "%Y-%m-%d %H:%M:%S" @@ -43,9 +31,15 @@ class Mai2Constants(): VER_MAIMAI_DX_UNIVERSE = 4 VER_MAIMAI_DX_UNIVERSE_PLUS = 5 - VERSION_STRING = ("maimai Delux", "maimai Delux PLUS", "maimai Delux Splash", "maimai Delux Splash PLUS", "maimai Delux Universe", - "maimai Delux Universe PLUS") + VERSION_STRING = ( + "maimai Delux", + "maimai Delux PLUS", + "maimai Delux Splash", + "maimai Delux Splash PLUS", + "maimai Delux Universe", + "maimai Delux Universe PLUS", + ) @classmethod def game_ver_to_string(cls, ver: int): - return cls.VERSION_STRING[ver] \ No newline at end of file + return cls.VERSION_STRING[ver] diff --git a/titles/mai2/database.py b/titles/mai2/database.py index 7a19e75..be9e518 100644 --- a/titles/mai2/database.py +++ b/titles/mai2/database.py @@ -1,6 +1,12 @@ from core.data import Data from core.config import CoreConfig -from titles.mai2.schema import Mai2ItemData, Mai2ProfileData, Mai2StaticData, Mai2ScoreData +from titles.mai2.schema import ( + Mai2ItemData, + Mai2ProfileData, + Mai2StaticData, + Mai2ScoreData, +) + class Mai2Data(Data): def __init__(self, cfg: CoreConfig) -> None: @@ -9,4 +15,4 @@ class Mai2Data(Data): self.profile = Mai2ProfileData(self.config, self.session) self.item = Mai2ItemData(self.config, self.session) self.static = Mai2StaticData(self.config, self.session) - self.score = Mai2ScoreData(self.config, self.session) \ No newline at end of file + self.score = Mai2ScoreData(self.config, self.session) diff --git a/titles/mai2/index.py b/titles/mai2/index.py index 305e389..0679d1f 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -20,12 +20,14 @@ from titles.mai2.universe import Mai2Universe from titles.mai2.universeplus import Mai2UniversePlus -class Mai2Servlet(): +class Mai2Servlet: def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: self.core_cfg = core_cfg self.game_cfg = Mai2Config() if path.exists(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"): - self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"))) + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}")) + ) self.versions = [ Mai2Base(core_cfg, self.game_cfg), @@ -39,34 +41,52 @@ class Mai2Servlet(): self.logger = logging.getLogger("mai2") log_fmt_str = "[%(asctime)s] Mai2 | %(levelname)s | %(message)s" log_fmt = logging.Formatter(log_fmt_str) - fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.core_cfg.server.log_dir, "mai2"), encoding='utf8', - when="d", backupCount=10) + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.core_cfg.server.log_dir, "mai2"), + encoding="utf8", + when="d", + backupCount=10, + ) fileHandler.setFormatter(log_fmt) - + consoleHandler = logging.StreamHandler() consoleHandler.setFormatter(log_fmt) self.logger.addHandler(fileHandler) self.logger.addHandler(consoleHandler) - + self.logger.setLevel(self.game_cfg.server.loglevel) - coloredlogs.install(level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str) + coloredlogs.install( + level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str + ) @classmethod - def get_allnet_info(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str, str]: + def get_allnet_info( + cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str + ) -> Tuple[bool, str, str]: game_cfg = Mai2Config() if path.exists(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"): - game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"))) + game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}")) + ) if not game_cfg.server.enable: return (False, "", "") - + if core_cfg.server.is_develop: - return (True, f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/", f"{core_cfg.title.hostname}:{core_cfg.title.port}/") - - return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", f"{core_cfg.title.hostname}/") + return ( + True, + f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/", + f"{core_cfg.title.hostname}:{core_cfg.title.port}/", + ) + + return ( + True, + f"http://{core_cfg.title.hostname}/{game_code}/$v/", + f"{core_cfg.title.hostname}/", + ) def render_POST(self, request: Request, version: int, url_path: str) -> bytes: if url_path.lower() == "/ping": @@ -78,34 +98,36 @@ class Mai2Servlet(): internal_ver = 0 endpoint = url_split[len(url_split) - 1] - if version < 105: # 1.0 + if version < 105: # 1.0 internal_ver = Mai2Constants.VER_MAIMAI_DX - elif version >= 105 and version < 110: # Plus + elif version >= 105 and version < 110: # Plus internal_ver = Mai2Constants.VER_MAIMAI_DX_PLUS - elif version >= 110 and version < 115: # Splash + elif version >= 110 and version < 115: # Splash internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH - elif version >= 115 and version < 120: # Splash Plus + elif version >= 115 and version < 120: # Splash Plus internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS - elif version >= 120 and version < 125: # Universe + elif version >= 120 and version < 125: # Universe internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE - elif version >= 125: # Universe Plus + elif version >= 125: # Universe Plus internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32: - # If we get a 32 character long hex string, it's a hash and we're - # doing encrypted. The likelyhood of false positives is low but + # If we get a 32 character long hex string, it's a hash and we're + # doing encrypted. The likelyhood of false positives is low but # technically not 0 self.logger.error("Encryption not supported at this time") - try: + try: unzip = zlib.decompress(req_raw) - + except zlib.error as e: - self.logger.error(f"Failed to decompress v{version} {endpoint} request -> {e}") + self.logger.error( + f"Failed to decompress v{version} {endpoint} request -> {e}" + ) return zlib.compress(b'{"stat": "0"}') - + req_data = json.loads(unzip) - + self.logger.info(f"v{version} {endpoint} request - {req_data}") func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" @@ -121,10 +143,10 @@ class Mai2Servlet(): except Exception as e: self.logger.error(f"Error handling v{version} method {endpoint} - {e}") return zlib.compress(b'{"stat": "0"}') - + if resp == None: - resp = {'returnCode': 1} - + resp = {"returnCode": 1} + self.logger.info(f"Response {resp}") - + return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) diff --git a/titles/mai2/plus.py b/titles/mai2/plus.py index 2af7bf6..a3c9288 100644 --- a/titles/mai2/plus.py +++ b/titles/mai2/plus.py @@ -8,7 +8,8 @@ from titles.mai2.base import Mai2Base from titles.mai2.config import Mai2Config from titles.mai2.const import Mai2Constants + class Mai2Plus(Mai2Base): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) - self.version = Mai2Constants.VER_MAIMAI_DX_PLUS \ No newline at end of file + self.version = Mai2Constants.VER_MAIMAI_DX_PLUS diff --git a/titles/mai2/read.py b/titles/mai2/read.py index 1652292..2c0567c 100644 --- a/titles/mai2/read.py +++ b/titles/mai2/read.py @@ -11,25 +11,35 @@ from read import BaseReader from titles.mai2.const import Mai2Constants from titles.mai2.database import Mai2Data + class Mai2Reader(BaseReader): - def __init__(self, config: CoreConfig, version: int, bin_dir: Optional[str], opt_dir: Optional[str], extra: Optional[str]) -> None: + def __init__( + self, + config: CoreConfig, + version: int, + bin_dir: Optional[str], + opt_dir: Optional[str], + extra: Optional[str], + ) -> None: super().__init__(config, version, bin_dir, opt_dir, extra) self.data = Mai2Data(config) try: - self.logger.info(f"Start importer for {Mai2Constants.game_ver_to_string(version)}") + self.logger.info( + f"Start importer for {Mai2Constants.game_ver_to_string(version)}" + ) except IndexError: self.logger.error(f"Invalid maidx version {version}") exit(1) - + def read(self) -> None: data_dirs = [] if self.bin_dir is not None: data_dirs += self.get_data_directories(self.bin_dir) - + if self.opt_dir is not None: - data_dirs += self.get_data_directories(self.opt_dir) - + data_dirs += self.get_data_directories(self.opt_dir) + for dir in data_dirs: self.logger.info(f"Read from {dir}") self.get_events(f"{dir}/event") @@ -43,47 +53,64 @@ class Mai2Reader(BaseReader): for dir in dirs: if os.path.exists(f"{root}/{dir}/Event.xml"): with open(f"{root}/{dir}/Event.xml", encoding="utf-8") as f: - troot = ET.fromstring(f.read()) - name = troot.find('name').find('str').text - id = int(troot.find('name').find('id').text) - event_type = int(troot.find('infoType').text) + name = troot.find("name").find("str").text + id = int(troot.find("name").find("id").text) + event_type = int(troot.find("infoType").text) - self.data.static.put_game_event(self.version, event_type, id, name) + self.data.static.put_game_event( + self.version, event_type, id, name + ) self.logger.info(f"Added event {id}...") - + def read_music(self, base_dir: str) -> None: self.logger.info(f"Reading music from {base_dir}...") for root, dirs, files in os.walk(base_dir): - for dir in dirs: + for dir in dirs: if os.path.exists(f"{root}/{dir}/Music.xml"): with open(f"{root}/{dir}/Music.xml", encoding="utf-8") as f: troot = ET.fromstring(f.read()) - song_id = int(troot.find('name').find('id').text) - title = troot.find('name').find('str').text - artist = troot.find('artistName').find('str').text - genre = troot.find('genreName').find('str').text - bpm = int(troot.find('bpm').text) - added_ver = troot.find('AddVersion').find('str').text + song_id = int(troot.find("name").find("id").text) + title = troot.find("name").find("str").text + artist = troot.find("artistName").find("str").text + genre = troot.find("genreName").find("str").text + bpm = int(troot.find("bpm").text) + added_ver = troot.find("AddVersion").find("str").text - note_data = troot.find('notesData').findall('Notes') + note_data = troot.find("notesData").findall("Notes") for dif in note_data: - path = dif.find('file').find('path').text + path = dif.find("file").find("path").text if path is not None: if os.path.exists(f"{root}/{dir}/{path}"): - chart_id = int(path.split(".")[0].split('_')[1]) - diff_num = float(f"{dif.find('level').text}.{dif.find('levelDecimal').text}") - note_designer = dif.find('notesDesigner').find('str').text + chart_id = int(path.split(".")[0].split("_")[1]) + diff_num = float( + f"{dif.find('level').text}.{dif.find('levelDecimal').text}" + ) + note_designer = ( + dif.find("notesDesigner").find("str").text + ) + + self.data.static.put_game_music( + self.version, + song_id, + chart_id, + title, + artist, + genre, + bpm, + added_ver, + diff_num, + note_designer, + ) + + self.logger.info( + f"Added music id {song_id} chart {chart_id}" + ) - self.data.static.put_game_music(self.version, song_id, chart_id, title, artist, - genre, bpm, added_ver, diff_num, note_designer) - - self.logger.info(f"Added music id {song_id} chart {chart_id}") - def read_tickets(self, base_dir: str) -> None: self.logger.info(f"Reading tickets from {base_dir}...") @@ -91,13 +118,14 @@ class Mai2Reader(BaseReader): for dir in dirs: if os.path.exists(f"{root}/{dir}/Ticket.xml"): with open(f"{root}/{dir}/Ticket.xml", encoding="utf-8") as f: - troot = ET.fromstring(f.read()) - name = troot.find('name').find('str').text - id = int(troot.find('name').find('id').text) - ticket_type = int(troot.find('ticketKind').find('id').text) - price = int(troot.find('creditNum').text) + name = troot.find("name").find("str").text + id = int(troot.find("name").find("id").text) + ticket_type = int(troot.find("ticketKind").find("id").text) + price = int(troot.find("creditNum").text) - self.data.static.put_game_ticket(self.version, id, ticket_type, price, name) + self.data.static.put_game_ticket( + self.version, id, ticket_type, price, name + ) self.logger.info(f"Added ticket {id}...") diff --git a/titles/mai2/schema/__init__.py b/titles/mai2/schema/__init__.py index c2be969..7a8c060 100644 --- a/titles/mai2/schema/__init__.py +++ b/titles/mai2/schema/__init__.py @@ -3,4 +3,4 @@ from titles.mai2.schema.item import Mai2ItemData from titles.mai2.schema.static import Mai2StaticData from titles.mai2.schema.score import Mai2ScoreData -__all__ = [Mai2ProfileData, Mai2ItemData, Mai2StaticData, Mai2ScoreData] \ No newline at end of file +__all__ = [Mai2ProfileData, Mai2ItemData, Mai2StaticData, Mai2ScoreData] diff --git a/titles/mai2/schema/item.py b/titles/mai2/schema/item.py index 4f283d3..072eb3e 100644 --- a/titles/mai2/schema/item.py +++ b/titles/mai2/schema/item.py @@ -12,20 +12,28 @@ character = Table( "mai2_item_character", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("character_id", Integer, nullable=False), Column("level", Integer, nullable=False, server_default="1"), Column("awakening", Integer, nullable=False, server_default="0"), Column("use_count", Integer, nullable=False, server_default="0"), UniqueConstraint("user", "character_id", name="mai2_item_character_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) card = Table( "mai2_item_card", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("card_kind", Integer, nullable=False), Column("card_id", Integer, nullable=False), Column("chara_id", Integer, nullable=False), @@ -33,54 +41,70 @@ card = Table( Column("start_date", String(255), nullable=False), Column("end_date", String(255), nullable=False), UniqueConstraint("user", "card_kind", "card_id", name="mai2_item_card_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) item = Table( "mai2_item_item", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("item_kind", Integer, nullable=False), Column("item_id", Integer, nullable=False), Column("stock", Integer, nullable=False, server_default="1"), Column("is_valid", Boolean, nullable=False, server_default="1"), UniqueConstraint("user", "item_kind", "item_id", name="mai2_item_item_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) map = Table( "mai2_item_map", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("map_id", Integer, nullable=False), Column("distance", Integer, nullable=False), Column("is_lock", Boolean, nullable=False, server_default="0"), Column("is_clear", Boolean, nullable=False, server_default="0"), Column("is_complete", Boolean, nullable=False, server_default="0"), UniqueConstraint("user", "map_id", name="mai2_item_map_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) login_bonus = Table( "mai2_item_login_bonus", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("bonus_id", Integer, nullable=False), Column("point", Integer, nullable=False), Column("is_current", Boolean, nullable=False, server_default="0"), Column("is_complete", Boolean, nullable=False, server_default="0"), UniqueConstraint("user", "bonus_id", name="mai2_item_login_bonus_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) friend_season_ranking = Table( "mai2_item_friend_season_ranking", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("season_id", Integer, nullable=False), Column("point", Integer, nullable=False), Column("rank", Integer, nullable=False), @@ -88,35 +112,46 @@ friend_season_ranking = Table( Column("user_name", String(8), nullable=False), Column("record_date", String(255), nullable=False), UniqueConstraint("user", "season_id", "user_name", name="mai2_item_login_bonus_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) favorite = Table( "mai2_item_favorite", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("itemKind", Integer, nullable=False), Column("itemIdList", JSON), UniqueConstraint("user", "itemKind", name="mai2_item_favorite_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) charge = Table( "mai2_item_charge", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("charge_id", Integer, nullable=False), Column("stock", Integer, nullable=False), Column("purchase_date", String(255), nullable=False), Column("valid_date", String(255), nullable=False), UniqueConstraint("user", "charge_id", name="mai2_item_charge_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) + class Mai2ItemData(BaseData): - def put_item(self, user_id: int, item_kind: int, item_id: int, stock: int, is_valid: bool) -> None: + def put_item( + self, user_id: int, item_kind: int, item_id: int, stock: int, is_valid: bool + ) -> None: sql = insert(item).values( user=user_id, item_kind=item_kind, @@ -132,28 +167,47 @@ class Mai2ItemData(BaseData): result = self.execute(conflict) if result is None: - self.logger.warn(f"put_item: failed to insert item! user_id: {user_id}, item_kind: {item_kind}, item_id: {item_id}") + self.logger.warn( + f"put_item: failed to insert item! user_id: {user_id}, item_kind: {item_kind}, item_id: {item_id}" + ) return None return result.lastrowid - + def get_items(self, user_id: int, item_kind: int = None) -> Optional[List[Row]]: if item_kind is None: sql = item.select(item.c.user == user_id) else: - sql = item.select(and_(item.c.user == user_id, item.c.item_kind == item_kind)) + sql = item.select( + and_(item.c.user == user_id, item.c.item_kind == item_kind) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - + def get_item(self, user_id: int, item_kind: int, item_id: int) -> Optional[Row]: - sql = item.select(and_(item.c.user == user_id, item.c.item_kind == item_kind, item.c.item_id == item_id)) + sql = item.select( + and_( + item.c.user == user_id, + item.c.item_kind == item_kind, + item.c.item_id == item_id, + ) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() - - def put_login_bonus(self, user_id: int, bonus_id: int, point: int, is_current: bool, is_complete: bool) -> None: + + def put_login_bonus( + self, + user_id: int, + bonus_id: int, + point: int, + is_current: bool, + is_complete: bool, + ) -> None: sql = insert(login_bonus).values( user=user_id, bonus_id=bonus_id, @@ -170,25 +224,39 @@ class Mai2ItemData(BaseData): result = self.execute(conflict) if result is None: - self.logger.warn(f"put_login_bonus: failed to insert item! user_id: {user_id}, bonus_id: {bonus_id}, point: {point}") + self.logger.warn( + f"put_login_bonus: failed to insert item! user_id: {user_id}, bonus_id: {bonus_id}, point: {point}" + ) return None return result.lastrowid - + def get_login_bonuses(self, user_id: int) -> Optional[List[Row]]: sql = login_bonus.select(login_bonus.c.user == user_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - + def get_login_bonus(self, user_id: int, bonus_id: int) -> Optional[Row]: - sql = login_bonus.select(and_(login_bonus.c.user == user_id, login_bonus.c.bonus_id == bonus_id)) + sql = login_bonus.select( + and_(login_bonus.c.user == user_id, login_bonus.c.bonus_id == bonus_id) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() - def put_map(self, user_id: int, map_id: int, distance: int, is_lock: bool, is_clear: bool, is_complete: bool) -> None: + def put_map( + self, + user_id: int, + map_id: int, + distance: int, + is_lock: bool, + is_clear: bool, + is_complete: bool, + ) -> None: sql = insert(map).values( user=user_id, map_id=map_id, @@ -207,25 +275,36 @@ class Mai2ItemData(BaseData): result = self.execute(conflict) if result is None: - self.logger.warn(f"put_map: failed to insert item! user_id: {user_id}, map_id: {map_id}, distance: {distance}") + self.logger.warn( + f"put_map: failed to insert item! user_id: {user_id}, map_id: {map_id}, distance: {distance}" + ) return None return result.lastrowid - + def get_maps(self, user_id: int) -> Optional[List[Row]]: sql = map.select(map.c.user == user_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - + def get_map(self, user_id: int, map_id: int) -> Optional[Row]: sql = map.select(and_(map.c.user == user_id, map.c.map_id == map_id)) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() - def put_character(self, user_id: int, character_id: int, level: int, awakening: int, use_count: int) -> None: + def put_character( + self, + user_id: int, + character_id: int, + level: int, + awakening: int, + use_count: int, + ) -> None: sql = insert(character).values( user=user_id, character_id=character_id, @@ -242,57 +321,64 @@ class Mai2ItemData(BaseData): result = self.execute(conflict) if result is None: - self.logger.warn(f"put_character: failed to insert item! user_id: {user_id}, character_id: {character_id}, level: {level}") + self.logger.warn( + f"put_character: failed to insert item! user_id: {user_id}, character_id: {character_id}, level: {level}" + ) return None return result.lastrowid - + def get_characters(self, user_id: int) -> Optional[List[Row]]: sql = character.select(character.c.user == user_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - + def get_character(self, user_id: int, character_id: int) -> Optional[Row]: - sql = character.select(and_(character.c.user == user_id, character.c.character_id == character_id)) + sql = character.select( + and_(character.c.user == user_id, character.c.character_id == character_id) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() - + def get_friend_season_ranking(self, user_id: int) -> Optional[Row]: sql = friend_season_ranking.select(friend_season_ranking.c.user == user_id) result = self.execute(sql) - if result is None:return None + if result is None: + return None return result.fetchone() - - def put_favorite(self, user_id: int, kind: int, item_id_list: List[int]) -> Optional[int]: + + def put_favorite( + self, user_id: int, kind: int, item_id_list: List[int] + ) -> Optional[int]: sql = insert(favorite).values( - user=user_id, - kind=kind, - item_id_list=item_id_list + user=user_id, kind=kind, item_id_list=item_id_list ) - conflict = sql.on_duplicate_key_update( - item_id_list=item_id_list - ) + conflict = sql.on_duplicate_key_update(item_id_list=item_id_list) result = self.execute(conflict) if result is None: - self.logger.warn(f"put_favorite: failed to insert item! user_id: {user_id}, kind: {kind}") + self.logger.warn( + f"put_favorite: failed to insert item! user_id: {user_id}, kind: {kind}" + ) return None return result.lastrowid - + def get_favorites(self, user_id: int, kind: int = None) -> Optional[Row]: if kind is None: sql = favorite.select(favorite.c.user == user_id) else: - sql = favorite.select(and_( - favorite.c.user == user_id, - favorite.c.itemKind == kind - )) + sql = favorite.select( + and_(favorite.c.user == user_id, favorite.c.itemKind == kind) + ) result = self.execute(sql) - if result is None:return None - return result.fetchall() \ No newline at end of file + if result is None: + return None + return result.fetchall() diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index 346645c..335a731 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -14,7 +14,11 @@ detail = Table( "mai2_profile_detail", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("version", Integer, nullable=False), Column("userName", String(25)), Column("isNetMember", Integer), @@ -41,9 +45,9 @@ detail = Table( Column("lastRomVersion", String(25)), Column("lastDataVersion", String(25)), Column("lastLoginDate", String(25)), - Column("lastPairLoginDate", String(25)), # new with uni+ + Column("lastPairLoginDate", String(25)), # new with uni+ Column("lastPlayDate", String(25)), - Column("lastTrialPlayDate", String(25)), # new with uni+ + Column("lastTrialPlayDate", String(25)), # new with uni+ Column("lastPlayCredit", Integer), Column("lastPlayMode", Integer), Column("lastPlaceId", Integer), @@ -90,16 +94,20 @@ detail = Table( Column("playerOldRating", BigInteger), Column("playerNewRating", BigInteger), Column("dateTime", BigInteger), - Column("banState", Integer), # new with uni+ + Column("banState", Integer), # new with uni+ UniqueConstraint("user", "version", name="mai2_profile_detail_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) ghost = Table( "mai2_profile_ghost", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("version_int", Integer, nullable=False), Column("name", String(25)), Column("iconId", Integer), @@ -120,15 +128,21 @@ ghost = Table( Column("resultBitList", JSON), Column("resultNum", Integer), Column("achievement", Integer), - UniqueConstraint("user", "version", "musicId", "difficulty", name="mai2_profile_ghost_uk"), - mysql_charset='utf8mb4' + UniqueConstraint( + "user", "version", "musicId", "difficulty", name="mai2_profile_ghost_uk" + ), + mysql_charset="utf8mb4", ) extend = Table( "mai2_profile_extend", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("version", Integer, nullable=False), Column("selectMusicId", Integer), Column("selectDifficultyId", Integer), @@ -145,14 +159,18 @@ extend = Table( Column("selectedCardList", JSON), Column("encountMapNpcList", JSON), UniqueConstraint("user", "version", name="mai2_profile_extend_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) option = Table( "mai2_profile_option", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("version", Integer, nullable=False), Column("selectMusicId", Integer), Column("optionKind", Integer), @@ -200,14 +218,18 @@ option = Table( Column("sortTab", Integer), Column("sortMusic", Integer), UniqueConstraint("user", "version", name="mai2_profile_option_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) rating = Table( "mai2_profile_rating", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("version", Integer, nullable=False), Column("rating", Integer), Column("ratingList", JSON), @@ -216,26 +238,34 @@ rating = Table( Column("nextNewRatingList", JSON), Column("udemae", JSON), UniqueConstraint("user", "version", name="mai2_profile_rating_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) region = Table( "mai2_profile_region", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("regionId", Integer), Column("playCount", Integer, server_default="1"), Column("created", String(25)), UniqueConstraint("user", "regionId", name="mai2_profile_region_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) activity = Table( "mai2_profile_activity", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("kind", Integer, nullable=False), Column("activityId", Integer, nullable=False), Column("param1", Integer, nullable=False), @@ -244,11 +274,14 @@ activity = Table( Column("param4", Integer, nullable=False), Column("sortNumber", Integer, nullable=False), UniqueConstraint("user", "kind", "activityId", name="mai2_profile_activity_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) + class Mai2ProfileData(BaseData): - def put_profile_detail(self, user_id: int, version: int, detail_data: Dict) -> Optional[Row]: + def put_profile_detail( + self, user_id: int, version: int, detail_data: Dict + ) -> Optional[Row]: detail_data["user"] = user_id detail_data["version"] = version sql = insert(detail).values(**detail_data) @@ -257,18 +290,25 @@ class Mai2ProfileData(BaseData): result = self.execute(conflict) if result is None: - self.logger.warn(f"put_profile: Failed to create profile! user_id {user_id}") + self.logger.warn( + f"put_profile: Failed to create profile! user_id {user_id}" + ) return None return result.lastrowid def get_profile_detail(self, user_id: int, version: int) -> Optional[Row]: - sql = select(detail).where(and_(detail.c.user == user_id, detail.c.version == version)) + sql = select(detail).where( + and_(detail.c.user == user_id, detail.c.version == version) + ) result = self.execute(sql) - if result is None:return None + if result is None: + return None return result.fetchone() - def put_profile_ghost(self, user_id: int, version: int, ghost_data: Dict) -> Optional[int]: + def put_profile_ghost( + self, user_id: int, version: int, ghost_data: Dict + ) -> Optional[int]: ghost_data["user"] = user_id ghost_data["version_int"] = version @@ -282,13 +322,18 @@ class Mai2ProfileData(BaseData): return result.lastrowid def get_profile_ghost(self, user_id: int, version: int) -> Optional[Row]: - sql = select(ghost).where(and_(ghost.c.user == user_id, ghost.c.version_int == version)) + sql = select(ghost).where( + and_(ghost.c.user == user_id, ghost.c.version_int == version) + ) result = self.execute(sql) - if result is None:return None + if result is None: + return None return result.fetchone() - def put_profile_extend(self, user_id: int, version: int, extend_data: Dict) -> Optional[int]: + def put_profile_extend( + self, user_id: int, version: int, extend_data: Dict + ) -> Optional[int]: extend_data["user"] = user_id extend_data["version"] = version @@ -302,13 +347,18 @@ class Mai2ProfileData(BaseData): return result.lastrowid def get_profile_extend(self, user_id: int, version: int) -> Optional[Row]: - sql = select(extend).where(and_(extend.c.user == user_id, extend.c.version == version)) + sql = select(extend).where( + and_(extend.c.user == user_id, extend.c.version == version) + ) result = self.execute(sql) - if result is None:return None + if result is None: + return None return result.fetchone() - def put_profile_option(self, user_id: int, version: int, option_data: Dict) -> Optional[int]: + def put_profile_option( + self, user_id: int, version: int, option_data: Dict + ) -> Optional[int]: option_data["user"] = user_id option_data["version"] = version @@ -322,13 +372,18 @@ class Mai2ProfileData(BaseData): return result.lastrowid def get_profile_option(self, user_id: int, version: int) -> Optional[Row]: - sql = select(option).where(and_(option.c.user == user_id, option.c.version == version)) + sql = select(option).where( + and_(option.c.user == user_id, option.c.version == version) + ) result = self.execute(sql) - if result is None:return None + if result is None: + return None return result.fetchone() - - def put_profile_rating(self, user_id: int, version: int, rating_data: Dict) -> Optional[int]: + + def put_profile_rating( + self, user_id: int, version: int, rating_data: Dict + ) -> Optional[int]: rating_data["user"] = user_id rating_data["version"] = version @@ -342,23 +397,24 @@ class Mai2ProfileData(BaseData): return result.lastrowid def get_profile_rating(self, user_id: int, version: int) -> Optional[Row]: - sql = select(rating).where(and_(rating.c.user == user_id, rating.c.version == version)) + sql = select(rating).where( + and_(rating.c.user == user_id, rating.c.version == version) + ) result = self.execute(sql) - if result is None:return None + if result is None: + return None return result.fetchone() def put_profile_region(self, user_id: int, region_id: int) -> Optional[int]: sql = insert(region).values( - user = user_id, - regionId = region_id, - created = datetime.strftime(datetime.now(), Mai2Constants.DATE_TIME_FORMAT) - ) - - conflict = sql.on_duplicate_key_update( - playCount = region.c.playCount + 1 + user=user_id, + regionId=region_id, + created=datetime.strftime(datetime.now(), Mai2Constants.DATE_TIME_FORMAT), ) + conflict = sql.on_duplicate_key_update(playCount=region.c.playCount + 1) + result = self.execute(conflict) if result is None: self.logger.warn(f"put_region: failed to update! {user_id}") @@ -369,34 +425,38 @@ class Mai2ProfileData(BaseData): sql = select(region).where(region.c.user == user_id) result = self.execute(sql) - if result is None:return None + if result is None: + return None return result.fetchall() - + def put_profile_activity(self, user_id: int, activity_data: Dict) -> Optional[int]: if "id" in activity_data: activity_data["activityId"] = activity_data["id"] activity_data.pop("id") - + activity_data["user"] = user_id - + sql = insert(activity).values(**activity_data) conflict = sql.on_duplicate_key_update(**activity_data) result = self.execute(conflict) if result is None: - self.logger.warn(f"put_profile_activity: failed to update! user_id: {user_id}") + self.logger.warn( + f"put_profile_activity: failed to update! user_id: {user_id}" + ) return None return result.lastrowid def get_profile_activity(self, user_id: int, kind: int = None) -> Optional[Row]: sql = activity.select( and_( - activity.c.user == user_id, + activity.c.user == user_id, (activity.c.kind == kind) if kind is not None else True, ) ) result = self.execute(sql) - if result is None:return None + if result is None: + return None return result.fetchone() diff --git a/titles/mai2/schema/score.py b/titles/mai2/schema/score.py index 59a600c..15bf519 100644 --- a/titles/mai2/schema/score.py +++ b/titles/mai2/schema/score.py @@ -12,7 +12,11 @@ best_score = Table( "mai2_score_best", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("musicId", Integer), Column("level", Integer), Column("playCount", Integer), @@ -22,14 +26,18 @@ best_score = Table( Column("deluxscoreMax", Integer), Column("scoreRank", Integer), UniqueConstraint("user", "musicId", "level", name="mai2_score_best_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) playlog = Table( "mai2_playlog", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("userId", BigInteger), Column("orderId", Integer), Column("playlogId", BigInteger), @@ -136,14 +144,18 @@ playlog = Table( Column("extNum1", Integer), Column("extNum2", Integer), Column("trialPlayAchievement", Integer), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) course = Table( "mai2_score_course", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("courseId", Integer), Column("isLastClear", Boolean), Column("totalRestlife", Integer), @@ -157,9 +169,10 @@ course = Table( Column("bestDeluxscore", Integer), Column("bestDeluxscoreDate", String(25)), UniqueConstraint("user", "courseId", name="mai2_score_best_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) + class Mai2ScoreData(BaseData): def put_best_score(self, user_id: int, score_data: Dict) -> Optional[int]: score_data["user"] = user_id @@ -169,33 +182,39 @@ class Mai2ScoreData(BaseData): result = self.execute(conflict) if result is None: - self.logger.error(f"put_best_score: Failed to insert best score! user_id {user_id}") + self.logger.error( + f"put_best_score: Failed to insert best score! user_id {user_id}" + ) return None return result.lastrowid def get_best_scores(self, user_id: int, song_id: int = None) -> Optional[List[Row]]: sql = best_score.select( and_( - best_score.c.user == user_id, - (best_score.c.song_id == song_id) if song_id is not None else True - ) + best_score.c.user == user_id, + (best_score.c.song_id == song_id) if song_id is not None else True, ) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - - def get_best_score(self, user_id: int, song_id: int, chart_id: int) -> Optional[Row]: + + def get_best_score( + self, user_id: int, song_id: int, chart_id: int + ) -> Optional[Row]: sql = best_score.select( and_( - best_score.c.user == user_id, - best_score.c.song_id == song_id, - best_score.c.chart_id == chart_id - ) + best_score.c.user == user_id, + best_score.c.song_id == song_id, + best_score.c.chart_id == chart_id, ) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() def put_playlog(self, user_id: int, playlog_data: Dict) -> Optional[int]: @@ -209,7 +228,7 @@ class Mai2ScoreData(BaseData): self.logger.error(f"put_playlog: Failed to insert! user_id {user_id}") return None return result.lastrowid - + def put_course(self, user_id: int, course_data: Dict) -> Optional[int]: course_data["user"] = user_id sql = insert(course).values(**course_data) @@ -221,10 +240,11 @@ class Mai2ScoreData(BaseData): self.logger.error(f"put_course: Failed to insert! user_id {user_id}") return None return result.lastrowid - + def get_courses(self, user_id: int) -> Optional[List[Row]]: sql = course.select(best_score.c.user == user_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() diff --git a/titles/mai2/schema/static.py b/titles/mai2/schema/static.py index 733e2ef..2908a47 100644 --- a/titles/mai2/schema/static.py +++ b/titles/mai2/schema/static.py @@ -12,20 +12,20 @@ event = Table( "mai2_static_event", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("version", Integer,nullable=False), + Column("version", Integer, nullable=False), Column("eventId", Integer), Column("type", Integer), Column("name", String(255)), Column("enabled", Boolean, server_default="1"), UniqueConstraint("version", "eventId", "type", name="mai2_static_event_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) music = Table( "mai2_static_music", metadata, - Column("id", Integer, primary_key=True, nullable=False), - Column("version", Integer,nullable=False), + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), Column("songId", Integer), Column("chartId", Integer), Column("title", String(255)), @@ -36,39 +36,42 @@ music = Table( Column("difficulty", Float), Column("noteDesigner", String(255)), UniqueConstraint("songId", "chartId", "version", name="mai2_static_music_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) ticket = Table( "mai2_static_ticket", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("version", Integer,nullable=False), + Column("version", Integer, nullable=False), Column("ticketId", Integer), Column("kind", Integer), Column("name", String(255)), Column("price", Integer, server_default="1"), Column("enabled", Boolean, server_default="1"), - UniqueConstraint("version","ticketId", name="mai2_static_ticket_uk"), - mysql_charset='utf8mb4' + UniqueConstraint("version", "ticketId", name="mai2_static_ticket_uk"), + mysql_charset="utf8mb4", ) + class Mai2StaticData(BaseData): - def put_game_event(self, version: int, type: int, event_id: int, name: str) -> Optional[int]: + def put_game_event( + self, version: int, type: int, event_id: int, name: str + ) -> Optional[int]: sql = insert(event).values( - version = version, - type = type, - eventId = event_id, - name = name, + version=version, + type=type, + eventId=event_id, + name=name, ) - conflict = sql.on_duplicate_key_update( - eventId = event_id - ) + conflict = sql.on_duplicate_key_update(eventId=event_id) result = self.execute(conflict) if result is None: - self.logger.warning(f"put_game_event: Failed to insert event! event_id {event_id} type {type} name {name}") + self.logger.warning( + f"put_game_event: Failed to insert event! event_id {event_id} type {type} name {name}" + ) return result.lastrowid def get_game_events(self, version: int) -> Optional[List[Row]]: @@ -78,50 +81,65 @@ class Mai2StaticData(BaseData): if result is None: return None return result.fetchall() - + def get_enabled_events(self, version: int) -> Optional[List[Row]]: - sql = select(event).where(and_( - event.c.version == version, - event.c.enabled == True - )) - - result = self.execute(sql) - if result is None: return None - return result.fetchall() - - def toggle_game_events(self, version: int, event_id: int, toggle: bool) -> Optional[List]: - sql = event.update(and_(event.c.version == version, event.c.event_id == event_id)).values( - enabled = int(toggle) + sql = select(event).where( + and_(event.c.version == version, event.c.enabled == True) ) result = self.execute(sql) if result is None: - self.logger.warning(f"toggle_game_events: Failed to update event! event_id {event_id} toggle {toggle}") + return None + return result.fetchall() + + def toggle_game_events( + self, version: int, event_id: int, toggle: bool + ) -> Optional[List]: + sql = event.update( + and_(event.c.version == version, event.c.event_id == event_id) + ).values(enabled=int(toggle)) + + result = self.execute(sql) + if result is None: + self.logger.warning( + f"toggle_game_events: Failed to update event! event_id {event_id} toggle {toggle}" + ) return result.last_updated_params() - - def put_game_music(self, version: int, song_id: int, chart_id: int, title: str, artist: str, - genre: str, bpm: str, added_version: str, difficulty: float, note_designer: str) -> None: + + def put_game_music( + self, + version: int, + song_id: int, + chart_id: int, + title: str, + artist: str, + genre: str, + bpm: str, + added_version: str, + difficulty: float, + note_designer: str, + ) -> None: sql = insert(music).values( - version = version, - songId = song_id, - chartId = chart_id, - title = title, - artist = artist, - genre = genre, - bpm = bpm, - addedVersion = added_version, - difficulty = difficulty, - noteDesigner = note_designer, + version=version, + songId=song_id, + chartId=chart_id, + title=title, + artist=artist, + genre=genre, + bpm=bpm, + addedVersion=added_version, + difficulty=difficulty, + noteDesigner=note_designer, ) conflict = sql.on_duplicate_key_update( - title = title, - artist = artist, - genre = genre, - bpm = bpm, - addedVersion = added_version, - difficulty = difficulty, - noteDesigner = note_designer, + title=title, + artist=artist, + genre=genre, + bpm=bpm, + addedVersion=added_version, + difficulty=difficulty, + noteDesigner=note_designer, ) result = self.execute(conflict) @@ -129,50 +147,64 @@ class Mai2StaticData(BaseData): self.logger.warn(f"Failed to insert song {song_id} chart {chart_id}") return None return result.lastrowid - - def put_game_ticket(self, version: int, ticket_id: int, ticket_type: int, ticket_price: int, name: str) -> Optional[int]: + + def put_game_ticket( + self, + version: int, + ticket_id: int, + ticket_type: int, + ticket_price: int, + name: str, + ) -> Optional[int]: sql = insert(ticket).values( - version = version, - ticketId = ticket_id, - kind = ticket_type, - price = ticket_price, - name = name + version=version, + ticketId=ticket_id, + kind=ticket_type, + price=ticket_price, + name=name, ) - - conflict = sql.on_duplicate_key_update( - price = ticket_price - ) + + conflict = sql.on_duplicate_key_update(price=ticket_price) result = self.execute(conflict) if result is None: self.logger.warn(f"Failed to insert charge {ticket_id} type {ticket_type}") return None return result.lastrowid - - def get_enabled_tickets(self, version: int, kind: int = None) -> Optional[List[Row]]: + + def get_enabled_tickets( + self, version: int, kind: int = None + ) -> Optional[List[Row]]: if kind is not None: - sql = select(ticket).where(and_( - ticket.c.version == version, - ticket.c.enabled == True, - ticket.c.kind == kind - )) + sql = select(ticket).where( + and_( + ticket.c.version == version, + ticket.c.enabled == True, + ticket.c.kind == kind, + ) + ) else: - sql = select(ticket).where(and_( - ticket.c.version == version, - ticket.c.enabled == True - )) + sql = select(ticket).where( + and_(ticket.c.version == version, ticket.c.enabled == True) + ) result = self.execute(sql) - if result is None:return None + if result is None: + return None return result.fetchall() - def get_music_chart(self, version: int, song_id: int, chart_id: int) -> Optional[List[Row]]: - sql = select(music).where(and_( - music.c.version == version, - music.c.songId == song_id, - music.c.chartId == chart_id - )) + def get_music_chart( + self, version: int, song_id: int, chart_id: int + ) -> Optional[List[Row]]: + sql = select(music).where( + and_( + music.c.version == version, + music.c.songId == song_id, + music.c.chartId == chart_id, + ) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() diff --git a/titles/mai2/splash.py b/titles/mai2/splash.py index 690645b..ad31695 100644 --- a/titles/mai2/splash.py +++ b/titles/mai2/splash.py @@ -8,7 +8,8 @@ from titles.mai2.base import Mai2Base from titles.mai2.config import Mai2Config from titles.mai2.const import Mai2Constants + class Mai2Splash(Mai2Base): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) - self.version = Mai2Constants.VER_MAIMAI_DX_SPLASH \ No newline at end of file + self.version = Mai2Constants.VER_MAIMAI_DX_SPLASH diff --git a/titles/mai2/splashplus.py b/titles/mai2/splashplus.py index eb6f940..54431c9 100644 --- a/titles/mai2/splashplus.py +++ b/titles/mai2/splashplus.py @@ -8,7 +8,8 @@ from titles.mai2.base import Mai2Base from titles.mai2.config import Mai2Config from titles.mai2.const import Mai2Constants + class Mai2SplashPlus(Mai2Base): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) - self.version = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS \ No newline at end of file + self.version = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS diff --git a/titles/mai2/universe.py b/titles/mai2/universe.py index be6472c..56c2d3f 100644 --- a/titles/mai2/universe.py +++ b/titles/mai2/universe.py @@ -8,7 +8,8 @@ from titles.mai2.base import Mai2Base from titles.mai2.const import Mai2Constants from titles.mai2.config import Mai2Config + class Mai2Universe(Mai2Base): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) - self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE \ No newline at end of file + self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE diff --git a/titles/mai2/universeplus.py b/titles/mai2/universeplus.py index 795206e..977fce9 100644 --- a/titles/mai2/universeplus.py +++ b/titles/mai2/universeplus.py @@ -8,7 +8,8 @@ from titles.mai2.base import Mai2Base from titles.mai2.const import Mai2Constants from titles.mai2.config import Mai2Config + class Mai2UniversePlus(Mai2Base): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) - self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS \ No newline at end of file + self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS diff --git a/titles/ongeki/__init__.py b/titles/ongeki/__init__.py index 7f03a8f..ddde049 100644 --- a/titles/ongeki/__init__.py +++ b/titles/ongeki/__init__.py @@ -7,4 +7,4 @@ index = OngekiServlet database = OngekiData reader = OngekiReader game_codes = [OngekiConstants.GAME_CODE] -current_schema_version = 2 \ No newline at end of file +current_schema_version = 2 diff --git a/titles/ongeki/base.py b/titles/ongeki/base.py index 3f6dc7a..4f7619c 100644 --- a/titles/ongeki/base.py +++ b/titles/ongeki/base.py @@ -11,6 +11,7 @@ from titles.ongeki.config import OngekiConfig from titles.ongeki.database import OngekiData from titles.ongeki.config import OngekiConfig + class OngekiBattleGrade(Enum): FAILED = 0 DRAW = 1 @@ -21,6 +22,7 @@ class OngekiBattleGrade(Enum): UNBELIEVABLE_GOLD = 6 UNBELIEVABLE_RAINBOW = 7 + class OngekiBattlePointGrade(Enum): FRESHMAN = 0 KYU10 = 1 @@ -45,20 +47,22 @@ class OngekiBattlePointGrade(Enum): DAN10 = 20 SODEN = 21 + class OngekiTechnicalGrade(Enum): - D = 0 - C = 1 - B = 2 - BB = 3 - BBB = 4 - A = 5 - AA = 6 - AAA = 7 - S = 8 - SS = 9 - SSS = 10 + D = 0 + C = 1 + B = 2 + BB = 3 + BBB = 4 + A = 5 + AA = 6 + AAA = 7 + S = 8 + SS = 9 + SSS = 10 SSSp = 11 + class OngekiDifficulty(Enum): BASIC = 0 ADVANCED = 1 @@ -66,6 +70,7 @@ class OngekiDifficulty(Enum): MASTER = 3 LUNATIC = 10 + class OngekiGPLogKind(Enum): NONE = 0 BUY1_START = 1 @@ -82,22 +87,28 @@ class OngekiGPLogKind(Enum): PAY_MAS_UNLOCK = 13 PAY_MONEY = 14 -class OngekiBase(): +class OngekiBase: def __init__(self, core_cfg: CoreConfig, game_cfg: OngekiConfig) -> None: self.core_cfg = core_cfg self.game_cfg = game_cfg self.data = OngekiData(core_cfg) self.date_time_format = "%Y-%m-%d %H:%M:%S" - self.date_time_format_ext = "%Y-%m-%d %H:%M:%S.%f" # needs to be lopped off at [:-5] + self.date_time_format_ext = ( + "%Y-%m-%d %H:%M:%S.%f" # needs to be lopped off at [:-5] + ) self.date_time_format_short = "%Y-%m-%d" self.logger = logging.getLogger("ongeki") self.game = OngekiConstants.GAME_CODE self.version = OngekiConstants.VER_ONGEKI def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - reboot_start = date.strftime(datetime.now() + timedelta(hours=3), self.date_time_format) - reboot_end = date.strftime(datetime.now() + timedelta(hours=4), self.date_time_format) + reboot_start = date.strftime( + datetime.now() + timedelta(hours=3), self.date_time_format + ) + reboot_end = date.strftime( + datetime.now() + timedelta(hours=4), self.date_time_format + ) return { "gameSetting": { "dataVersion": "1.00.00", @@ -128,20 +139,53 @@ class OngekiBase(): def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: return {"length": 0, "gameRankingList": []} - + def handle_get_game_point_api_request(self, data: Dict) -> Dict: """ Sets the GP ammount for A and B sets for 1 - 3 crdits """ - return {"length":6,"gamePointList":[ - {"type":0,"cost":100,"startDate":"2000-01-01 05:00:00.0","endDate":"2099-01-01 05:00:00.0"}, - {"type":1,"cost":200,"startDate":"2000-01-01 05:00:00.0","endDate":"2099-01-01 05:00:00.0"}, - {"type":2,"cost":300,"startDate":"2000-01-01 05:00:00.0","endDate":"2099-01-01 05:00:00.0"}, - {"type":3,"cost":120,"startDate":"2000-01-01 05:00:00.0","endDate":"2099-01-01 05:00:00.0"}, - {"type":4,"cost":240,"startDate":"2000-01-01 05:00:00.0","endDate":"2099-01-01 05:00:00.0"}, - {"type":5,"cost":360,"startDate":"2000-01-01 05:00:00.0","endDate":"2099-01-01 05:00:00.0"} - ]} - + return { + "length": 6, + "gamePointList": [ + { + "type": 0, + "cost": 100, + "startDate": "2000-01-01 05:00:00.0", + "endDate": "2099-01-01 05:00:00.0", + }, + { + "type": 1, + "cost": 200, + "startDate": "2000-01-01 05:00:00.0", + "endDate": "2099-01-01 05:00:00.0", + }, + { + "type": 2, + "cost": 300, + "startDate": "2000-01-01 05:00:00.0", + "endDate": "2099-01-01 05:00:00.0", + }, + { + "type": 3, + "cost": 120, + "startDate": "2000-01-01 05:00:00.0", + "endDate": "2099-01-01 05:00:00.0", + }, + { + "type": 4, + "cost": 240, + "startDate": "2000-01-01 05:00:00.0", + "endDate": "2099-01-01 05:00:00.0", + }, + { + "type": 5, + "cost": 360, + "startDate": "2000-01-01 05:00:00.0", + "endDate": "2099-01-01 05:00:00.0", + }, + ], + } + def handle_game_login_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "gameLogin"} @@ -184,11 +228,19 @@ class OngekiBase(): def handle_upsert_user_gplog_api_request(self, data: Dict) -> Dict: user = data["userId"] - if user >= 200000000000000: # Account for guest play + if user >= 200000000000000: # Account for guest play user = None - self.data.log.put_gp_log(user, data["usedCredit"], data["placeName"], data["userGplog"]["trxnDate"], - data["userGplog"]["placeId"], data["userGplog"]["kind"], data["userGplog"]["pattern"], data["userGplog"]["currentGP"]) + self.data.log.put_gp_log( + user, + data["usedCredit"], + data["placeName"], + data["userGplog"]["trxnDate"], + data["userGplog"]["placeId"], + data["userGplog"]["kind"], + data["userGplog"]["pattern"], + data["userGplog"]["currentGP"], + ) return {"returnCode": 1, "apiName": "UpsertUserGplogApi"} @@ -197,39 +249,53 @@ class OngekiBase(): def handle_get_game_event_api_request(self, data: Dict) -> Dict: evts = self.data.static.get_enabled_events(self.version) - + evt_list = [] for event in evts: - evt_list.append({ - "type": event["type"], - "id": event["eventId"], - "startDate": "2017-12-05 07:00:00.0", - "endDate": "2099-12-31 00:00:00.0" - }) - - return {"type": data["type"], "length": len(evt_list), "gameEventList": evt_list} + evt_list.append( + { + "type": event["type"], + "id": event["eventId"], + "startDate": "2017-12-05 07:00:00.0", + "endDate": "2099-12-31 00:00:00.0", + } + ) + + return { + "type": data["type"], + "length": len(evt_list), + "gameEventList": evt_list, + } def handle_get_game_id_list_api_request(self, data: Dict) -> Dict: - game_idlist: list[str, Any] = [] #1 to 230 & 8000 to 8050 - + game_idlist: list[str, Any] = [] # 1 to 230 & 8000 to 8050 + if data["type"] == 1: - for i in range(1,231): + for i in range(1, 231): game_idlist.append({"type": 1, "id": i}) - return {"type": data["type"], "length": len(game_idlist), "gameIdlistList": game_idlist} + return { + "type": data["type"], + "length": len(game_idlist), + "gameIdlistList": game_idlist, + } elif data["type"] == 2: - for i in range(8000,8051): + for i in range(8000, 8051): game_idlist.append({"type": 2, "id": i}) - return {"type": data["type"], "length": len(game_idlist), "gameIdlistList": game_idlist} + return { + "type": data["type"], + "length": len(game_idlist), + "gameIdlistList": game_idlist, + } def handle_get_user_region_api_request(self, data: Dict) -> Dict: return {"userId": data["userId"], "length": 0, "userRegionList": []} def handle_get_user_preview_api_request(self, data: Dict) -> Dict: profile = self.data.profile.get_profile_preview(data["userId"], self.version) - - if profile is None: + + if profile is None: return { - "userId": data["userId"], + "userId": data["userId"], "isLogin": False, "lastLoginDate": "0000-00-00 00:00:00", "userName": "", @@ -240,12 +306,12 @@ class OngekiBase(): "lastGameId": "", "lastRomVersion": "", "lastDataVersion": "", - "lastPlayDate": "", + "lastPlayDate": "", "nameplateId": 0, - "trophyId": 0, - "cardId": 0, - "dispPlayerLv": 0, - "dispRating": 0, + "trophyId": 0, + "cardId": 0, + "dispPlayerLv": 0, + "dispRating": 0, "dispBP": 0, "headphone": 0, "banStatus": 0, @@ -253,7 +319,7 @@ class OngekiBase(): } return { - "userId": data["userId"], + "userId": data["userId"], "isLogin": False, "lastLoginDate": profile["lastPlayDate"], "userName": profile["userName"], @@ -264,12 +330,12 @@ class OngekiBase(): "lastGameId": profile["lastGameId"], "lastRomVersion": profile["lastRomVersion"], "lastDataVersion": profile["lastDataVersion"], - "lastPlayDate": profile["lastPlayDate"], + "lastPlayDate": profile["lastPlayDate"], "nameplateId": profile["nameplateId"], - "trophyId": profile["trophyId"], - "cardId": profile["cardId"], - "dispPlayerLv": profile["dispPlayerLv"], - "dispRating": profile["dispRating"], + "trophyId": profile["trophyId"], + "cardId": profile["cardId"], + "dispPlayerLv": profile["dispPlayerLv"], + "dispRating": profile["dispRating"], "dispBP": profile["dispBP"], "headphone": profile["headphone"], "banStatus": profile["banStatus"], @@ -297,7 +363,8 @@ class OngekiBase(): def handle_get_user_tech_event_api_request(self, data: Dict) -> Dict: user_tech_event_list = self.data.item.get_tech_event(data["userId"]) - if user_tech_event_list is None: return {} + if user_tech_event_list is None: + return {} tech_evt = [] for evt in user_tech_event_list: @@ -313,11 +380,11 @@ class OngekiBase(): } def handle_get_user_tech_event_ranking_api_request(self, data: Dict) -> Dict: - #user_event_ranking_list = self.data.item.get_tech_event_ranking(data["userId"]) - #if user_event_ranking_list is None: return {} + # user_event_ranking_list = self.data.item.get_tech_event_ranking(data["userId"]) + # if user_event_ranking_list is None: return {} evt_ranking = [] - #for evt in user_event_ranking_list: + # for evt in user_event_ranking_list: # tmp = evt._asdict() # tmp.pop("id") # tmp.pop("user") @@ -331,7 +398,8 @@ class OngekiBase(): def handle_get_user_kop_api_request(self, data: Dict) -> Dict: kop_list = self.data.profile.get_kop(data["userId"]) - if kop_list is None: return {} + if kop_list is None: + return {} for kop in kop_list: kop.pop("user") @@ -349,10 +417,10 @@ class OngekiBase(): next_idx = data["nextIndex"] start_idx = next_idx end_idx = max_ct + start_idx - + if len(song_list[start_idx:]) > max_ct: next_idx += max_ct - + else: next_idx = -1 @@ -360,15 +428,20 @@ class OngekiBase(): "userId": data["userId"], "length": len(song_list[start_idx:end_idx]), "nextIndex": next_idx, - "userMusicList": song_list[start_idx:end_idx] + "userMusicList": song_list[start_idx:end_idx], } def handle_get_user_item_api_request(self, data: Dict) -> Dict: kind = data["nextIndex"] / 10000000000 p = self.data.item.get_items(data["userId"], kind) - if p is None: - return {"userId": data["userId"], "nextIndex": -1, "itemKind": kind, "userItemList": []} + if p is None: + return { + "userId": data["userId"], + "nextIndex": -1, + "itemKind": kind, + "userItemList": [], + } items: list[Dict[str, Any]] = [] for i in range(data["nextIndex"] % 10000000000, len(p)): @@ -381,14 +454,23 @@ class OngekiBase(): xout = kind * 10000000000 + (data["nextIndex"] % 10000000000) + len(items) - if len(items) < data["maxCount"] or data["maxCount"] == 0: nextIndex = 0 - else: nextIndex = xout + if len(items) < data["maxCount"] or data["maxCount"] == 0: + nextIndex = 0 + else: + nextIndex = xout - return {"userId": data["userId"], "nextIndex": int(nextIndex), "itemKind": int(kind), "length": len(items), "userItemList": items} + return { + "userId": data["userId"], + "nextIndex": int(nextIndex), + "itemKind": int(kind), + "length": len(items), + "userItemList": items, + } def handle_get_user_option_api_request(self, data: Dict) -> Dict: o = self.data.profile.get_profile_options(data["userId"]) - if o is None: return {} + if o is None: + return {} # get the dict representation of the row so we can modify values user_opts = o._asdict() @@ -401,14 +483,17 @@ class OngekiBase(): def handle_get_user_data_api_request(self, data: Dict) -> Dict: p = self.data.profile.get_profile_data(data["userId"], self.version) - if p is None: return {} + if p is None: + return {} cards = self.data.card.get_user_cards(data["userId"]) if cards is None or len(cards) == 0: # This should never happen - self.logger.error(f"handle_get_user_data_api_request: Internal error - No cards found for user id {data['userId']}") + self.logger.error( + f"handle_get_user_data_api_request: Internal error - No cards found for user id {data['userId']}" + ) return {} - + # get the dict representation of the row so we can modify values user_data = p._asdict() @@ -422,14 +507,14 @@ class OngekiBase(): # add access code that we don't store user_data["accessCode"] = cards[0]["access_code"] - return {"userId": data["userId"], "userData":user_data} + return {"userId": data["userId"], "userData": user_data} def handle_get_user_event_ranking_api_request(self, data: Dict) -> Dict: - #user_event_ranking_list = self.data.item.get_event_ranking(data["userId"]) - #if user_event_ranking_list is None: return {} + # user_event_ranking_list = self.data.item.get_event_ranking(data["userId"]) + # if user_event_ranking_list is None: return {} evt_ranking = [] - #for evt in user_event_ranking_list: + # for evt in user_event_ranking_list: # tmp = evt._asdict() # tmp.pop("id") # tmp.pop("user") @@ -443,7 +528,8 @@ class OngekiBase(): def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: user_login_bonus_list = self.data.item.get_login_bonuses(data["userId"]) - if user_login_bonus_list is None: return {} + if user_login_bonus_list is None: + return {} login_bonuses = [] for scenerio in user_login_bonus_list: @@ -451,16 +537,19 @@ class OngekiBase(): tmp.pop("id") tmp.pop("user") login_bonuses.append(tmp) - + return { - "userId": data["userId"], - "length": len(login_bonuses), - "userLoginBonusList": login_bonuses + "userId": data["userId"], + "length": len(login_bonuses), + "userLoginBonusList": login_bonuses, } def handle_get_user_bp_base_request(self, data: Dict) -> Dict: - p = self.data.profile.get_profile(self.game, self.version, user_id = data["userId"]) - if p is None: return {} + p = self.data.profile.get_profile( + self.game, self.version, user_id=data["userId"] + ) + if p is None: + return {} profile = json.loads(p["data"]) return { "userId": data["userId"], @@ -470,7 +559,8 @@ class OngekiBase(): def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict: recent_rating = self.data.profile.get_profile_recent_rating(data["userId"]) - if recent_rating is None: return {} + if recent_rating is None: + return {} userRecentRatingList = recent_rating["recentRating"] @@ -482,31 +572,35 @@ class OngekiBase(): def handle_get_user_activity_api_request(self, data: Dict) -> Dict: activity = self.data.profile.get_profile_activity(data["userId"], data["kind"]) - if activity is None: return {} - + if activity is None: + return {} + user_activity = [] - + for act in activity: - user_activity.append({ - "kind": act["kind"], - "id": act["activityId"], - "sortNumber": act["sortNumber"], - "param1": act["param1"], - "param2": act["param2"], - "param3": act["param3"], - "param4": act["param4"], - }) + user_activity.append( + { + "kind": act["kind"], + "id": act["activityId"], + "sortNumber": act["sortNumber"], + "param1": act["param1"], + "param2": act["param2"], + "param3": act["param3"], + "param4": act["param4"], + } + ) return { - "userId": data["userId"], + "userId": data["userId"], "length": len(user_activity), "kind": data["kind"], - "userActivityList": user_activity + "userActivityList": user_activity, } def handle_get_user_story_api_request(self, data: Dict) -> Dict: user_stories = self.data.item.get_stories(data["userId"]) - if user_stories is None: return {} + if user_stories is None: + return {} story_list = [] for story in user_stories: @@ -516,14 +610,15 @@ class OngekiBase(): story_list.append(tmp) return { - "userId": data["userId"], - "length": len(story_list), - "userStoryList": story_list + "userId": data["userId"], + "length": len(story_list), + "userStoryList": story_list, } def handle_get_user_chapter_api_request(self, data: Dict) -> Dict: user_chapters = self.data.item.get_chapters(data["userId"]) - if user_chapters is None: return {} + if user_chapters is None: + return {} chapter_list = [] for chapter in user_chapters: @@ -531,11 +626,11 @@ class OngekiBase(): tmp.pop("id") tmp.pop("user") chapter_list.append(tmp) - + return { - "userId": data["userId"], - "length": len(chapter_list), - "userChapterList": chapter_list + "userId": data["userId"], + "length": len(chapter_list), + "userChapterList": chapter_list, } def handle_get_user_training_room_by_key_api_request(self, data: Dict) -> Dict: @@ -547,7 +642,8 @@ class OngekiBase(): def handle_get_user_character_api_request(self, data: Dict) -> Dict: user_characters = self.data.item.get_characters(data["userId"]) - if user_characters is None: return {} + if user_characters is None: + return {} character_list = [] for character in user_characters: @@ -555,16 +651,17 @@ class OngekiBase(): tmp.pop("id") tmp.pop("user") character_list.append(tmp) - + return { - "userId": data["userId"], - "length": len(character_list), - "userCharacterList": character_list + "userId": data["userId"], + "length": len(character_list), + "userCharacterList": character_list, } def handle_get_user_card_api_request(self, data: Dict) -> Dict: user_cards = self.data.item.get_cards(data["userId"]) - if user_cards is None: return {} + if user_cards is None: + return {} card_list = [] for card in user_cards: @@ -572,17 +669,18 @@ class OngekiBase(): tmp.pop("id") tmp.pop("user") card_list.append(tmp) - + return { - "userId": data["userId"], - "length": len(card_list), - "userCardList": card_list + "userId": data["userId"], + "length": len(card_list), + "userCardList": card_list, } def handle_get_user_deck_by_key_api_request(self, data: Dict) -> Dict: # Auth key doesn't matter, it just wants all the decks decks = self.data.item.get_decks(data["userId"]) - if decks is None: return {} + if decks is None: + return {} deck_list = [] for deck in decks: @@ -599,7 +697,8 @@ class OngekiBase(): def handle_get_user_trade_item_api_request(self, data: Dict) -> Dict: user_trade_items = self.data.item.get_trade_items(data["userId"]) - if user_trade_items is None: return {} + if user_trade_items is None: + return {} trade_item_list = [] for trade_item in user_trade_items: @@ -616,7 +715,8 @@ class OngekiBase(): def handle_get_user_scenario_api_request(self, data: Dict) -> Dict: user_scenerio = self.data.item.get_scenerios(data["userId"]) - if user_scenerio is None: return {} + if user_scenerio is None: + return {} scenerio_list = [] for scenerio in user_scenerio: @@ -624,7 +724,7 @@ class OngekiBase(): tmp.pop("id") tmp.pop("user") scenerio_list.append(tmp) - + return { "userId": data["userId"], "length": len(scenerio_list), @@ -633,7 +733,8 @@ class OngekiBase(): def handle_get_user_ratinglog_api_request(self, data: Dict) -> Dict: rating_log = self.data.profile.get_profile_rating_log(data["userId"]) - if rating_log is None: return {} + if rating_log is None: + return {} userRatinglogList = [] for rating in rating_log: @@ -650,7 +751,8 @@ class OngekiBase(): def handle_get_user_mission_point_api_request(self, data: Dict) -> Dict: user_mission_point_list = self.data.item.get_mission_points(data["userId"]) - if user_mission_point_list is None: return {} + if user_mission_point_list is None: + return {} mission_point_list = [] for evt_music in user_mission_point_list: @@ -667,7 +769,8 @@ class OngekiBase(): def handle_get_user_event_point_api_request(self, data: Dict) -> Dict: user_event_point_list = self.data.item.get_event_points(data["userId"]) - if user_event_point_list is None: return {} + if user_event_point_list is None: + return {} event_point_list = [] for evt_music in user_event_point_list: @@ -684,7 +787,8 @@ class OngekiBase(): def handle_get_user_music_item_api_request(self, data: Dict) -> Dict: user_music_item_list = self.data.item.get_music_items(data["userId"]) - if user_music_item_list is None: return {} + if user_music_item_list is None: + return {} music_item_list = [] for evt_music in user_music_item_list: @@ -701,7 +805,8 @@ class OngekiBase(): def handle_get_user_event_music_api_request(self, data: Dict) -> Dict: user_evt_music_list = self.data.item.get_event_music(data["userId"]) - if user_evt_music_list is None: return {} + if user_evt_music_list is None: + return {} evt_music_list = [] for evt_music in user_evt_music_list: @@ -718,7 +823,8 @@ class OngekiBase(): def handle_get_user_boss_api_request(self, data: Dict) -> Dict: p = self.data.item.get_bosses(data["userId"]) - if p is None: return {} + if p is None: + return {} boss_list = [] for boss in p: @@ -740,7 +846,9 @@ class OngekiBase(): # The isNew fields are new as of Red and up. We just won't use them for now. if "userData" in upsert and len(upsert["userData"]) > 0: - self.data.profile.put_profile_data(user_id, self.version, upsert["userData"][0]) + self.data.profile.put_profile_data( + user_id, self.version, upsert["userData"][0] + ) if "userOption" in upsert and len(upsert["userOption"]) > 0: self.data.profile.put_profile_options(user_id, upsert["userOption"][0]) @@ -751,27 +859,37 @@ class OngekiBase(): if "userActivityList" in upsert: for act in upsert["userActivityList"]: - self.data.profile.put_profile_activity(user_id, act["kind"], act["id"], act["sortNumber"], act["param1"], - act["param2"], act["param3"], act["param4"]) - + self.data.profile.put_profile_activity( + user_id, + act["kind"], + act["id"], + act["sortNumber"], + act["param1"], + act["param2"], + act["param3"], + act["param4"], + ) + if "userRecentRatingList" in upsert: - self.data.profile.put_profile_recent_rating(user_id, upsert["userRecentRatingList"]) - + self.data.profile.put_profile_recent_rating( + user_id, upsert["userRecentRatingList"] + ) + if "userBpBaseList" in upsert: self.data.profile.put_profile_bp_list(user_id, upsert["userBpBaseList"]) - + if "userMusicDetailList" in upsert: for x in upsert["userMusicDetailList"]: self.data.score.put_best_score(user_id, x) - + if "userCharacterList" in upsert: for x in upsert["userCharacterList"]: self.data.item.put_character(user_id, x) - + if "userCardList" in upsert: for x in upsert["userCardList"]: self.data.item.put_card(user_id, x) - + if "userDeckList" in upsert: for x in upsert["userDeckList"]: self.data.item.put_deck(user_id, x) @@ -779,43 +897,45 @@ class OngekiBase(): if "userTrainingRoomList" in upsert: for x in upsert["userTrainingRoomList"]: self.data.profile.put_training_room(user_id, x) - + if "userStoryList" in upsert: for x in upsert["userStoryList"]: self.data.item.put_story(user_id, x) - + if "userChapterList" in upsert: for x in upsert["userChapterList"]: self.data.item.put_chapter(user_id, x) - + if "userMemoryChapterList" in upsert: for x in upsert["userMemoryChapterList"]: self.data.item.put_memorychapter(user_id, x) - + if "userItemList" in upsert: for x in upsert["userItemList"]: self.data.item.put_item(user_id, x) - + if "userMusicItemList" in upsert: for x in upsert["userMusicItemList"]: self.data.item.put_music_item(user_id, x) - + if "userLoginBonusList" in upsert: for x in upsert["userLoginBonusList"]: self.data.item.put_login_bonus(user_id, x) - + if "userEventPointList" in upsert: for x in upsert["userEventPointList"]: self.data.item.put_event_point(user_id, x) - + if "userMissionPointList" in upsert: for x in upsert["userMissionPointList"]: self.data.item.put_mission_point(user_id, x) - + if "userRatinglogList" in upsert: for x in upsert["userRatinglogList"]: - self.data.profile.put_profile_rating_log(user_id, x["dataVersion"], x["highestRating"]) - + self.data.profile.put_profile_rating_log( + user_id, x["dataVersion"], x["highestRating"] + ) + if "userBossList" in upsert: for x in upsert["userBossList"]: self.data.item.put_boss(user_id, x) @@ -844,7 +964,7 @@ class OngekiBase(): for x in upsert["userKopList"]: self.data.profile.put_kop(user_id, x) - return {'returnCode': 1, 'apiName': 'upsertUserAll'} + return {"returnCode": 1, "apiName": "upsertUserAll"} def handle_get_user_rival_api_request(self, data: Dict) -> Dict: """ @@ -857,29 +977,28 @@ class OngekiBase(): "length": 0, "userRivalList": [], } - + return { "userId": data["userId"], "length": len(rival_list), "userRivalList": rival_list._asdict(), } - def handle_get_user_rival_data_api_reqiest(self, data:Dict) -> Dict: + def handle_get_user_rival_data_api_reqiest(self, data: Dict) -> Dict: """ Added in Bright """ rivals = [] for rival in data["userRivalList"]: - name = self.data.profile.get_profile_name(rival["rivalUserId"], self.version) + name = self.data.profile.get_profile_name( + rival["rivalUserId"], self.version + ) if name is None: continue - rivals.append({ - "rivalUserId": rival["rival"], - "rivalUserName": name - }) - + rivals.append({"rivalUserId": rival["rival"], "rivalUserName": name}) + return { "userId": data["userId"], "length": len(rivals), @@ -893,11 +1012,9 @@ class OngekiBase(): rival_id = data["rivalUserId"] next_idx = data["nextIndex"] max_ct = data["maxCount"] - music = self.handle_get_user_music_api_request({ - "userId": rival_id, - "nextIndex": next_idx, - "maxCount": max_ct - }) + music = self.handle_get_user_music_api_request( + {"userId": rival_id, "nextIndex": next_idx, "maxCount": max_ct} + ) for song in music["userMusicList"]: song["userRivalMusicDetailList"] = song["userMusicDetailList"] @@ -921,18 +1038,15 @@ class OngekiBase(): tmp = md._asdict() tmp.pop("user") tmp.pop("id") - + for song in song_list: if song["userMusicDetailList"][0]["musicId"] == tmp["musicId"]: found = True song["userMusicDetailList"].append(tmp) song["length"] = len(song["userMusicDetailList"]) break - + if not found: - song_list.append({ - "length": 1, - "userMusicDetailList": [tmp] - }) - + song_list.append({"length": 1, "userMusicDetailList": [tmp]}) + return song_list diff --git a/titles/ongeki/bright.py b/titles/ongeki/bright.py index 8f66f93..4b2a06f 100644 --- a/titles/ongeki/bright.py +++ b/titles/ongeki/bright.py @@ -31,7 +31,8 @@ class OngekiBright(OngekiBase): if cards is None or len(cards) == 0: # This should never happen self.logger.error( - f"handle_get_user_data_api_request: Internal error - No cards found for user id {data['userId']}") + f"handle_get_user_data_api_request: Internal error - No cards found for user id {data['userId']}" + ) return {} # get the dict representation of the row so we can modify values @@ -86,7 +87,7 @@ class OngekiBright(OngekiBase): "userId": data["userId"], "length": len(card_list[start_idx:end_idx]), "nextIndex": next_idx, - "userCardList": card_list[start_idx:end_idx] + "userCardList": card_list[start_idx:end_idx], } def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: @@ -115,31 +116,26 @@ class OngekiBright(OngekiBase): "userId": data["userId"], "length": len(character_list[start_idx:end_idx]), "nextIndex": next_idx, - "userCharacterList": character_list[start_idx:end_idx] + "userCharacterList": character_list[start_idx:end_idx], } def handle_get_user_gacha_api_request(self, data: Dict) -> Dict: user_gachas = self.data.item.get_user_gachas(data["userId"]) if user_gachas is None: - return { - "userId": data["userId"], - "length": 0, - "userGachaList": [] - } + return {"userId": data["userId"], "length": 0, "userGachaList": []} user_gacha_list = [] for gacha in user_gachas: tmp = gacha._asdict() tmp.pop("id") tmp.pop("user") - tmp["dailyGachaDate"] = datetime.strftime( - tmp["dailyGachaDate"], "%Y-%m-%d") + tmp["dailyGachaDate"] = datetime.strftime(tmp["dailyGachaDate"], "%Y-%m-%d") user_gacha_list.append(tmp) return { "userId": data["userId"], "length": len(user_gacha_list), - "userGachaList": user_gacha_list + "userGachaList": user_gacha_list, } def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: @@ -147,21 +143,16 @@ class OngekiBright(OngekiBase): def handle_cm_get_user_gacha_supply_api_request(self, data: Dict) -> Dict: # not used for now? not sure what it even does - user_gacha_supplies = self.data.item.get_user_gacha_supplies( - data["userId"]) + user_gacha_supplies = self.data.item.get_user_gacha_supplies(data["userId"]) if user_gacha_supplies is None: - return { - "supplyId": 1, - "length": 0, - "supplyCardList": [] - } + return {"supplyId": 1, "length": 0, "supplyCardList": []} supply_list = [gacha["cardId"] for gacha in user_gacha_supplies] return { "supplyId": 1, "length": len(supply_list), - "supplyCardList": supply_list + "supplyCardList": supply_list, } def handle_get_game_gacha_api_request(self, data: Dict) -> Dict: @@ -182,29 +173,33 @@ class OngekiBright(OngekiBase): tmp = gacha._asdict() tmp.pop("id") tmp.pop("version") - tmp["startDate"] = datetime.strftime( - tmp["startDate"], "%Y-%m-%d %H:%M:%S") - tmp["endDate"] = datetime.strftime( - tmp["endDate"], "%Y-%m-%d %H:%M:%S") + tmp["startDate"] = datetime.strftime(tmp["startDate"], "%Y-%m-%d %H:%M:%S") + tmp["endDate"] = datetime.strftime(tmp["endDate"], "%Y-%m-%d %H:%M:%S") tmp["noticeStartDate"] = datetime.strftime( - tmp["noticeStartDate"], "%Y-%m-%d %H:%M:%S") + tmp["noticeStartDate"], "%Y-%m-%d %H:%M:%S" + ) tmp["noticeEndDate"] = datetime.strftime( - tmp["noticeEndDate"], "%Y-%m-%d %H:%M:%S") + tmp["noticeEndDate"], "%Y-%m-%d %H:%M:%S" + ) tmp["convertEndDate"] = datetime.strftime( - tmp["convertEndDate"], "%Y-%m-%d %H:%M:%S") + tmp["convertEndDate"], "%Y-%m-%d %H:%M:%S" + ) # make sure to only show gachas for the current version # so only up to bright, 1140 is the first bright memory gacha if self.version == OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY: game_gacha_list.append(tmp) - elif self.version == OngekiConstants.VER_ONGEKI_BRIGHT and tmp["gachaId"] < 1140: + elif ( + self.version == OngekiConstants.VER_ONGEKI_BRIGHT + and tmp["gachaId"] < 1140 + ): game_gacha_list.append(tmp) return { "length": len(game_gacha_list), "gameGachaList": game_gacha_list, # no clue - "registIdList": [] + "registIdList": [], } def handle_roll_gacha_api_request(self, data: Dict) -> Dict: @@ -251,7 +246,7 @@ class OngekiBright(OngekiBase): assert len(rarity) == 100 # uniform distribution to get the rarity of the card - rolls = [rarity[randint(0, len(rarity)-1)] for _ in range(num_rolls)] + rolls = [rarity[randint(0, len(rarity) - 1)] for _ in range(num_rolls)] # if SSR book used, make sure you always get one SSR if book_used == 1: @@ -273,15 +268,9 @@ class OngekiBright(OngekiBase): 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 - }) + cards_sr.append({"cardId": card["cardId"], "rarity": 2}) elif card["rarity"] == 4: - cards_ssr.append({ - "cardId": card["cardId"], - "rarity": 3 - }) + cards_ssr.append({"cardId": card["cardId"], "rarity": 3}) else: cards_sr = self.data.static.get_cards_by_rarity(self.version, 2) cards_ssr = self.data.static.get_cards_by_rarity(self.version, 3) @@ -294,46 +283,39 @@ class OngekiBright(OngekiBase): for card in gacha_cards: # make sure to add the cards to the corresponding rarity if card["rarity"] == 2: - cards_r += [{ - "cardId": card["cardId"], - "rarity": 1 - }] * chances + cards_r += [{"cardId": card["cardId"], "rarity": 1}] * chances if card["rarity"] == 3: - cards_sr += [{ - "cardId": card["cardId"], - "rarity": 2 - }] * chances + cards_sr += [{"cardId": card["cardId"], "rarity": 2}] * chances elif card["rarity"] == 4: - cards_ssr += [{ - "cardId": card["cardId"], - "rarity": 3 - }] * chances + cards_ssr += [{"cardId": card["cardId"], "rarity": 3}] * chances # get the card id for each roll rolled_cards = [] for i in range(len(rolls)): if rolls[i] == 1: - rolled_cards.append(cards_r[randint(0, len(cards_r)-1)]) + rolled_cards.append(cards_r[randint(0, len(cards_r) - 1)]) elif rolls[i] == 2: - rolled_cards.append(cards_sr[randint(0, len(cards_sr)-1)]) + rolled_cards.append(cards_sr[randint(0, len(cards_sr) - 1)]) elif rolls[i] == 3: - rolled_cards.append(cards_ssr[randint(0, len(cards_ssr)-1)]) + rolled_cards.append(cards_ssr[randint(0, len(cards_ssr) - 1)]) game_gacha_card_list = [] for card in rolled_cards: - game_gacha_card_list.append({ - "gachaId": data["gachaId"], - "cardId": card["cardId"], - # +1 because Card Maker is weird - "rarity": card["rarity"] + 1, - "weight": 1, - "isPickup": False, - "isSelect": False - }) + game_gacha_card_list.append( + { + "gachaId": data["gachaId"], + "cardId": card["cardId"], + # +1 because Card Maker is weird + "rarity": card["rarity"] + 1, + "weight": 1, + "isPickup": False, + "isSelect": False, + } + ) return { "length": len(game_gacha_card_list), - "gameGachaCardList": game_gacha_card_list + "gameGachaCardList": game_gacha_card_list, } def handle_cm_upsert_user_gacha_api_request(self, data: Dict): @@ -342,12 +324,12 @@ class OngekiBright(OngekiBase): gacha_id = data["gachaId"] gacha_count = data["gachaCnt"] - play_date = datetime.strptime(data["playDate"][:10], '%Y-%m-%d') + play_date = datetime.strptime(data["playDate"][:10], "%Y-%m-%d") select_point = data["selectPoint"] total_gacha_count, ceiling_gacha_count = 0, 0 daily_gacha_cnt, five_gacha_cnt, eleven_gacha_cnt = 0, 0, 0 - daily_gacha_date = datetime.strptime('2000-01-01', '%Y-%m-%d') + daily_gacha_date = datetime.strptime("2000-01-01", "%Y-%m-%d") # check if the user previously rolled the exact same gacha user_gacha = self.data.item.get_user_gacha(user_id, gacha_id) @@ -374,9 +356,11 @@ class OngekiBright(OngekiBase): selectPoint=select_point, useSelectPoint=0, dailyGachaCnt=daily_gacha_cnt + gacha_count, - fiveGachaCnt=five_gacha_cnt+1 if gacha_count == 5 else five_gacha_cnt, - elevenGachaCnt=eleven_gacha_cnt+1 if gacha_count == 11 else eleven_gacha_cnt, - dailyGachaDate=daily_gacha_date + fiveGachaCnt=five_gacha_cnt + 1 if gacha_count == 5 else five_gacha_cnt, + elevenGachaCnt=eleven_gacha_cnt + 1 + if gacha_count == 11 + else eleven_gacha_cnt, + dailyGachaDate=daily_gacha_date, ) if "userData" in upsert and len(upsert["userData"]) > 0: @@ -385,11 +369,13 @@ class OngekiBright(OngekiBase): if p is not None: # save the bright memory profile self.data.profile.put_profile_data( - user_id, self.version, upsert["userData"][0]) + user_id, self.version, upsert["userData"][0] + ) else: # save the bright profile self.data.profile.put_profile_data( - user_id, self.version, upsert["userData"][0]) + user_id, self.version, upsert["userData"][0] + ) if "userCharacterList" in upsert: for x in upsert["userCharacterList"]: @@ -407,7 +393,7 @@ class OngekiBright(OngekiBase): # if "gameGachaCardList" in upsert: # for x in upsert["gameGachaCardList"]: - return {'returnCode': 1, 'apiName': 'CMUpsertUserGachaApi'} + return {"returnCode": 1, "apiName": "CMUpsertUserGachaApi"} def handle_cm_upsert_user_select_gacha_api_request(self, data: Dict) -> Dict: upsert = data["cmUpsertUserSelectGacha"] @@ -419,11 +405,13 @@ class OngekiBright(OngekiBase): if p is not None: # save the bright memory profile self.data.profile.put_profile_data( - user_id, self.version, upsert["userData"][0]) + user_id, self.version, upsert["userData"][0] + ) else: # save the bright profile self.data.profile.put_profile_data( - user_id, self.version, upsert["userData"][0]) + user_id, self.version, upsert["userData"][0] + ) if "userCharacterList" in upsert: for x in upsert["userCharacterList"]: @@ -439,10 +427,10 @@ class OngekiBright(OngekiBase): user_id, x["gachaId"], selectPoint=0, - useSelectPoint=x["useSelectPoint"] + useSelectPoint=x["useSelectPoint"], ) - return {'returnCode': 1, 'apiName': 'cmUpsertUserSelectGacha'} + return {"returnCode": 1, "apiName": "cmUpsertUserSelectGacha"} def handle_get_game_gacha_card_by_id_api_request(self, data: Dict) -> Dict: game_gacha_cards = self.data.static.get_gacha_cards(data["gachaId"]) @@ -459,7 +447,7 @@ class OngekiBright(OngekiBase): "rarity": 4, "weight": 1, "isPickup": False, - "isSelect": True + "isSelect": True, }, { "gachaId": data["gachaId"], @@ -467,7 +455,7 @@ class OngekiBright(OngekiBase): "rarity": 3, "weight": 2, "isPickup": False, - "isSelect": True + "isSelect": True, }, { "gachaId": data["gachaId"], @@ -475,7 +463,7 @@ class OngekiBright(OngekiBase): "rarity": 3, "weight": 2, "isPickup": False, - "isSelect": True + "isSelect": True, }, { "gachaId": data["gachaId"], @@ -483,7 +471,7 @@ class OngekiBright(OngekiBase): "rarity": 2, "weight": 3, "isPickup": False, - "isSelect": True + "isSelect": True, }, { "gachaId": data["gachaId"], @@ -491,7 +479,7 @@ class OngekiBright(OngekiBase): "rarity": 2, "weight": 3, "isPickup": False, - "isSelect": True + "isSelect": True, }, { "gachaId": data["gachaId"], @@ -499,12 +487,12 @@ class OngekiBright(OngekiBase): "rarity": 2, "weight": 3, "isPickup": False, - "isSelect": True - } + "isSelect": True, + }, ], "emissionList": [], "afterCalcList": [], - "ssrBookCalcList": [] + "ssrBookCalcList": [], } game_gacha_card_list = [] @@ -521,7 +509,7 @@ class OngekiBright(OngekiBase): # again no clue "emissionList": [], "afterCalcList": [], - "ssrBookCalcList": [] + "ssrBookCalcList": [], } def handle_get_game_theater_api_request(self, data: Dict) -> Dict: @@ -548,18 +536,14 @@ class OngekiBright(OngekiBase): } """ - return { - "length": 0, - "gameTheaterList": [], - "registIdList": [] - } + return {"length": 0, "gameTheaterList": [], "registIdList": []} def handle_cm_upsert_user_print_playlog_api_request(self, data: Dict) -> Dict: return { "returnCode": 1, "orderId": 0, "serialId": "11111111111111111111", - "apiName": "CMUpsertUserPrintPlaylogApi" + "apiName": "CMUpsertUserPrintPlaylogApi", } def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: @@ -567,34 +551,32 @@ class OngekiBright(OngekiBase): "returnCode": 1, "orderId": 0, "serialId": "11111111111111111111", - "apiName": "CMUpsertUserPrintlogApi" + "apiName": "CMUpsertUserPrintlogApi", } def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: user_print_detail = data["userPrintDetail"] # generate random serial id - serial_id = ''.join([str(randint(0, 9)) for _ in range(20)]) + serial_id = "".join([str(randint(0, 9)) for _ in range(20)]) # not needed because are either zero or unset user_print_detail.pop("orderId") user_print_detail.pop("printNumber") user_print_detail.pop("serialId") user_print_detail["printDate"] = datetime.strptime( - user_print_detail["printDate"], '%Y-%m-%d' + user_print_detail["printDate"], "%Y-%m-%d" ) # add the entry to the user print table with the random serialId self.data.item.put_user_print_detail( - data["userId"], - serial_id, - user_print_detail + data["userId"], serial_id, user_print_detail ) return { "returnCode": 1, "serialId": serial_id, - "apiName": "CMUpsertUserPrintApi" + "apiName": "CMUpsertUserPrintApi", } def handle_cm_upsert_user_all_api_request(self, data: Dict) -> Dict: @@ -607,17 +589,26 @@ class OngekiBright(OngekiBase): if p is not None: # save the bright memory profile self.data.profile.put_profile_data( - user_id, self.version, upsert["userData"][0]) + user_id, self.version, upsert["userData"][0] + ) else: # save the bright profile self.data.profile.put_profile_data( - user_id, self.version, upsert["userData"][0]) + user_id, self.version, upsert["userData"][0] + ) if "userActivityList" in upsert: for act in upsert["userActivityList"]: self.data.profile.put_profile_activity( - user_id, act["kind"], act["id"], act["sortNumber"], - act["param1"], act["param2"], act["param3"], act["param4"]) + user_id, + act["kind"], + act["id"], + act["sortNumber"], + act["param1"], + act["param2"], + act["param3"], + act["param4"], + ) if "userItemList" in upsert: for x in upsert["userItemList"]: @@ -627,4 +618,4 @@ class OngekiBright(OngekiBase): for x in upsert["userCardList"]: self.data.item.put_card(user_id, x) - return {'returnCode': 1, 'apiName': 'cmUpsertUserAll'} + return {"returnCode": 1, "apiName": "cmUpsertUserAll"} diff --git a/titles/ongeki/brightmemory.py b/titles/ongeki/brightmemory.py index c3e4ef1..954d0e5 100644 --- a/titles/ongeki/brightmemory.py +++ b/titles/ongeki/brightmemory.py @@ -30,14 +30,96 @@ class OngekiBrightMemory(OngekiBright): def handle_get_user_memory_chapter_api_request(self, data: Dict) -> Dict: memories = self.data.item.get_memorychapters(data["userId"]) if not memories: - return {"userId": data["userId"], "length":6, "userMemoryChapterList":[ - {"gaugeId":0, "isClear": False, "gaugeNum": 0, "chapterId": 70001, "jewelCount": 0, "isBossWatched": False, "isStoryWatched": False, "isDialogWatched": False, "isEndingWatched": False, "lastPlayMusicId": 0, "lastPlayMusicLevel": 0, "lastPlayMusicCategory": 0}, - {"gaugeId":0, "isClear": False, "gaugeNum": 0, "chapterId": 70002, "jewelCount": 0, "isBossWatched": False, "isStoryWatched": False, "isDialogWatched": False, "isEndingWatched": False, "lastPlayMusicId": 0, "lastPlayMusicLevel": 0, "lastPlayMusicCategory": 0}, - {"gaugeId":0, "isClear": False, "gaugeNum": 0, "chapterId": 70003, "jewelCount": 0, "isBossWatched": False, "isStoryWatched": False, "isDialogWatched": False, "isEndingWatched": False, "lastPlayMusicId": 0, "lastPlayMusicLevel": 0, "lastPlayMusicCategory": 0}, - {"gaugeId":0, "isClear": False, "gaugeNum": 0, "chapterId": 70004, "jewelCount": 0, "isBossWatched": False, "isStoryWatched": False, "isDialogWatched": False, "isEndingWatched": False, "lastPlayMusicId": 0, "lastPlayMusicLevel": 0, "lastPlayMusicCategory": 0}, - {"gaugeId":0, "isClear": False, "gaugeNum": 0, "chapterId": 70005, "jewelCount": 0, "isBossWatched": False, "isStoryWatched": False, "isDialogWatched": False, "isEndingWatched": False, "lastPlayMusicId": 0, "lastPlayMusicLevel": 0, "lastPlayMusicCategory": 0}, - {"gaugeId":0, "isClear": False, "gaugeNum": 0, "chapterId": 70099, "jewelCount": 0, "isBossWatched": False, "isStoryWatched": False, "isDialogWatched": False, "isEndingWatched": False, "lastPlayMusicId": 0, "lastPlayMusicLevel": 0, "lastPlayMusicCategory": 0} - ]} + return { + "userId": data["userId"], + "length": 6, + "userMemoryChapterList": [ + { + "gaugeId": 0, + "isClear": False, + "gaugeNum": 0, + "chapterId": 70001, + "jewelCount": 0, + "isBossWatched": False, + "isStoryWatched": False, + "isDialogWatched": False, + "isEndingWatched": False, + "lastPlayMusicId": 0, + "lastPlayMusicLevel": 0, + "lastPlayMusicCategory": 0, + }, + { + "gaugeId": 0, + "isClear": False, + "gaugeNum": 0, + "chapterId": 70002, + "jewelCount": 0, + "isBossWatched": False, + "isStoryWatched": False, + "isDialogWatched": False, + "isEndingWatched": False, + "lastPlayMusicId": 0, + "lastPlayMusicLevel": 0, + "lastPlayMusicCategory": 0, + }, + { + "gaugeId": 0, + "isClear": False, + "gaugeNum": 0, + "chapterId": 70003, + "jewelCount": 0, + "isBossWatched": False, + "isStoryWatched": False, + "isDialogWatched": False, + "isEndingWatched": False, + "lastPlayMusicId": 0, + "lastPlayMusicLevel": 0, + "lastPlayMusicCategory": 0, + }, + { + "gaugeId": 0, + "isClear": False, + "gaugeNum": 0, + "chapterId": 70004, + "jewelCount": 0, + "isBossWatched": False, + "isStoryWatched": False, + "isDialogWatched": False, + "isEndingWatched": False, + "lastPlayMusicId": 0, + "lastPlayMusicLevel": 0, + "lastPlayMusicCategory": 0, + }, + { + "gaugeId": 0, + "isClear": False, + "gaugeNum": 0, + "chapterId": 70005, + "jewelCount": 0, + "isBossWatched": False, + "isStoryWatched": False, + "isDialogWatched": False, + "isEndingWatched": False, + "lastPlayMusicId": 0, + "lastPlayMusicLevel": 0, + "lastPlayMusicCategory": 0, + }, + { + "gaugeId": 0, + "isClear": False, + "gaugeNum": 0, + "chapterId": 70099, + "jewelCount": 0, + "isBossWatched": False, + "isStoryWatched": False, + "isDialogWatched": False, + "isEndingWatched": False, + "lastPlayMusicId": 0, + "lastPlayMusicLevel": 0, + "lastPlayMusicCategory": 0, + }, + ], + } memory_chp = [] for chp in memories: @@ -49,14 +131,11 @@ class OngekiBrightMemory(OngekiBright): return { "userId": data["userId"], "length": len(memory_chp), - "userMemoryChapterList": memory_chp + "userMemoryChapterList": memory_chp, } def handle_get_game_music_release_state_api_request(self, data: Dict) -> Dict: - return { - "techScore": 0, - "cardNum": 0 - } + return {"techScore": 0, "cardNum": 0} def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: # check for a bright memory profile diff --git a/titles/ongeki/config.py b/titles/ongeki/config.py index 3a89f29..1117b39 100644 --- a/titles/ongeki/config.py +++ b/titles/ongeki/config.py @@ -3,26 +3,34 @@ from typing import List from core.config import CoreConfig -class OngekiServerConfig(): +class OngekiServerConfig: def __init__(self, parent_config: "OngekiConfig") -> None: self.__config = parent_config @property def enable(self) -> bool: - return CoreConfig.get_config_field(self.__config, 'ongeki', 'server', 'enable', default=True) + return CoreConfig.get_config_field( + self.__config, "ongeki", "server", "enable", default=True + ) @property def loglevel(self) -> int: - return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'ongeki', 'server', 'loglevel', default="info")) + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "ongeki", "server", "loglevel", default="info" + ) + ) -class OngekiGachaConfig(): +class OngekiGachaConfig: def __init__(self, parent_config: "OngekiConfig") -> None: self.__config = parent_config @property def enabled_gachas(self) -> List[int]: - return CoreConfig.get_config_field(self.__config, 'ongeki', 'gachas', 'enabled_gachas', default=[]) + return CoreConfig.get_config_field( + self.__config, "ongeki", "gachas", "enabled_gachas", default=[] + ) class OngekiConfig(dict): diff --git a/titles/ongeki/const.py b/titles/ongeki/const.py index 08abcf8..ceef317 100644 --- a/titles/ongeki/const.py +++ b/titles/ongeki/const.py @@ -2,7 +2,7 @@ from typing import Final, Dict from enum import Enum -class OngekiConstants(): +class OngekiConstants: GAME_CODE = "SDDT" CONFIG_NAME = "ongeki.yaml" @@ -16,28 +16,31 @@ class OngekiConstants(): VER_ONGEKI_BRIGHT = 6 VER_ONGEKI_BRIGHT_MEMORY = 7 - EVT_TYPES: Enum = Enum('EVT_TYPES', [ - 'None', - 'Announcement', - 'Movie', - 'AddMyList', - 'UnlockChapter', - 'JewelEvent', - 'RankingEvent', - 'AcceptRankingEvent', - 'UnlockMusic', - 'UnlockCard', - 'UnlockTrophy', - 'UnlockNamePlate', - 'UnlockLimitBreakItem', - 'MissionEvent', - 'DailyBonus', - 'UnlockBossLockEarly', - 'UnlockPurchaseItem', - 'TechChallengeEvent', - 'AcceptTechChallengeEvent', - 'SilverJewelEvent', - ]) + EVT_TYPES: Enum = Enum( + "EVT_TYPES", + [ + "None", + "Announcement", + "Movie", + "AddMyList", + "UnlockChapter", + "JewelEvent", + "RankingEvent", + "AcceptRankingEvent", + "UnlockMusic", + "UnlockCard", + "UnlockTrophy", + "UnlockNamePlate", + "UnlockLimitBreakItem", + "MissionEvent", + "DailyBonus", + "UnlockBossLockEarly", + "UnlockPurchaseItem", + "TechChallengeEvent", + "AcceptTechChallengeEvent", + "SilverJewelEvent", + ], + ) class CM_GACHA_KINDS(Enum): Normal = 0 @@ -61,8 +64,16 @@ class OngekiConstants(): Master = 3 Lunatic = 10 - VERSION_NAMES = ("ONGEKI", "ONGEKI+", "ONGEKI Summer", "ONGEKI Summer+", "ONGEKI Red", "ONGEKI Red+", - "ONGEKI Bright", "ONGEKI Bright Memory") + VERSION_NAMES = ( + "ONGEKI", + "ONGEKI+", + "ONGEKI Summer", + "ONGEKI Summer+", + "ONGEKI Red", + "ONGEKI Red+", + "ONGEKI Bright", + "ONGEKI Bright Memory", + ) @classmethod def game_ver_to_string(cls, ver: int): diff --git a/titles/ongeki/database.py b/titles/ongeki/database.py index a2168e4..89255c0 100644 --- a/titles/ongeki/database.py +++ b/titles/ongeki/database.py @@ -3,6 +3,7 @@ from core.config import CoreConfig from titles.ongeki.schema import OngekiItemData, OngekiProfileData, OngekiScoreData from titles.ongeki.schema import OngekiStaticData, OngekiLogData + class OngekiData(Data): def __init__(self, cfg: CoreConfig) -> None: super().__init__(cfg) @@ -11,4 +12,4 @@ class OngekiData(Data): self.profile = OngekiProfileData(cfg, self.session) self.score = OngekiScoreData(cfg, self.session) self.static = OngekiStaticData(cfg, self.session) - self.log = OngekiLogData(cfg, self.session) \ No newline at end of file + self.log = OngekiLogData(cfg, self.session) diff --git a/titles/ongeki/index.py b/titles/ongeki/index.py index 09ab18c..07c8ff2 100644 --- a/titles/ongeki/index.py +++ b/titles/ongeki/index.py @@ -21,12 +21,15 @@ from titles.ongeki.redplus import OngekiRedPlus from titles.ongeki.bright import OngekiBright from titles.ongeki.brightmemory import OngekiBrightMemory -class OngekiServlet(): + +class OngekiServlet: def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: self.core_cfg = core_cfg self.game_cfg = OngekiConfig() if path.exists(f"{cfg_dir}/{OngekiConstants.CONFIG_NAME}"): - self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{OngekiConstants.CONFIG_NAME}"))) + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{OngekiConstants.CONFIG_NAME}")) + ) self.versions = [ OngekiBase(core_cfg, self.game_cfg), @@ -42,34 +45,52 @@ class OngekiServlet(): self.logger = logging.getLogger("ongeki") log_fmt_str = "[%(asctime)s] Ongeki | %(levelname)s | %(message)s" log_fmt = logging.Formatter(log_fmt_str) - fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.core_cfg.server.log_dir, "ongeki"), encoding='utf8', - when="d", backupCount=10) + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.core_cfg.server.log_dir, "ongeki"), + encoding="utf8", + when="d", + backupCount=10, + ) fileHandler.setFormatter(log_fmt) - + consoleHandler = logging.StreamHandler() consoleHandler.setFormatter(log_fmt) self.logger.addHandler(fileHandler) self.logger.addHandler(consoleHandler) - + self.logger.setLevel(self.game_cfg.server.loglevel) - coloredlogs.install(level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str) - + coloredlogs.install( + level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str + ) + @classmethod - def get_allnet_info(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str, str]: + def get_allnet_info( + cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str + ) -> Tuple[bool, str, str]: game_cfg = OngekiConfig() if path.exists(f"{cfg_dir}/{OngekiConstants.CONFIG_NAME}"): - game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{OngekiConstants.CONFIG_NAME}"))) + game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{OngekiConstants.CONFIG_NAME}")) + ) if not game_cfg.server.enable: return (False, "", "") - + if core_cfg.server.is_develop: - return (True, f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/", f"{core_cfg.title.hostname}:{core_cfg.title.port}/") - - return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", f"{core_cfg.title.hostname}/") + return ( + True, + f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/", + f"{core_cfg.title.hostname}:{core_cfg.title.port}/", + ) + + return ( + True, + f"http://{core_cfg.title.hostname}/{game_code}/$v/", + f"{core_cfg.title.hostname}/", + ) def render_POST(self, request: Request, version: int, url_path: str) -> bytes: if url_path.lower() == "/ping": @@ -80,39 +101,41 @@ class OngekiServlet(): internal_ver = 0 endpoint = url_split[len(url_split) - 1] - if version < 105: # 1.0 + if version < 105: # 1.0 internal_ver = OngekiConstants.VER_ONGEKI - elif version >= 105 and version < 110: # Plus + elif version >= 105 and version < 110: # Plus internal_ver = OngekiConstants.VER_ONGEKI_PLUS - elif version >= 110 and version < 115: # Summer + elif version >= 110 and version < 115: # Summer internal_ver = OngekiConstants.VER_ONGEKI_SUMMER - elif version >= 115 and version < 120: # Summer Plus + elif version >= 115 and version < 120: # Summer Plus internal_ver = OngekiConstants.VER_ONGEKI_SUMMER_PLUS - elif version >= 120 and version < 125: # Red + elif version >= 120 and version < 125: # Red internal_ver = OngekiConstants.VER_ONGEKI_RED - elif version >= 125 and version < 130: # Red Plus + elif version >= 125 and version < 130: # Red Plus internal_ver = OngekiConstants.VER_ONGEKI_RED_PLUS - elif version >= 130 and version < 135: # Bright + elif version >= 130 and version < 135: # Bright internal_ver = OngekiConstants.VER_ONGEKI_BRIGHT - elif version >= 135 and version < 140: # 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: - # If we get a 32 character long hex string, it's a hash and we're - # doing encrypted. The likelyhood of false positives is low but + # If we get a 32 character long hex string, it's a hash and we're + # doing encrypted. The likelyhood of false positives is low but # technically not 0 self.logger.error("Encryption not supported at this time") return b"" - try: + try: unzip = zlib.decompress(req_raw) - + except zlib.error as e: - self.logger.error(f"Failed to decompress v{version} {endpoint} request -> {e}") + self.logger.error( + f"Failed to decompress v{version} {endpoint} request -> {e}" + ) return zlib.compress(b'{"stat": "0"}') - + req_data = json.loads(unzip) - + self.logger.info(f"v{version} {endpoint} request - {req_data}") func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" @@ -128,13 +151,10 @@ class OngekiServlet(): except Exception as e: self.logger.error(f"Error handling v{version} method {endpoint} - {e}") return zlib.compress(b'{"stat": "0"}') - + if resp == None: - resp = {'returnCode': 1} - + resp = {"returnCode": 1} + self.logger.info(f"Response {resp}") - + return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) - - - \ No newline at end of file diff --git a/titles/ongeki/plus.py b/titles/ongeki/plus.py index 8875503..9168576 100644 --- a/titles/ongeki/plus.py +++ b/titles/ongeki/plus.py @@ -5,11 +5,12 @@ from titles.ongeki.base import OngekiBase from titles.ongeki.const import OngekiConstants from titles.ongeki.config import OngekiConfig + class OngekiPlus(OngekiBase): def __init__(self, core_cfg: CoreConfig, game_cfg: OngekiConfig) -> None: super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_PLUS - + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: ret = super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.05.00" diff --git a/titles/ongeki/read.py b/titles/ongeki/read.py index 397fa8c..b64194f 100644 --- a/titles/ongeki/read.py +++ b/titles/ongeki/read.py @@ -13,14 +13,21 @@ from titles.ongeki.config import OngekiConfig class OngekiReader(BaseReader): - def __init__(self, config: CoreConfig, version: int, bin_dir: Optional[str], - opt_dir: Optional[str], extra: Optional[str]) -> None: + def __init__( + self, + config: CoreConfig, + version: int, + bin_dir: Optional[str], + opt_dir: Optional[str], + extra: Optional[str], + ) -> None: super().__init__(config, version, bin_dir, opt_dir, extra) self.data = OngekiData(config) try: self.logger.info( - f"Start importer for {OngekiConstants.game_ver_to_string(version)}") + f"Start importer for {OngekiConstants.game_ver_to_string(version)}" + ) except IndexError: self.logger.error(f"Invalid ongeki version {version}") exit(1) @@ -42,14 +49,14 @@ class OngekiReader(BaseReader): self.logger.info(f"Reading cards from {base_dir}...") version_ids = { - '1000': OngekiConstants.VER_ONGEKI, - '1005': OngekiConstants.VER_ONGEKI_PLUS, - '1010': OngekiConstants.VER_ONGEKI_SUMMER, - '1015': OngekiConstants.VER_ONGEKI_SUMMER_PLUS, - '1020': OngekiConstants.VER_ONGEKI_RED, - '1025': OngekiConstants.VER_ONGEKI_RED_PLUS, - '1030': OngekiConstants.VER_ONGEKI_BRIGHT, - '1035': OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY + "1000": OngekiConstants.VER_ONGEKI, + "1005": OngekiConstants.VER_ONGEKI_PLUS, + "1010": OngekiConstants.VER_ONGEKI_SUMMER, + "1015": OngekiConstants.VER_ONGEKI_SUMMER_PLUS, + "1020": OngekiConstants.VER_ONGEKI_RED, + "1025": OngekiConstants.VER_ONGEKI_RED_PLUS, + "1030": OngekiConstants.VER_ONGEKI_BRIGHT, + "1035": OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY, } for root, dirs, files in os.walk(base_dir): @@ -58,34 +65,39 @@ class OngekiReader(BaseReader): with open(f"{root}/{dir}/Card.xml", "r", encoding="utf-8") as f: troot = ET.fromstring(f.read()) - card_id = int(troot.find('Name').find('id').text) + card_id = int(troot.find("Name").find("id").text) # skip already existing cards - if self.data.static.get_card( - OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY, card_id) is not None: - + if ( + self.data.static.get_card( + OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY, card_id + ) + is not None + ): self.logger.info(f"Card {card_id} already added, skipping") continue - name = troot.find('Name').find('str').text - chara_id = int(troot.find('CharaID').find('id').text) - nick_name = troot.find('NickName').text - school = troot.find('School').find('str').text - attribute = troot.find('Attribute').text - gakunen = troot.find('Gakunen').find('str').text + name = troot.find("Name").find("str").text + chara_id = int(troot.find("CharaID").find("id").text) + nick_name = troot.find("NickName").text + school = troot.find("School").find("str").text + attribute = troot.find("Attribute").text + gakunen = troot.find("Gakunen").find("str").text rarity = OngekiConstants.RARITY_TYPES[ - troot.find('Rarity').text].value + troot.find("Rarity").text + ].value level_param = [] - for lvl in troot.find('LevelParam').findall('int'): + for lvl in troot.find("LevelParam").findall("int"): level_param.append(lvl.text) - skill_id = int(troot.find('SkillID').find('id').text) - cho_kai_ka_skill_id = int(troot.find('ChoKaikaSkillID').find('id').text) + skill_id = int(troot.find("SkillID").find("id").text) + cho_kai_ka_skill_id = int( + troot.find("ChoKaikaSkillID").find("id").text + ) - version = version_ids[ - troot.find('VersionID').find('id').text] - card_number = troot.find('CardNumberString').text + version = version_ids[troot.find("VersionID").find("id").text] + card_number = troot.find("CardNumberString").text self.data.static.put_card( version, @@ -97,10 +109,10 @@ class OngekiReader(BaseReader): attribute=attribute, gakunen=gakunen, rarity=rarity, - levelParam=','.join(level_param), + levelParam=",".join(level_param), skillId=skill_id, choKaikaSkillId=cho_kai_ka_skill_id, - cardNumber=card_number + cardNumber=card_number, ) self.logger.info(f"Added card {card_id}") @@ -113,13 +125,13 @@ class OngekiReader(BaseReader): with open(f"{root}/{dir}/Event.xml", "r", encoding="utf-8") as f: troot = ET.fromstring(f.read()) - name = troot.find('Name').find('str').text - id = int(troot.find('Name').find('id').text) - event_type = OngekiConstants.EVT_TYPES[troot.find( - 'EventType').text].value + name = troot.find("Name").find("str").text + id = int(troot.find("Name").find("id").text) + event_type = OngekiConstants.EVT_TYPES[ + troot.find("EventType").text + ].value - self.data.static.put_event( - self.version, id, event_type, name) + self.data.static.put_event(self.version, id, event_type, name) self.logger.info(f"Added event {id}") def read_music(self, base_dir: str) -> None: @@ -138,15 +150,15 @@ class OngekiReader(BaseReader): if root is None: continue - name = troot.find('Name') - song_id = name.find('id').text - title = name.find('str').text - artist = troot.find('ArtistName').find('str').text - genre = troot.find('Genre').find('str').text + name = troot.find("Name") + song_id = name.find("id").text + title = name.find("str").text + artist = troot.find("ArtistName").find("str").text + genre = troot.find("Genre").find("str").text fumens = troot.find("FumenData") - for fumens_data in fumens.findall('FumenData'): - path = fumens_data.find('FumenFile').find('path').text + for fumens_data in fumens.findall("FumenData"): + path = fumens_data.find("FumenFile").find("path").text if path is None or not os.path.exists(f"{root}/{dir}/{path}"): continue @@ -156,6 +168,6 @@ class OngekiReader(BaseReader): ) 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}") + self.version, song_id, chart_id, title, artist, genre, level + ) + self.logger.info(f"Added song {song_id} chart {chart_id}") diff --git a/titles/ongeki/red.py b/titles/ongeki/red.py index 047286e..52b9d59 100644 --- a/titles/ongeki/red.py +++ b/titles/ongeki/red.py @@ -5,11 +5,12 @@ from titles.ongeki.base import OngekiBase from titles.ongeki.const import OngekiConstants from titles.ongeki.config import OngekiConfig + class OngekiRed(OngekiBase): def __init__(self, core_cfg: CoreConfig, game_cfg: OngekiConfig) -> None: super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_RED - + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: ret = super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.20.00" diff --git a/titles/ongeki/redplus.py b/titles/ongeki/redplus.py index a4df205..1f69690 100644 --- a/titles/ongeki/redplus.py +++ b/titles/ongeki/redplus.py @@ -5,11 +5,12 @@ from titles.ongeki.base import OngekiBase from titles.ongeki.const import OngekiConstants from titles.ongeki.config import OngekiConfig + class OngekiRedPlus(OngekiBase): def __init__(self, core_cfg: CoreConfig, game_cfg: OngekiConfig) -> None: super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_RED_PLUS - + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: ret = super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.25.00" diff --git a/titles/ongeki/schema/__init__.py b/titles/ongeki/schema/__init__.py index 9b1e1da..b93a16c 100644 --- a/titles/ongeki/schema/__init__.py +++ b/titles/ongeki/schema/__init__.py @@ -4,4 +4,10 @@ from titles.ongeki.schema.static import OngekiStaticData from titles.ongeki.schema.score import OngekiScoreData from titles.ongeki.schema.log import OngekiLogData -__all__ = [OngekiProfileData, OngekiItemData, OngekiStaticData, OngekiScoreData, OngekiLogData] \ No newline at end of file +__all__ = [ + OngekiProfileData, + OngekiItemData, + OngekiStaticData, + OngekiScoreData, + OngekiLogData, +] diff --git a/titles/ongeki/schema/item.py b/titles/ongeki/schema/item.py index 001388a..d406597 100644 --- a/titles/ongeki/schema/item.py +++ b/titles/ongeki/schema/item.py @@ -28,7 +28,7 @@ card = Table( Column("isAcquired", Boolean), Column("created", String(25)), UniqueConstraint("user", "cardId", name="ongeki_user_card_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) deck = Table( @@ -41,7 +41,7 @@ deck = Table( Column("cardId2", Integer), Column("cardId3", Integer), UniqueConstraint("user", "deckId", name="ongeki_user_deck_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) character = Table( @@ -59,10 +59,10 @@ character = Table( Column("intimateCountDate", String(25)), Column("isNew", Boolean), UniqueConstraint("user", "characterId", name="ongeki_user_character_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) -boss = Table ( +boss = Table( "ongeki_user_boss", metadata, Column("id", Integer, primary_key=True, nullable=False), @@ -72,10 +72,10 @@ boss = Table ( Column("isClear", Boolean), Column("eventId", Integer), UniqueConstraint("user", "musicId", "eventId", name="ongeki_user_boss_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) -story = Table ( +story = Table( "ongeki_user_story", metadata, Column("id", Integer, primary_key=True, nullable=False), @@ -87,7 +87,7 @@ story = Table ( Column("lastPlayMusicCategory", Integer), Column("lastPlayMusicLevel", Integer), UniqueConstraint("user", "storyId", name="ongeki_user_story_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) chapter = Table( @@ -105,7 +105,7 @@ chapter = Table( Column("skipTiming1", Integer), Column("skipTiming2", Integer), UniqueConstraint("user", "chapterId", name="ongeki_user_chapter_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) memorychapter = Table( @@ -126,7 +126,7 @@ memorychapter = Table( Column("lastPlayMusicLevel", Integer), Column("lastPlayMusicCategory", Integer), UniqueConstraint("user", "chapterId", name="ongeki_user_memorychapter_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) item = Table( @@ -139,7 +139,7 @@ item = Table( Column("stock", Integer), Column("isValid", Boolean), UniqueConstraint("user", "itemKind", "itemId", name="ongeki_user_item_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) music_item = Table( @@ -150,7 +150,7 @@ music_item = Table( Column("musicId", Integer), Column("status", Integer), UniqueConstraint("user", "musicId", name="ongeki_user_music_item_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) login_bonus = Table( @@ -162,7 +162,7 @@ login_bonus = Table( Column("bonusCount", Integer), Column("lastUpdateDate", String(25)), UniqueConstraint("user", "bonusId", name="ongeki_user_login_bonus_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) event_point = Table( @@ -174,7 +174,7 @@ event_point = Table( Column("point", Integer), Column("isRankingRewarded", Boolean), UniqueConstraint("user", "eventId", name="ongeki_user_event_point_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) mission_point = Table( @@ -185,7 +185,7 @@ mission_point = Table( Column("eventId", Integer), Column("point", Integer), UniqueConstraint("user", "eventId", name="ongeki_user_mission_point_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) scenerio = Table( @@ -196,7 +196,7 @@ scenerio = Table( Column("scenarioId", Integer), Column("playCount", Integer), UniqueConstraint("user", "scenarioId", name="ongeki_user_scenerio_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) trade_item = Table( @@ -207,8 +207,10 @@ trade_item = Table( Column("chapterId", Integer), Column("tradeItemId", Integer), Column("tradeCount", Integer), - UniqueConstraint("user", "chapterId", "tradeItemId", name="ongeki_user_trade_item_uk"), - mysql_charset='utf8mb4' + UniqueConstraint( + "user", "chapterId", "tradeItemId", name="ongeki_user_trade_item_uk" + ), + mysql_charset="utf8mb4", ) event_music = Table( @@ -224,8 +226,10 @@ event_music = Table( Column("platinumScoreMax", Integer), Column("techRecordDate", String(25)), Column("isTechNewRecord", Boolean), - UniqueConstraint("user", "eventId", "type", "musicId", "level", name="ongeki_user_event_music"), - mysql_charset='utf8mb4' + UniqueConstraint( + "user", "eventId", "type", "musicId", "level", name="ongeki_user_event_music" + ), + mysql_charset="utf8mb4", ) tech_event = Table( @@ -240,14 +244,18 @@ tech_event = Table( Column("isRankingRewarded", Boolean), Column("isTotalTechNewRecord", Boolean), UniqueConstraint("user", "eventId", name="ongeki_user_tech_event_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) gacha = Table( "ongeki_user_gacha", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("gachaId", Integer, nullable=False), Column("totalGachaCnt", Integer, server_default="0"), Column("ceilingGachaCnt", Integer, server_default="0"), @@ -258,17 +266,21 @@ gacha = Table( Column("elevenGachaCnt", Integer, server_default="0"), Column("dailyGachaDate", TIMESTAMP, nullable=False, server_default=func.now()), UniqueConstraint("user", "gachaId", name="ongeki_user_gacha_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) gacha_supply = Table( "ongeki_user_gacha_supply", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("cardId", Integer, nullable=False), UniqueConstraint("user", "cardId", name="ongeki_user_gacha_supply_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) @@ -276,7 +288,11 @@ print_detail = Table( "ongeki_user_print_detail", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("cardId", Integer, nullable=False), Column("cardType", Integer, server_default="0"), Column("printDate", TIMESTAMP, nullable=False), @@ -297,11 +313,11 @@ print_detail = Table( Column("printOption9", Boolean, server_default="1"), Column("printOption10", Boolean, server_default="0"), UniqueConstraint("serialId", name="ongeki_user_print_detail_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) -class OngekiItemData(BaseData): +class OngekiItemData(BaseData): def put_card(self, aime_id: int, card_data: Dict) -> Optional[int]: card_data["user"] = aime_id @@ -318,7 +334,8 @@ class OngekiItemData(BaseData): sql = select(card).where(card.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() def put_character(self, aime_id: int, character_data: Dict) -> Optional[int]: @@ -332,12 +349,13 @@ class OngekiItemData(BaseData): self.logger.warn(f"put_character: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - + def get_characters(self, aime_id: int) -> Optional[List[Dict]]: sql = select(character).where(character.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() def put_deck(self, aime_id: int, deck_data: Dict) -> Optional[int]: @@ -351,19 +369,21 @@ class OngekiItemData(BaseData): self.logger.warn(f"put_deck: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - + def get_deck(self, aime_id: int, deck_id: int) -> Optional[Dict]: sql = select(deck).where(and_(deck.c.user == aime_id, deck.c.deckId == deck_id)) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() - + def get_decks(self, aime_id: int) -> Optional[List[Dict]]: sql = select(deck).where(deck.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() def put_boss(self, aime_id: int, boss_data: Dict) -> Optional[int]: @@ -377,7 +397,7 @@ class OngekiItemData(BaseData): self.logger.warn(f"put_boss: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - + def put_story(self, aime_id: int, story_data: Dict) -> Optional[int]: story_data["user"] = aime_id @@ -389,12 +409,13 @@ class OngekiItemData(BaseData): self.logger.warn(f"put_story: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - + def get_stories(self, aime_id: int) -> Optional[List[Dict]]: sql = select(story).where(story.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() def put_chapter(self, aime_id: int, chapter_data: Dict) -> Optional[int]: @@ -408,12 +429,13 @@ class OngekiItemData(BaseData): self.logger.warn(f"put_chapter: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - + def get_chapters(self, aime_id: int) -> Optional[List[Dict]]: sql = select(chapter).where(chapter.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() def put_item(self, aime_id: int, item_data: Dict) -> Optional[int]: @@ -427,22 +449,26 @@ class OngekiItemData(BaseData): self.logger.warn(f"put_item: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - + def get_item(self, aime_id: int, item_id: int, item_kind: int) -> Optional[Dict]: sql = select(item).where(and_(item.c.user == aime_id, item.c.itemId == item_id)) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() def get_items(self, aime_id: int, item_kind: int = None) -> Optional[List[Dict]]: if item_kind is None: sql = select(item).where(item.c.user == aime_id) else: - sql = select(item).where(and_(item.c.user == aime_id, item.c.itemKind == item_kind)) + sql = select(item).where( + and_(item.c.user == aime_id, item.c.itemKind == item_kind) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() def put_music_item(self, aime_id: int, music_item_data: Dict) -> Optional[int]: @@ -456,12 +482,13 @@ class OngekiItemData(BaseData): self.logger.warn(f"put_music_item: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - + def get_music_items(self, aime_id: int) -> Optional[List[Dict]]: sql = select(music_item).where(music_item.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() def put_login_bonus(self, aime_id: int, login_bonus_data: Dict) -> Optional[int]: @@ -475,15 +502,18 @@ class OngekiItemData(BaseData): self.logger.warn(f"put_login_bonus: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - + def get_login_bonuses(self, aime_id: int) -> Optional[List[Dict]]: sql = select(login_bonus).where(login_bonus.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - def put_mission_point(self, aime_id: int, mission_point_data: Dict) -> Optional[int]: + def put_mission_point( + self, aime_id: int, mission_point_data: Dict + ) -> Optional[int]: mission_point_data["user"] = aime_id sql = insert(mission_point).values(**mission_point_data) @@ -494,14 +524,15 @@ class OngekiItemData(BaseData): self.logger.warn(f"put_mission_point: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - + def get_mission_points(self, aime_id: int) -> Optional[List[Dict]]: sql = select(mission_point).where(mission_point.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - + def put_event_point(self, aime_id: int, event_point_data: Dict) -> Optional[int]: event_point_data["user"] = aime_id @@ -513,12 +544,13 @@ class OngekiItemData(BaseData): self.logger.warn(f"put_event_point: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - + def get_event_points(self, aime_id: int) -> Optional[List[Dict]]: sql = select(event_point).where(event_point.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() def put_scenerio(self, aime_id: int, scenerio_data: Dict) -> Optional[int]: @@ -532,12 +564,13 @@ class OngekiItemData(BaseData): self.logger.warn(f"put_scenerio: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - + def get_scenerios(self, aime_id: int) -> Optional[List[Dict]]: sql = select(scenerio).where(scenerio.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() def put_trade_item(self, aime_id: int, trade_item_data: Dict) -> Optional[int]: @@ -551,14 +584,15 @@ class OngekiItemData(BaseData): self.logger.warn(f"put_trade_item: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - + def get_trade_items(self, aime_id: int) -> Optional[List[Dict]]: sql = select(trade_item).where(trade_item.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - + def put_event_music(self, aime_id: int, event_music_data: Dict) -> Optional[int]: event_music_data["user"] = aime_id @@ -570,12 +604,13 @@ class OngekiItemData(BaseData): self.logger.warn(f"put_event_music: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - + def get_event_music(self, aime_id: int) -> Optional[List[Dict]]: sql = select(event_music).where(event_music.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() def put_tech_event(self, aime_id: int, tech_event_data: Dict) -> Optional[int]: @@ -589,22 +624,26 @@ class OngekiItemData(BaseData): self.logger.warn(f"put_tech_event: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - + def get_tech_event(self, aime_id: int) -> Optional[List[Dict]]: sql = select(tech_event).where(tech_event.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() def get_bosses(self, aime_id: int) -> Optional[List[Dict]]: sql = select(boss).where(boss.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - def put_memorychapter(self, aime_id: int, memorychapter_data: Dict) -> Optional[int]: + def put_memorychapter( + self, aime_id: int, memorychapter_data: Dict + ) -> Optional[int]: memorychapter_data["user"] = aime_id sql = insert(memorychapter).values(**memorychapter_data) @@ -625,10 +664,7 @@ class OngekiItemData(BaseData): return result.fetchall() def get_user_gacha(self, aime_id: int, gacha_id: int) -> Optional[Row]: - sql = gacha.select(and_( - gacha.c.user == aime_id, - gacha.c.gachaId == gacha_id - )) + sql = gacha.select(and_(gacha.c.user == aime_id, gacha.c.gachaId == gacha_id)) result = self.execute(sql) if result is None: @@ -652,15 +688,9 @@ class OngekiItemData(BaseData): return result.fetchall() def put_user_gacha(self, aime_id: int, gacha_id: int, **data) -> Optional[int]: - sql = insert(gacha).values( - user=aime_id, - gachaId=gacha_id, - **data) + sql = insert(gacha).values(user=aime_id, gachaId=gacha_id, **data) - conflict = sql.on_duplicate_key_update( - user=aime_id, - gachaId=gacha_id, - **data) + conflict = sql.on_duplicate_key_update(user=aime_id, gachaId=gacha_id, **data) result = self.execute(conflict) if result is None: @@ -668,20 +698,21 @@ class OngekiItemData(BaseData): return None return result.lastrowid - def put_user_print_detail(self, aime_id: int, serial_id: str, - user_print_data: Dict) -> Optional[int]: + def put_user_print_detail( + self, aime_id: int, serial_id: str, user_print_data: Dict + ) -> Optional[int]: sql = insert(print_detail).values( - user=aime_id, - serialId=serial_id, - **user_print_data) + user=aime_id, serialId=serial_id, **user_print_data + ) conflict = sql.on_duplicate_key_update( - user=aime_id, - serialId=serial_id, - **user_print_data) + user=aime_id, serialId=serial_id, **user_print_data + ) result = self.execute(conflict) if result is None: - self.logger.warn(f"put_user_print_detail: Failed to insert! aime_id: {aime_id}") + self.logger.warn( + f"put_user_print_detail: Failed to insert! aime_id: {aime_id}" + ) return None return result.lastrowid diff --git a/titles/ongeki/schema/log.py b/titles/ongeki/schema/log.py index 67ed778..701e8e0 100644 --- a/titles/ongeki/schema/log.py +++ b/titles/ongeki/schema/log.py @@ -15,11 +15,13 @@ gp_log = Table( Column("usedCredit", Integer), Column("placeName", String(255)), Column("trxnDate", String(255)), - Column("placeId", Integer), # Making this an FK would mess with people playing with default KC - Column("kind", Integer), - Column("pattern", Integer), + Column( + "placeId", Integer + ), # Making this an FK would mess with people playing with default KC + Column("kind", Integer), + Column("pattern", Integer), Column("currentGP", Integer), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) session_log = Table( @@ -32,12 +34,22 @@ session_log = Table( Column("playDate", String(10)), Column("userPlayDate", String(25)), Column("isPaid", Boolean), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) + class OngekiLogData(BaseData): - def put_gp_log(self, aime_id: Optional[int], used_credit: int, place_name: str, tx_date: str, place_id: int, - kind: int, pattern: int, current_gp: int) -> Optional[int]: + def put_gp_log( + self, + aime_id: Optional[int], + used_credit: int, + place_name: str, + tx_date: str, + place_id: int, + kind: int, + pattern: int, + current_gp: int, + ) -> Optional[int]: sql = insert(gp_log).values( user=aime_id, usedCredit=used_credit, @@ -51,5 +63,7 @@ class OngekiLogData(BaseData): result = self.execute(sql) if result is None: - self.logger.warn(f"put_gp_log: Failed to insert GP log! aime_id: {aime_id} kind {kind} pattern {pattern} current_gp {current_gp}") - return result.lastrowid \ No newline at end of file + self.logger.warn( + f"put_gp_log: Failed to insert GP log! aime_id: {aime_id} kind {kind} pattern {pattern} current_gp {current_gp}" + ) + return result.lastrowid diff --git a/titles/ongeki/schema/profile.py b/titles/ongeki/schema/profile.py index ce07490..a112bf2 100644 --- a/titles/ongeki/schema/profile.py +++ b/titles/ongeki/schema/profile.py @@ -16,7 +16,11 @@ profile = Table( "ongeki_profile_data", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("version", Integer, nullable=False), Column("userName", String(8)), Column("level", Integer), @@ -81,7 +85,7 @@ profile = Table( Column("lastEmoneyCredit", Integer, server_default="0"), Column("isDialogWatchedSuggestMemory", Boolean, server_default="0"), UniqueConstraint("user", "version", name="ongeki_profile_profile_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) # No point setting defaults since the game sends everything on profile creation anyway @@ -89,7 +93,11 @@ option = Table( "ongeki_profile_option", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("optionSet", Integer), Column("speed", Integer), Column("mirror", Integer), @@ -128,14 +136,18 @@ option = Table( Column("stealthField", Integer), Column("colorWallBright", Integer), UniqueConstraint("user", name="ongeki_profile_option_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) activity = Table( "ongeki_profile_activity", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("kind", Integer), Column("activityId", Integer), Column("sortNumber", Integer), @@ -144,40 +156,52 @@ activity = Table( Column("param3", Integer), Column("param4", Integer), UniqueConstraint("user", "kind", "activityId", name="ongeki_profile_activity_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) recent_rating = Table( "ongeki_profile_recent_rating", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("recentRating", JSON), UniqueConstraint("user", name="ongeki_profile_recent_rating_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) rating_log = Table( "ongeki_profile_rating_log", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("highestRating", Integer), Column("dataVersion", String(10)), UniqueConstraint("user", "dataVersion", name="ongeki_profile_rating_log_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) region = Table( "ongeki_profile_region", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("regionId", Integer), Column("playCount", Integer), Column("created", String(25)), UniqueConstraint("user", "regionId", name="ongeki_profile_region_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) training_room = Table( @@ -185,12 +209,12 @@ training_room = Table( metadata, Column("id", Integer, primary_key=True, nullable=False), Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), - Column("roomId", Integer), + Column("roomId", Integer), Column("authKey", Integer), Column("cardId", Integer), Column("valueDate", String(25)), UniqueConstraint("user", "roomId", name="ongeki_profile_training_room_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) kop = Table( @@ -199,14 +223,14 @@ kop = Table( Column("id", Integer, primary_key=True, nullable=False), Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), Column("authKey", Integer), - Column("kopId", Integer), + Column("kopId", Integer), Column("areaId", Integer), Column("totalTechScore", Integer), Column("totalPlatinumScore", Integer), Column("techRecordDate", String(25)), Column("isTotalTechNewRecord", Boolean), UniqueConstraint("user", "kopId", name="ongeki_profile_kop_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) rival = Table( @@ -214,86 +238,112 @@ rival = Table( metadata, Column("id", Integer, primary_key=True, nullable=False), Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), - Column("rivalUserId", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column( + "rivalUserId", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + ), UniqueConstraint("user", "rivalUserId", name="ongeki_profile_rival_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) class OngekiProfileData(BaseData): def __init__(self, cfg: CoreConfig, conn: Connection) -> None: super().__init__(cfg, conn) - self.date_time_format_ext = "%Y-%m-%d %H:%M:%S.%f" # needs to be lopped off at [:-5] + self.date_time_format_ext = ( + "%Y-%m-%d %H:%M:%S.%f" # needs to be lopped off at [:-5] + ) self.date_time_format_short = "%Y-%m-%d" def get_profile_name(self, aime_id: int, version: int) -> Optional[str]: - sql = select(profile.c.userName).where(and_(profile.c.user == aime_id, profile.c.version == version)) - - result = self.execute(sql) - if result is None: return None - - row = result.fetchone() - if row is None: return None - - return row["userName"] - - def get_profile_preview(self, aime_id: int, version: int) -> Optional[Row]: - sql = select([profile, option]).join(option, profile.c.user == option.c.user).filter( + sql = select(profile.c.userName).where( and_(profile.c.user == aime_id, profile.c.version == version) ) result = self.execute(sql) - if result is None: return None + if result is None: + return None + + row = result.fetchone() + if row is None: + return None + + return row["userName"] + + def get_profile_preview(self, aime_id: int, version: int) -> Optional[Row]: + sql = ( + select([profile, option]) + .join(option, profile.c.user == option.c.user) + .filter(and_(profile.c.user == aime_id, profile.c.version == version)) + ) + + result = self.execute(sql) + if result is None: + return None return result.fetchone() def get_profile_data(self, aime_id: int, version: int) -> Optional[Row]: - sql = select(profile).where(and_( - profile.c.user == aime_id, - profile.c.version == version, - )) + sql = select(profile).where( + and_( + profile.c.user == aime_id, + profile.c.version == version, + ) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() def get_profile_options(self, aime_id: int) -> Optional[Row]: - sql = select(option).where(and_( - option.c.user == aime_id, - )) + sql = select(option).where( + and_( + option.c.user == aime_id, + ) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() def get_profile_recent_rating(self, aime_id: int) -> Optional[List[Row]]: sql = select(recent_rating).where(recent_rating.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() def get_profile_rating_log(self, aime_id: int) -> Optional[List[Row]]: sql = select(rating_log).where(recent_rating.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - def get_profile_activity(self, aime_id: int, kind: int = None) -> Optional[List[Row]]: - sql = select(activity).where(and_( - activity.c.user == aime_id, - (activity.c.kind == kind) if kind is not None else True - )) + def get_profile_activity( + self, aime_id: int, kind: int = None + ) -> Optional[List[Row]]: + sql = select(activity).where( + and_( + activity.c.user == aime_id, + (activity.c.kind == kind) if kind is not None else True, + ) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() def get_kop(self, aime_id: int) -> Optional[List[Row]]: sql = select(kop).where(kop.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() def get_rivals(self, aime_id: int) -> Optional[List[Row]]: @@ -326,48 +376,62 @@ class OngekiProfileData(BaseData): result = self.execute(conflict) if result is None: - self.logger.warn(f"put_profile_options: Failed to update! aime_id: {aime_id}") + self.logger.warn( + f"put_profile_options: Failed to update! aime_id: {aime_id}" + ) return None return result.lastrowid - def put_profile_recent_rating(self, aime_id: int, recent_rating_data: List[Dict]) -> Optional[int]: + def put_profile_recent_rating( + self, aime_id: int, recent_rating_data: List[Dict] + ) -> Optional[int]: sql = insert(recent_rating).values( - user=aime_id, - recentRating=recent_rating_data + user=aime_id, recentRating=recent_rating_data ) - conflict = sql.on_duplicate_key_update( - recentRating=recent_rating_data - ) + conflict = sql.on_duplicate_key_update(recentRating=recent_rating_data) result = self.execute(conflict) if result is None: - self.logger.warn(f"put_profile_recent_rating: failed to update recent rating! aime_id {aime_id}") + self.logger.warn( + f"put_profile_recent_rating: failed to update recent rating! aime_id {aime_id}" + ) return None return result.lastrowid - def put_profile_bp_list(self, aime_id: int, bp_base_list: List[Dict]) -> Optional[int]: + def put_profile_bp_list( + self, aime_id: int, bp_base_list: List[Dict] + ) -> Optional[int]: pass - - def put_profile_rating_log(self, aime_id: int, data_version: str, highest_rating: int) -> Optional[int]: + + def put_profile_rating_log( + self, aime_id: int, data_version: str, highest_rating: int + ) -> Optional[int]: sql = insert(rating_log).values( - user=aime_id, - dataVersion=data_version, - highestRating=highest_rating + user=aime_id, dataVersion=data_version, highestRating=highest_rating ) - conflict = sql.on_duplicate_key_update( - highestRating=highest_rating - ) + conflict = sql.on_duplicate_key_update(highestRating=highest_rating) result = self.execute(conflict) if result is None: - self.logger.warn(f"put_profile_rating_log: failed to update rating log! aime_id {aime_id} data_version {data_version} highest_rating {highest_rating}") + self.logger.warn( + f"put_profile_rating_log: failed to update rating log! aime_id {aime_id} data_version {data_version} highest_rating {highest_rating}" + ) return None return result.lastrowid - def put_profile_activity(self, aime_id: int, kind: int, activity_id: int, sort_num: int, - p1: int, p2: int, p3: int, p4: int) -> Optional[int]: + def put_profile_activity( + self, + aime_id: int, + kind: int, + activity_id: int, + sort_num: int, + p1: int, + p2: int, + p3: int, + p4: int, + ) -> Optional[int]: sql = insert(activity).values( user=aime_id, kind=kind, @@ -376,29 +440,24 @@ class OngekiProfileData(BaseData): param1=p1, param2=p2, param3=p3, - param4=p4 + param4=p4, ) conflict = sql.on_duplicate_key_update( - sortNumber=sort_num, - param1=p1, - param2=p2, - param3=p3, - param4=p4 + sortNumber=sort_num, param1=p1, param2=p2, param3=p3, param4=p4 ) result = self.execute(conflict) if result is None: - self.logger.warn(f"put_profile_activity: failed to put activity! aime_id {aime_id} kind {kind} activity_id {activity_id}") + self.logger.warn( + f"put_profile_activity: failed to put activity! aime_id {aime_id} kind {kind} activity_id {activity_id}" + ) return None return result.lastrowid - + def put_profile_region(self, aime_id: int, region: int, date: str) -> Optional[int]: sql = insert(activity).values( - user=aime_id, - region=region, - playCount=1, - created=date + user=aime_id, region=region, playCount=1, created=date ) conflict = sql.on_duplicate_key_update( @@ -407,10 +466,12 @@ class OngekiProfileData(BaseData): result = self.execute(conflict) if result is None: - self.logger.warn(f"put_profile_region: failed to update! aime_id {aime_id} region {region}") + self.logger.warn( + f"put_profile_region: failed to update! aime_id {aime_id} region {region}" + ) return None return result.lastrowid - + def put_training_room(self, aime_id: int, room_detail: Dict) -> Optional[int]: room_detail["user"] = aime_id @@ -422,7 +483,7 @@ class OngekiProfileData(BaseData): self.logger.warn(f"put_best_score: Failed to add score! aime_id: {aime_id}") return None return result.lastrowid - + def put_kop(self, aime_id: int, kop_data: Dict) -> Optional[int]: kop_data["user"] = aime_id @@ -434,17 +495,16 @@ class OngekiProfileData(BaseData): self.logger.warn(f"put_kop: Failed to add score! aime_id: {aime_id}") return None return result.lastrowid - - def put_rival(self, aime_id: int, rival_id: int) -> Optional[int]: - sql = insert(rival).values( - user = aime_id, - rivalUserId = rival_id - ) - conflict = sql.on_duplicate_key_update(rival = rival_id) + def put_rival(self, aime_id: int, rival_id: int) -> Optional[int]: + sql = insert(rival).values(user=aime_id, rivalUserId=rival_id) + + conflict = sql.on_duplicate_key_update(rival=rival_id) result = self.execute(conflict) if result is None: - self.logger.warn(f"put_rival: failed to update! aime_id: {aime_id}, rival_id: {rival_id}") + self.logger.warn( + f"put_rival: failed to update! aime_id: {aime_id}, rival_id: {rival_id}" + ) return None return result.lastrowid diff --git a/titles/ongeki/schema/score.py b/titles/ongeki/schema/score.py index e526005..8bb9fc9 100644 --- a/titles/ongeki/schema/score.py +++ b/titles/ongeki/schema/score.py @@ -11,14 +11,18 @@ score_best = Table( "ongeki_score_best", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("musicId", Integer, nullable=False), Column("level", Integer, nullable=False), - Column("playCount", Integer, nullable=False), + Column("playCount", Integer, nullable=False), Column("techScoreMax", Integer, nullable=False), Column("techScoreRank", Integer, nullable=False), Column("battleScoreMax", Integer, nullable=False), - Column("battleScoreRank", Integer, nullable=False), + Column("battleScoreRank", Integer, nullable=False), Column("maxComboCount", Integer, nullable=False), Column("maxOverKill", Float, nullable=False), Column("maxTeamOverKill", Float, nullable=False), @@ -30,14 +34,18 @@ score_best = Table( Column("isStoryWatched", Boolean, nullable=False), Column("platinumScoreMax", Integer), UniqueConstraint("user", "musicId", "level", name="ongeki_best_score_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) playlog = Table( "ongeki_score_playlog", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("sortNumber", Integer), Column("placeId", Integer), Column("placeName", String(255)), @@ -99,25 +107,30 @@ playlog = Table( Column("battlePoint", Integer), Column("platinumScore", Integer), Column("platinumScoreMax", Integer), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) tech_count = Table( "ongeki_score_tech_count", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("levelId", Integer, nullable=False), Column("allBreakCount", Integer), Column("allBreakPlusCount", Integer), UniqueConstraint("user", "levelId", name="ongeki_tech_count_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) + class OngekiScoreData(BaseData): def get_tech_count(self, aime_id: int) -> Optional[List[Dict]]: return [] - + def put_tech_count(self, aime_id: int, tech_count_data: Dict) -> Optional[int]: tech_count_data["user"] = aime_id @@ -129,17 +142,20 @@ class OngekiScoreData(BaseData): self.logger.warn(f"put_tech_count: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - + def get_best_scores(self, aime_id: int) -> Optional[List[Dict]]: sql = select(score_best).where(score_best.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - def get_best_score(self, aime_id: int, song_id: int, chart_id: int = None) -> Optional[List[Dict]]: + def get_best_score( + self, aime_id: int, song_id: int, chart_id: int = None + ) -> Optional[List[Dict]]: return [] - + def put_best_score(self, aime_id: int, music_detail: Dict) -> Optional[int]: music_detail["user"] = aime_id @@ -161,4 +177,4 @@ class OngekiScoreData(BaseData): if result is None: self.logger.warn(f"put_playlog: Failed to add playlog! aime_id: {aime_id}") return None - return result.lastrowid \ No newline at end of file + return result.lastrowid diff --git a/titles/ongeki/schema/static.py b/titles/ongeki/schema/static.py index 7c5ed68..ed81ebf 100644 --- a/titles/ongeki/schema/static.py +++ b/titles/ongeki/schema/static.py @@ -18,7 +18,7 @@ events = Table( Column("name", String(255)), Column("enabled", Boolean, server_default="1"), UniqueConstraint("version", "eventId", "type", name="ongeki_static_events_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) @@ -34,7 +34,7 @@ music = Table( Column("genre", String(255)), Column("level", Float), UniqueConstraint("version", "songId", "chartId", name="ongeki_static_music_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) gachas = Table( @@ -57,7 +57,7 @@ gachas = Table( Column("noticeEndDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"), Column("convertEndDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"), UniqueConstraint("version", "gachaId", "gachaName", name="ongeki_static_gachas_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) gacha_cards = Table( @@ -71,7 +71,7 @@ gacha_cards = Table( Column("isPickup", Boolean, server_default="0"), Column("isSelect", Boolean, server_default="0"), UniqueConstraint("gachaId", "cardId", name="ongeki_static_gacha_cards_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) cards = Table( @@ -92,16 +92,13 @@ cards = Table( Column("choKaikaSkillId", Integer, nullable=False), Column("cardNumber", String(255)), UniqueConstraint("version", "cardId", name="ongeki_static_cards_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) class OngekiStaticData(BaseData): def put_card(self, version: int, card_id: int, **card_data) -> Optional[int]: - sql = insert(cards).values( - version=version, - cardId=card_id, - **card_data) + sql = insert(cards).values(version=version, cardId=card_id, **card_data) conflict = sql.on_duplicate_key_update(**card_data) @@ -112,10 +109,7 @@ class OngekiStaticData(BaseData): return result.lastrowid def get_card(self, version: int, card_id: int) -> Optional[Dict]: - sql = cards.select(and_( - cards.c.version <= version, - cards.c.cardId == card_id - )) + sql = cards.select(and_(cards.c.version <= version, cards.c.cardId == card_id)) result = self.execute(sql) if result is None: @@ -126,10 +120,9 @@ class OngekiStaticData(BaseData): if not card_number.startswith("[O.N.G.E.K.I.]"): card_number = f"[O.N.G.E.K.I.]{card_number}" - sql = cards.select(and_( - cards.c.version <= version, - cards.c.cardNumber == card_number - )) + sql = cards.select( + and_(cards.c.version <= version, cards.c.cardNumber == card_number) + ) result = self.execute(sql) if result is None: @@ -137,10 +130,7 @@ class OngekiStaticData(BaseData): return result.fetchone() def get_card_by_name(self, version: int, name: str) -> Optional[Dict]: - sql = cards.select(and_( - cards.c.version <= version, - cards.c.name == name - )) + sql = cards.select(and_(cards.c.version <= version, cards.c.name == name)) result = self.execute(sql) if result is None: @@ -156,24 +146,27 @@ class OngekiStaticData(BaseData): return result.fetchall() def get_cards_by_rarity(self, version: int, rarity: int) -> Optional[List[Dict]]: - sql = cards.select(and_( - cards.c.version <= version, - cards.c.rarity == rarity - )) + sql = cards.select(and_(cards.c.version <= version, cards.c.rarity == rarity)) result = self.execute(sql) if result is None: return None return result.fetchall() - def put_gacha(self, version: int, gacha_id: int, gacha_name: int, - gacha_kind: int, **gacha_data) -> Optional[int]: + def put_gacha( + self, + version: int, + gacha_id: int, + gacha_name: int, + gacha_kind: int, + **gacha_data, + ) -> Optional[int]: sql = insert(gachas).values( version=version, gachaId=gacha_id, gachaName=gacha_name, kind=gacha_kind, - **gacha_data + **gacha_data, ) conflict = sql.on_duplicate_key_update( @@ -181,7 +174,7 @@ class OngekiStaticData(BaseData): gachaId=gacha_id, gachaName=gacha_name, kind=gacha_kind, - **gacha_data + **gacha_data, ) result = self.execute(conflict) @@ -191,10 +184,9 @@ class OngekiStaticData(BaseData): return result.lastrowid def get_gacha(self, version: int, gacha_id: int) -> Optional[Dict]: - sql = gachas.select(and_( - gachas.c.version <= version, - gachas.c.gachaId == gacha_id - )) + sql = gachas.select( + and_(gachas.c.version <= version, gachas.c.gachaId == gacha_id) + ) result = self.execute(sql) if result is None: @@ -202,8 +194,7 @@ class OngekiStaticData(BaseData): return result.fetchone() def get_gachas(self, version: int) -> Optional[List[Dict]]: - sql = gachas.select( - gachas.c.version == version).order_by( + sql = gachas.select(gachas.c.version == version).order_by( gachas.c.gachaId.asc() ) @@ -212,17 +203,13 @@ class OngekiStaticData(BaseData): return None return result.fetchall() - def put_gacha_card(self, gacha_id: int, card_id: int, **gacha_card) -> Optional[int]: - sql = insert(gacha_cards).values( - gachaId=gacha_id, - cardId=card_id, - **gacha_card - ) + def put_gacha_card( + self, gacha_id: int, card_id: int, **gacha_card + ) -> Optional[int]: + sql = insert(gacha_cards).values(gachaId=gacha_id, cardId=card_id, **gacha_card) conflict = sql.on_duplicate_key_update( - gachaId=gacha_id, - cardId=card_id, - **gacha_card + gachaId=gacha_id, cardId=card_id, **gacha_card ) result = self.execute(conflict) @@ -232,25 +219,25 @@ class OngekiStaticData(BaseData): return result.lastrowid def get_gacha_cards(self, gacha_id: int) -> Optional[List[Dict]]: - sql = gacha_cards.select( - gacha_cards.c.gachaId == gacha_id - ) + sql = gacha_cards.select(gacha_cards.c.gachaId == gacha_id) result = self.execute(sql) if result is None: return None return result.fetchall() - def put_event(self, version: int, event_id: int, event_type: int, event_name: str) -> Optional[int]: + def put_event( + self, version: int, event_id: int, event_type: int, event_name: str + ) -> Optional[int]: sql = insert(events).values( - version = version, - eventId = event_id, - type = event_type, - name = event_name, + version=version, + eventId=event_id, + type=event_type, + name=event_name, ) conflict = sql.on_duplicate_key_update( - name = event_name, + name=event_name, ) result = self.execute(conflict) @@ -260,63 +247,88 @@ class OngekiStaticData(BaseData): return result.lastrowid def get_event(self, version: int, event_id: int) -> Optional[List[Dict]]: - sql = select(events).where(and_(events.c.version == version, events.c.eventId == event_id)) - + sql = select(events).where( + and_(events.c.version == version, events.c.eventId == event_id) + ) + result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() def get_events(self, version: int) -> Optional[List[Dict]]: sql = select(events).where(events.c.version == version) - + result = self.execute(sql) - if result is None: return None - return result.fetchall() - - def get_enabled_events(self, version: int) -> Optional[List[Dict]]: - sql = select(events).where(and_(events.c.version == version, events.c.enabled == True)) - - result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - def put_chart(self, version: int, song_id: int, chart_id: int, title: str, artist: str, genre: str, level: float) -> Optional[int]: + def get_enabled_events(self, version: int) -> Optional[List[Dict]]: + sql = select(events).where( + and_(events.c.version == version, events.c.enabled == True) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def put_chart( + self, + version: int, + song_id: int, + chart_id: int, + title: str, + artist: str, + genre: str, + level: float, + ) -> Optional[int]: sql = insert(music).values( - version = version, - songId = song_id, - chartId = chart_id, - title = title, - artist = artist, - genre = genre, - level = level, + version=version, + songId=song_id, + chartId=chart_id, + title=title, + artist=artist, + genre=genre, + level=level, ) conflict = sql.on_duplicate_key_update( - title = title, - artist = artist, - genre = genre, - level = level, + title=title, + artist=artist, + genre=genre, + level=level, ) result = self.execute(conflict) if result is None: - self.logger.warn(f"Failed to insert chart! song_id: {song_id}, chart_id: {chart_id}") + self.logger.warn( + f"Failed to insert chart! song_id: {song_id}, chart_id: {chart_id}" + ) return None return result.lastrowid - def get_chart(self, version: int, song_id: int, chart_id: int = None) -> Optional[List[Dict]]: + def get_chart( + self, version: int, song_id: int, chart_id: int = None + ) -> Optional[List[Dict]]: pass def get_music(self, version: int) -> Optional[List[Dict]]: pass - def get_music_chart(self, version: int, song_id: int, chart_id: int) -> Optional[List[Row]]: - sql = select(music).where(and_( - music.c.version == version, - music.c.songId == song_id, - music.c.chartId == chart_id - )) + def get_music_chart( + self, version: int, song_id: int, chart_id: int + ) -> Optional[List[Row]]: + sql = select(music).where( + and_( + music.c.version == version, + music.c.songId == song_id, + music.c.chartId == chart_id, + ) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() diff --git a/titles/ongeki/summer.py b/titles/ongeki/summer.py index 24ed290..adc8c0f 100644 --- a/titles/ongeki/summer.py +++ b/titles/ongeki/summer.py @@ -5,11 +5,12 @@ from titles.ongeki.base import OngekiBase from titles.ongeki.const import OngekiConstants from titles.ongeki.config import OngekiConfig + class OngekiSummer(OngekiBase): def __init__(self, core_cfg: CoreConfig, game_cfg: OngekiConfig) -> None: super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_SUMMER - + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: ret = super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.10.00" diff --git a/titles/ongeki/summerplus.py b/titles/ongeki/summerplus.py index 188e618..8b2cd03 100644 --- a/titles/ongeki/summerplus.py +++ b/titles/ongeki/summerplus.py @@ -5,11 +5,12 @@ from titles.ongeki.base import OngekiBase from titles.ongeki.const import OngekiConstants from titles.ongeki.config import OngekiConfig + class OngekiSummerPlus(OngekiBase): def __init__(self, core_cfg: CoreConfig, game_cfg: OngekiConfig) -> None: super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_SUMMER_PLUS - + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: ret = super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.15.00" diff --git a/titles/pokken/__init__.py b/titles/pokken/__init__.py index 6340de8..ed2ee23 100644 --- a/titles/pokken/__init__.py +++ b/titles/pokken/__init__.py @@ -5,4 +5,4 @@ from titles.pokken.database import PokkenData index = PokkenServlet database = PokkenData game_codes = [PokkenConstants.GAME_CODE] -current_schema_version = 1 \ No newline at end of file +current_schema_version = 1 diff --git a/titles/pokken/base.py b/titles/pokken/base.py index 33232e4..f1f9eb3 100644 --- a/titles/pokken/base.py +++ b/titles/pokken/base.py @@ -6,12 +6,13 @@ from core.config import CoreConfig from titles.pokken.config import PokkenConfig from titles.pokken.proto import jackal_pb2 -class PokkenBase(): + +class PokkenBase: def __init__(self, core_cfg: CoreConfig, game_cfg: PokkenConfig) -> None: self.core_cfg = core_cfg self.game_cfg = game_cfg self.version = 0 - + def handle_noop(self, request: Any) -> bytes: res = jackal_pb2.Response() res.result = 1 @@ -25,7 +26,7 @@ class PokkenBase(): res.type = jackal_pb2.MessageType.PING return res.SerializeToString() - + def handle_register_pcb(self, request: jackal_pb2.RegisterPcbRequestData) -> bytes: res = jackal_pb2.Response() res.result = 1 @@ -37,20 +38,20 @@ class PokkenBase(): "MatchingServer": { "host": f"https://{self.game_cfg.server.hostname}", "port": self.game_cfg.server.port_matching, - "url": "/matching" + "url": "/matching", }, "StunServer": { "addr": self.game_cfg.server.hostname, - "port": self.game_cfg.server.port_stun + "port": self.game_cfg.server.port_stun, }, "TurnServer": { "addr": self.game_cfg.server.hostname, - "port": self.game_cfg.server.port_turn + "port": self.game_cfg.server.port_turn, }, "AdmissionUrl": f"ws://{self.game_cfg.server.hostname}:{self.game_cfg.server.port_admission}", "locationId": 123, "logfilename": "JackalMatchingLibrary.log", - "biwalogfilename": "./biwa.log" + "biwalogfilename": "./biwa.log", } regist_pcb.bnp_baseuri = f"{self.core_cfg.title.hostname}/bna" regist_pcb.biwa_setting = json.dumps(biwa_setting) @@ -66,21 +67,27 @@ class PokkenBase(): return res.SerializeToString() - def handle_save_client_log(self, request: jackal_pb2.SaveClientLogRequestData) -> bytes: + def handle_save_client_log( + self, request: jackal_pb2.SaveClientLogRequestData + ) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.SAVE_CLIENT_LOG return res.SerializeToString() - def handle_check_diagnosis(self, request: jackal_pb2.CheckDiagnosisRequestData) -> bytes: + def handle_check_diagnosis( + self, request: jackal_pb2.CheckDiagnosisRequestData + ) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.CHECK_DIAGNOSIS return res.SerializeToString() - def handle_load_client_settings(self, request: jackal_pb2.CheckDiagnosisRequestData) -> bytes: + def handle_load_client_settings( + self, request: jackal_pb2.CheckDiagnosisRequestData + ) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.LOAD_CLIENT_SETTINGS diff --git a/titles/pokken/config.py b/titles/pokken/config.py index 9b2ae0f..3907838 100644 --- a/titles/pokken/config.py +++ b/titles/pokken/config.py @@ -1,48 +1,72 @@ from core.config import CoreConfig -class PokkenServerConfig(): + +class PokkenServerConfig: def __init__(self, parent_config: "PokkenConfig"): self.__config = parent_config - + @property def hostname(self) -> str: - return CoreConfig.get_config_field(self.__config, 'pokken', 'server', 'hostname', default="localhost") - + return CoreConfig.get_config_field( + self.__config, "pokken", "server", "hostname", default="localhost" + ) + @property def enable(self) -> bool: - return CoreConfig.get_config_field(self.__config, 'pokken', 'server', 'enable', default=True) + return CoreConfig.get_config_field( + self.__config, "pokken", "server", "enable", default=True + ) @property def loglevel(self) -> int: - return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'pokken', 'server', 'loglevel', default="info")) + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "pokken", "server", "loglevel", default="info" + ) + ) @property def port(self) -> int: - return CoreConfig.get_config_field(self.__config, 'pokken', 'server', 'port', default=9000) + return CoreConfig.get_config_field( + self.__config, "pokken", "server", "port", default=9000 + ) @property def port_matching(self) -> int: - return CoreConfig.get_config_field(self.__config, 'pokken', 'server', 'port_matching', default=9001) + return CoreConfig.get_config_field( + self.__config, "pokken", "server", "port_matching", default=9001 + ) @property def port_stun(self) -> int: - return CoreConfig.get_config_field(self.__config, 'pokken', 'server', 'port_stun', default=9002) + return CoreConfig.get_config_field( + self.__config, "pokken", "server", "port_stun", default=9002 + ) @property def port_turn(self) -> int: - return CoreConfig.get_config_field(self.__config, 'pokken', 'server', 'port_turn', default=9003) + return CoreConfig.get_config_field( + self.__config, "pokken", "server", "port_turn", default=9003 + ) @property def port_admission(self) -> int: - return CoreConfig.get_config_field(self.__config, 'pokken', 'server', 'port_admission', default=9004) - + return CoreConfig.get_config_field( + self.__config, "pokken", "server", "port_admission", default=9004 + ) + @property def ssl_cert(self) -> str: - return CoreConfig.get_config_field(self.__config, 'pokken', 'server', 'ssl_cert', default="cert/pokken.crt") + return CoreConfig.get_config_field( + self.__config, "pokken", "server", "ssl_cert", default="cert/pokken.crt" + ) @property def ssl_key(self) -> str: - return CoreConfig.get_config_field(self.__config, 'pokken', 'server', 'ssl_key', default="cert/pokken.key") + return CoreConfig.get_config_field( + self.__config, "pokken", "server", "ssl_key", default="cert/pokken.key" + ) + class PokkenConfig(dict): def __init__(self) -> None: diff --git a/titles/pokken/const.py b/titles/pokken/const.py index 203925c..802a7b9 100644 --- a/titles/pokken/const.py +++ b/titles/pokken/const.py @@ -1,12 +1,12 @@ -class PokkenConstants(): +class PokkenConstants: GAME_CODE = "SDAK" CONFIG_NAME = "pokken.yaml" VER_POKKEN = 0 - VERSION_NAMES = ("Pokken Tournament") + VERSION_NAMES = "Pokken Tournament" @classmethod def game_ver_to_string(cls, ver: int): - return cls.VERSION_NAMES[ver] \ No newline at end of file + return cls.VERSION_NAMES[ver] diff --git a/titles/pokken/database.py b/titles/pokken/database.py index eff928b..f77f172 100644 --- a/titles/pokken/database.py +++ b/titles/pokken/database.py @@ -1,6 +1,7 @@ from core.data import Data from core.config import CoreConfig + class PokkenData(Data): def __init__(self, cfg: CoreConfig) -> None: - super().__init__(cfg) \ No newline at end of file + super().__init__(cfg) diff --git a/titles/pokken/index.py b/titles/pokken/index.py index ccf21de..bbf1204 100644 --- a/titles/pokken/index.py +++ b/titles/pokken/index.py @@ -14,6 +14,7 @@ from titles.pokken.config import PokkenConfig from titles.pokken.base import PokkenBase from titles.pokken.const import PokkenConstants + class PokkenServlet(resource.Resource): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: self.isLeaf = True @@ -27,79 +28,109 @@ class PokkenServlet(resource.Resource): if not hasattr(self.logger, "inited"): log_fmt_str = "[%(asctime)s] Pokken | %(levelname)s | %(message)s" log_fmt = logging.Formatter(log_fmt_str) - fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.core_cfg.server.log_dir, "pokken"), encoding='utf8', - when="d", backupCount=10) + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.core_cfg.server.log_dir, "pokken"), + encoding="utf8", + when="d", + backupCount=10, + ) fileHandler.setFormatter(log_fmt) - + consoleHandler = logging.StreamHandler() consoleHandler.setFormatter(log_fmt) self.logger.addHandler(fileHandler) self.logger.addHandler(consoleHandler) - + self.logger.setLevel(self.game_cfg.server.loglevel) - coloredlogs.install(level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str) + coloredlogs.install( + level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str + ) self.logger.inited = True self.base = PokkenBase(core_cfg, self.game_cfg) @classmethod - def get_allnet_info(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str, str]: + def get_allnet_info( + cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str + ) -> Tuple[bool, str, str]: game_cfg = PokkenConfig() - + if path.exists(f"{cfg_dir}/{PokkenConstants.CONFIG_NAME}"): - game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{PokkenConstants.CONFIG_NAME}"))) + game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{PokkenConstants.CONFIG_NAME}")) + ) if not game_cfg.server.enable: return (False, "", "") - + if core_cfg.server.is_develop: - return (True, f"https://{game_cfg.server.hostname}:{game_cfg.server.port}/{game_code}/$v/", f"{game_cfg.server.hostname}:{game_cfg.server.port}/") - - return (True, f"https://{game_cfg.server.hostname}/{game_code}/$v/", f"{game_cfg.server.hostname}/") + return ( + True, + f"https://{game_cfg.server.hostname}:{game_cfg.server.port}/{game_code}/$v/", + f"{game_cfg.server.hostname}:{game_cfg.server.port}/", + ) + + return ( + True, + f"https://{game_cfg.server.hostname}/{game_code}/$v/", + f"{game_cfg.server.hostname}/", + ) @classmethod - def get_mucha_info(cls, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str, str]: + def get_mucha_info( + cls, core_cfg: CoreConfig, cfg_dir: str + ) -> Tuple[bool, str, str]: game_cfg = PokkenConfig() - + if path.exists(f"{cfg_dir}/{PokkenConstants.CONFIG_NAME}"): - game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{PokkenConstants.CONFIG_NAME}"))) + game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{PokkenConstants.CONFIG_NAME}")) + ) if not game_cfg.server.enable: return (False, "", "") - + return (True, "PKFN") - + def setup(self): """ There's currently no point in having this server on because Twisted - won't play ball with both the fact that it's TLSv1.1, and because the + won't play ball with both the fact that it's TLSv1.1, and because the types of certs that pokken will accept are too flimsy for Twisted so it will throw a fit. Currently leaving this here in case a bypass is discovered in the future, but it's unlikly. For now, just use NGINX. """ - if self.game_cfg.server.enable and self.core_cfg.server.is_develop: + if self.game_cfg.server.enable and self.core_cfg.server.is_develop: key_exists = path.exists(self.game_cfg.server.ssl_key) cert_exists = path.exists(self.game_cfg.server.ssl_cert) - + if key_exists and cert_exists: - endpoints.serverFromString(reactor, f"ssl:{self.game_cfg.server.port}"\ - f":interface={self.core_cfg.server.listen_address}:privateKey={self.game_cfg.server.ssl_key}:"\ - f"certKey={self.game_cfg.server.ssl_cert}")\ - .listen(server.Site(self)) - - self.logger.info(f"Pokken title server ready on port {self.game_cfg.server.port}") + endpoints.serverFromString( + reactor, + f"ssl:{self.game_cfg.server.port}" + f":interface={self.core_cfg.server.listen_address}:privateKey={self.game_cfg.server.ssl_key}:" + f"certKey={self.game_cfg.server.ssl_cert}", + ).listen(server.Site(self)) + + self.logger.info( + f"Pokken title server ready on port {self.game_cfg.server.port}" + ) else: - self.logger.error(f"Could not find cert at {self.game_cfg.server.ssl_key} or key at {self.game_cfg.server.ssl_cert}, Pokken not running.") + self.logger.error( + f"Could not find cert at {self.game_cfg.server.ssl_key} or key at {self.game_cfg.server.ssl_cert}, Pokken not running." + ) - def render_POST(self, request: Request, version: int = 0, endpoints: str = "") -> bytes: + def render_POST( + self, request: Request, version: int = 0, endpoints: str = "" + ) -> bytes: if endpoints == "": endpoints = request.uri.decode() if endpoints.startswith("/matching"): self.logger.info("Matching request") - + content = request.content.getvalue() if content == b"": self.logger.info("Empty request") @@ -113,7 +144,7 @@ class PokkenServlet(resource.Resource): return b"" endpoint = jackal_pb2.MessageType(pokken_request.type).name.lower() - + self.logger.info(f"{endpoint} request") handler = getattr(self.base, f"handle_{endpoint}", None) diff --git a/titles/pokken/proto/jackal_pb2.py b/titles/pokken/proto/jackal_pb2.py index 782403d..9a2e30e 100644 --- a/titles/pokken/proto/jackal_pb2.py +++ b/titles/pokken/proto/jackal_pb2.py @@ -6,134 +6,134 @@ from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database + # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0cjackal.proto\x12\x0fjackal.protobuf\"\xae\x0b\n\x07Request\x12*\n\x04type\x18\x01 \x02(\x0e\x32\x1c.jackal.protobuf.MessageType\x12.\n\x04noop\x18\x02 \x01(\x0b\x32 .jackal.protobuf.NoopRequestData\x12.\n\x04ping\x18\x03 \x01(\x0b\x32 .jackal.protobuf.PingRequestData\x12=\n\x0cregister_pcb\x18\x04 \x01(\x0b\x32\'.jackal.protobuf.RegisterPcbRequestData\x12\x35\n\x08save_ads\x18\x05 \x01(\x0b\x32#.jackal.protobuf.SaveAdsRequestData\x12\x46\n\x11\x63heck_access_code\x18\x06 \x01(\x0b\x32+.jackal.protobuf.CheckAccessCodeRequestData\x12\x46\n\x11set_bnpassid_lock\x18\x07 \x01(\x0b\x32+.jackal.protobuf.SetBnpassIdLockRequestData\x12\x37\n\tload_user\x18\x08 \x01(\x0b\x32$.jackal.protobuf.LoadUserRequestData\x12\x37\n\tsave_user\x18\n \x01(\x0b\x32$.jackal.protobuf.SaveUserRequestData\x12\x43\n\x0f\x63heck_diagnosis\x18\x0b \x01(\x0b\x32*.jackal.protobuf.CheckDiagnosisRequestData\x12\x42\n\x0fsave_client_log\x18\x0c \x01(\x0b\x32).jackal.protobuf.SaveClientLogRequestData\x12L\n\x14pre_load_information\x18\r \x01(\x0b\x32..jackal.protobuf.PreLoadInformationRequestData\x12\x45\n\x10load_information\x18\x0e \x01(\x0b\x32+.jackal.protobuf.LoadInformationRequestData\x12\x42\n\x0fpre_save_replay\x18\x0f \x01(\x0b\x32).jackal.protobuf.PreSaveReplayRequestData\x12;\n\x0bsave_replay\x18\x10 \x01(\x0b\x32&.jackal.protobuf.SaveReplayRequestData\x12;\n\x0bsave_charge\x18\x11 \x01(\x0b\x32&.jackal.protobuf.SaveChargeRequestData\x12?\n\rcheck_ranking\x18\x12 \x01(\x0b\x32(.jackal.protobuf.CheckRankingRequestData\x12=\n\x0cload_ranking\x18\x13 \x01(\x0b\x32\'.jackal.protobuf.LoadRankingRequestData\x12\x42\n\x0fsave_ingame_log\x18\x14 \x01(\x0b\x32).jackal.protobuf.SaveInGameLogRequestData\x12[\n\x1cpre_load_information_attract\x18\x15 \x01(\x0b\x32\x35.jackal.protobuf.PreLoadInformationAttractRequestData\x12T\n\x18load_information_attract\x18\x16 \x01(\x0b\x32\x32.jackal.protobuf.LoadInformationAttractRequestData\x12L\n\x14load_client_settings\x18\x17 \x01(\x0b\x32..jackal.protobuf.LoadClientSettingsRequestData\"\xd4\x0b\n\x08Response\x12*\n\x04type\x18\x01 \x02(\x0e\x32\x1c.jackal.protobuf.MessageType\x12\x0e\n\x06result\x18\x02 \x02(\r\x12/\n\x04noop\x18\x03 \x01(\x0b\x32!.jackal.protobuf.NoopResponseData\x12/\n\x04ping\x18\x04 \x01(\x0b\x32!.jackal.protobuf.PingResponseData\x12>\n\x0cregister_pcb\x18\x05 \x01(\x0b\x32(.jackal.protobuf.RegisterPcbResponseData\x12\x36\n\x08save_ads\x18\x06 \x01(\x0b\x32$.jackal.protobuf.SaveAdsResponseData\x12G\n\x11\x63heck_access_code\x18\x07 \x01(\x0b\x32,.jackal.protobuf.CheckAccessCodeResponseData\x12G\n\x11set_bnpassid_lock\x18\x08 \x01(\x0b\x32,.jackal.protobuf.SetBnpassIdLockResponseData\x12\x38\n\tload_user\x18\t \x01(\x0b\x32%.jackal.protobuf.LoadUserResponseData\x12\x38\n\tsave_user\x18\x0b \x01(\x0b\x32%.jackal.protobuf.SaveUserResponseData\x12\x44\n\x0f\x63heck_diagnosis\x18\x0c \x01(\x0b\x32+.jackal.protobuf.CheckDiagnosisResponseData\x12\x43\n\x0fsave_client_log\x18\r \x01(\x0b\x32*.jackal.protobuf.SaveClientLogResponseData\x12M\n\x14pre_load_information\x18\x0e \x01(\x0b\x32/.jackal.protobuf.PreLoadInformationResponseData\x12\x46\n\x10load_information\x18\x0f \x01(\x0b\x32,.jackal.protobuf.LoadInformationResponseData\x12\x43\n\x0fpre_save_replay\x18\x10 \x01(\x0b\x32*.jackal.protobuf.PreSaveReplayResponseData\x12<\n\x0bsave_replay\x18\x11 \x01(\x0b\x32\'.jackal.protobuf.SaveReplayResponseData\x12<\n\x0bsave_charge\x18\x12 \x01(\x0b\x32\'.jackal.protobuf.SaveChargeResponseData\x12@\n\rcheck_ranking\x18\x13 \x01(\x0b\x32).jackal.protobuf.CheckRankingResponseData\x12>\n\x0cload_ranking\x18\x14 \x01(\x0b\x32(.jackal.protobuf.LoadRankingResponseData\x12\x43\n\x0fsave_ingame_log\x18\x15 \x01(\x0b\x32*.jackal.protobuf.SaveInGameLogResponseData\x12\\\n\x1cpre_load_information_attract\x18\x16 \x01(\x0b\x32\x36.jackal.protobuf.PreLoadInformationAttractResponseData\x12U\n\x18load_information_attract\x18\x17 \x01(\x0b\x32\x33.jackal.protobuf.LoadInformationAttractResponseData\x12M\n\x14load_client_settings\x18\x18 \x01(\x0b\x32/.jackal.protobuf.LoadClientSettingsResponseData\"1\n\x0fNoopRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\"\x12\n\x10NoopResponseData\"1\n\x0fPingRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\"\x12\n\x10PingResponseData\"\xce\x02\n\x16RegisterPcbRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x10\n\x08pcb_type\x18\x02 \x02(\r\x12\x15\n\rlocation_name\x18\x03 \x02(\t\x12\x19\n\x11location_nickname\x18\x04 \x02(\t\x12\x11\n\tpref_code\x18\x05 \x01(\r\x12\r\n\x05\x61\x64\x64r0\x18\x06 \x01(\t\x12\r\n\x05\x61\x64\x64r1\x18\x07 \x01(\t\x12\r\n\x05\x61\x64\x64r2\x18\x08 \x01(\t\x12\r\n\x05\x61\x64\x64r3\x18\t \x01(\t\x12\x0e\n\x06loc_id\x18\n \x02(\t\x12\x14\n\x0c\x63ountry_code\x18\x0b \x02(\t\x12\x13\n\x0bregion_code\x18\x0c \x02(\r\x12\r\n\x05karma\x18\r \x02(\x05\x12\x0f\n\x07game_id\x18\x0e \x02(\t\x12\x10\n\x08game_ver\x18\x0f \x02(\t\x12\x10\n\x08\x64isk_ver\x18\x10 \x02(\t\x12\x12\n\nutc_offset\x18\x11 \x02(\x05\"\x85\x01\n\x17RegisterPcbResponseData\x12\x13\n\x0bserver_time\x18\x01 \x02(\r\x12\x15\n\roperate_start\x18\x02 \x01(\t\x12\x13\n\x0boperate_end\x18\x03 \x01(\t\x12\x13\n\x0b\x62np_baseuri\x18\x04 \x01(\t\x12\x14\n\x0c\x62iwa_setting\x18\x05 \x02(\t\"\xd7\x02\n\x12SaveAdsRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x17\n\x0f\x61\x64s_start_count\x18\x03 \x02(\r\x12\x16\n\x0e\x61\x64s_coin_count\x18\x04 \x02(\r\x12\x19\n\x11\x61\x64s_service_count\x18\x05 \x02(\r\x12\x1a\n\x12\x61\x64s_freeplay_count\x18\x06 \x02(\r\x12\x1a\n\x12\x61\x64s_operation_days\x18\x07 \x02(\r\x12\x1a\n\x12\x61\x64s_power_on_count\x18\x08 \x02(\r\x12\x46\n\rads_play_time\x18\t \x03(\x0b\x32/.jackal.protobuf.SaveAdsRequestData.AdsPlayTime\x1a\x39\n\x0b\x41\x64sPlayTime\x12\x12\n\npokemon_id\x18\x65 \x02(\r\x12\x16\n\x0e\x63harplay_count\x18\x66 \x02(\r\"\x15\n\x13SaveAdsResponseData\"M\n\x1a\x43heckAccessCodeRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x0f\n\x07\x63hip_id\x18\x03 \x02(\t\"M\n\x1b\x43heckAccessCodeResponseData\x12\x19\n\x11\x63ommidserv_result\x18\x01 \x02(\r\x12\x13\n\x0b\x61\x63\x63\x65ss_code\x18\x02 \x02(\t\"\xa4\x01\n\x1aSetBnpassIdLockRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x13\n\x0b\x62\x61napass_id\x18\x03 \x02(\r\x12\x13\n\x0b\x64\x65vice_type\x18\x04 \x02(\r\x12\x0f\n\x07\x63hip_id\x18\x05 \x02(\t\x12\x13\n\x0b\x61\x63\x63\x65ss_code\x18\x06 \x01(\t\x12\x16\n\x0e\x63\x61rd_lock_time\x18\x07 \x02(\r\"\x1d\n\x1bSetBnpassIdLockResponseData\"\x85\x01\n\x13LoadUserRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x13\n\x0b\x64\x65vice_type\x18\x03 \x02(\r\x12\x0f\n\x07\x63hip_id\x18\x04 \x02(\t\x12\x13\n\x0b\x61\x63\x63\x65ss_code\x18\x05 \x01(\t\x12\x13\n\x0b\x63\x61rd_status\x18\x06 \x02(\x08\"\xfc\x12\n\x14LoadUserResponseData\x12\x19\n\x11\x63ommidserv_result\x18\x01 \x02(\r\x12\x11\n\tload_hash\x18\x02 \x02(\r\x12\x17\n\x0f\x63\x61rdlock_status\x18\x03 \x02(\x08\x12\x13\n\x0b\x62\x61napass_id\x18\x04 \x02(\r\x12\x13\n\x0b\x61\x63\x63\x65ss_code\x18\x05 \x01(\t\x12\x15\n\rnew_card_flag\x18\x06 \x02(\x08\x12\x1e\n\x16precedent_release_flag\x18\x07 \x02(\r\x12\x18\n\x10navi_newbie_flag\x18\x08 \x02(\x08\x12\x18\n\x10navi_enable_flag\x18\t \x02(\x08\x12\x18\n\x10pad_vibrate_flag\x18\n \x02(\x08\x12\x18\n\x10home_region_code\x18\x0b \x02(\r\x12\x15\n\rhome_loc_name\x18\x0c \x02(\t\x12\x11\n\tpref_code\x18\r \x02(\r\x12\x14\n\x0ctrainer_name\x18\x0e \x01(\t\x12\x1a\n\x12trainer_rank_point\x18\x0f \x02(\r\x12\x0e\n\x06wallet\x18\x10 \x02(\r\x12\x13\n\x0b\x66ight_money\x18\x11 \x02(\r\x12\x13\n\x0bscore_point\x18\x12 \x02(\r\x12\x15\n\rgrade_max_num\x18\x13 \x02(\r\x12\x15\n\rextra_counter\x18\x14 \x01(\r\x12\x1e\n\x16tutorial_progress_flag\x18\x15 \x03(\r\x12\x17\n\x0ftotal_play_days\x18\x16 \x02(\r\x12\x16\n\x0eplay_date_time\x18\x17 \x02(\r\x12\x1a\n\x12lucky_box_fail_num\x18\x18 \x02(\r\x12\x1d\n\x15\x65vent_reward_get_flag\x18\x19 \x02(\r\x12\x14\n\x0crank_pvp_all\x18\x1a \x02(\r\x12\x14\n\x0crank_pvp_loc\x18\x1b \x02(\r\x12\x14\n\x0crank_cpu_all\x18\x1c \x02(\r\x12\x14\n\x0crank_cpu_loc\x18\x1d \x02(\r\x12\x12\n\nrank_event\x18\x1e \x02(\r\x12\x11\n\tawake_num\x18\x1f \x02(\r\x12\x17\n\x0fuse_support_num\x18 \x02(\r\x12\x16\n\x0erankmatch_flag\x18! \x02(\r\x12\x1a\n\x12rankmatch_progress\x18\" \x03(\r\x12\x15\n\rrankmatch_max\x18# \x01(\r\x12\x19\n\x11rankmatch_success\x18$ \x01(\r\x12\x10\n\x08\x62\x65\x61t_num\x18% \x01(\x05\x12\x15\n\rtitle_text_id\x18& \x02(\r\x12\x16\n\x0etitle_plate_id\x18\' \x02(\r\x12\x1b\n\x13title_decoration_id\x18( \x02(\r\x12\x1c\n\x14support_pokemon_list\x18) \x03(\r\x12\x15\n\rsupport_set_1\x18* \x03(\r\x12\x15\n\rsupport_set_2\x18+ \x03(\r\x12\x15\n\rsupport_set_3\x18, \x03(\r\x12\x14\n\x0cnavi_trainer\x18- \x02(\r\x12\x17\n\x0fnavi_version_id\x18. \x02(\r\x12\x16\n\x0e\x61id_skill_list\x18/ \x03(\r\x12\x11\n\taid_skill\x18\x30 \x02(\r\x12\x17\n\x0f\x63omment_text_id\x18\x31 \x02(\r\x12\x17\n\x0f\x63omment_word_id\x18\x32 \x02(\r\x12\x1a\n\x12latest_use_pokemon\x18\x33 \x02(\r\x12\x11\n\tex_ko_num\x18\x34 \x02(\r\x12\x0f\n\x07wko_num\x18\x35 \x02(\r\x12\x16\n\x0etimeup_win_num\x18\x36 \x02(\r\x12\x13\n\x0b\x63ool_ko_num\x18\x37 \x02(\r\x12\x16\n\x0eperfect_ko_num\x18\x38 \x02(\r\x12\x13\n\x0brecord_flag\x18\x39 \x02(\r\x12\x1c\n\x14site_register_status\x18: \x02(\r\x12\x14\n\x0c\x63ontinue_num\x18; \x02(\r\x12\x18\n\x10\x61\x63hievement_flag\x18< \x03(\r\x12\x13\n\x0b\x61vatar_body\x18= \x01(\r\x12\x15\n\ravatar_gender\x18> \x01(\r\x12\x19\n\x11\x61vatar_background\x18? \x01(\r\x12\x13\n\x0b\x61vatar_head\x18@ \x01(\r\x12\x1a\n\x12\x61vatar_battleglass\x18\x41 \x01(\r\x12\x14\n\x0c\x61vatar_face0\x18\x42 \x01(\r\x12\x14\n\x0c\x61vatar_face1\x18\x43 \x01(\r\x12\x14\n\x0c\x61vatar_face2\x18\x44 \x01(\r\x12\x16\n\x0e\x61vatar_bodyall\x18\x45 \x01(\r\x12\x13\n\x0b\x61vatar_wear\x18\x46 \x01(\r\x12\x18\n\x10\x61vatar_accessory\x18G \x01(\r\x12\x14\n\x0c\x61vatar_stamp\x18H \x01(\r\x12G\n\x0cpokemon_data\x18I \x03(\x0b\x32\x31.jackal.protobuf.LoadUserResponseData.PokemonData\x12\x13\n\x0b\x65vent_state\x18J \x02(\r\x12\x10\n\x08\x65vent_id\x18K \x02(\r\x12\x1e\n\x16sp_bonus_category_id_1\x18L \x02(\r\x12\x1c\n\x14sp_bonus_key_value_1\x18M \x02(\r\x12\x1e\n\x16sp_bonus_category_id_2\x18N \x02(\r\x12\x1c\n\x14sp_bonus_key_value_2\x18O \x02(\r\x12\x1a\n\x12last_play_event_id\x18P \x01(\r\x12\x1e\n\x16\x65vent_achievement_flag\x18Q \x03(\r\x12\x1f\n\x17\x65vent_achievement_param\x18R \x03(\r\x1a\xf0\x02\n\x0bPokemonData\x12\x0f\n\x07\x63har_id\x18\x65 \x02(\r\x12\x1c\n\x14illustration_book_no\x18\x66 \x02(\r\x12\x13\n\x0bpokemon_exp\x18g \x02(\r\x12\x19\n\x11\x62\x61ttle_num_vs_wan\x18h \x02(\r\x12\x12\n\nwin_vs_wan\x18i \x02(\r\x12\x19\n\x11\x62\x61ttle_num_vs_lan\x18j \x02(\r\x12\x12\n\nwin_vs_lan\x18k \x02(\r\x12\x19\n\x11\x62\x61ttle_num_vs_cpu\x18l \x02(\r\x12\x0f\n\x07win_cpu\x18m \x02(\r\x12\x1f\n\x17\x62\x61ttle_all_num_tutorial\x18n \x02(\r\x12\x1b\n\x13\x62\x61ttle_num_tutorial\x18o \x02(\r\x12\x14\n\x0c\x62p_point_atk\x18p \x02(\r\x12\x14\n\x0c\x62p_point_res\x18q \x02(\r\x12\x14\n\x0c\x62p_point_def\x18r \x02(\r\x12\x13\n\x0b\x62p_point_sp\x18s \x02(\r\"\x98\x0c\n\x13SaveUserRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x13\n\x0b\x62\x61napass_id\x18\x03 \x02(\r\x12\x1e\n\x16get_trainer_rank_point\x18\x04 \x01(\x05\x12\x11\n\tget_money\x18\x05 \x02(\r\x12\x17\n\x0fget_score_point\x18\x06 \x01(\r\x12\x15\n\rgrade_max_num\x18\x07 \x01(\r\x12\x15\n\rextra_counter\x18\x08 \x01(\r\x12\x1e\n\x16tutorial_progress_flag\x18\t \x03(\r\x12\x1d\n\x15\x65vent_reward_get_flag\x18\n \x01(\r\x12\x14\n\x0c\x63ontinue_num\x18\x0b \x02(\r\x12\x17\n\x0ftotal_play_days\x18\x0c \x02(\r\x12\x18\n\x10\x61\x63hievement_flag\x18\r \x03(\r\x12\x11\n\tawake_num\x18\x0e \x02(\r\x12\x17\n\x0fuse_support_num\x18\x0f \x02(\r\x12\x16\n\x0erankmatch_flag\x18\x10 \x02(\r\x12\x1a\n\x12rank_match_process\x18\x11 \x03(\r\x12\x16\n\x0erank_match_max\x18\x12 \x01(\r\x12\x1a\n\x12rank_match_success\x18\x13 \x01(\r\x12\x10\n\x08\x62\x65\x61t_num\x18\x14 \x01(\x05\x12\x15\n\rsupport_set_1\x18\x15 \x03(\r\x12\x15\n\rsupport_set_2\x18\x16 \x03(\r\x12\x15\n\rsupport_set_3\x18\x17 \x03(\r\x12\x44\n\x0b\x62\x61ttle_data\x18\x18 \x02(\x0b\x32/.jackal.protobuf.SaveUserRequestData.BattleData\x12\x46\n\x0cpokemon_data\x18\x19 \x02(\x0b\x32\x30.jackal.protobuf.SaveUserRequestData.PokemonData\x12\x1c\n\x14trainer_name_pending\x18\x1a \x01(\t\x12\x15\n\ravatar_gender\x18\x1b \x01(\r\x12\x15\n\rcontinue_flag\x18\x1c \x02(\x08\x12\x14\n\x0creq_sendtime\x18\x1d \x02(\r\x12\x15\n\rplay_all_time\x18\x1e \x02(\r\x12\x11\n\tload_hash\x18\x1f \x02(\r\x12\x44\n\x0breward_data\x18 \x03(\x0b\x32/.jackal.protobuf.SaveUserRequestData.RewardData\x12\x13\n\x0b\x65vent_state\x18! \x01(\r\x12\x11\n\taid_skill\x18\" \x01(\r\x12\x1a\n\x12last_play_event_id\x18# \x01(\r\x12\x1e\n\x16\x65vent_achievement_flag\x18$ \x03(\r\x12\x1f\n\x17\x65vent_achievement_param\x18% \x03(\r\x1a\xec\x01\n\nBattleData\x12\x11\n\tplay_mode\x18\x65 \x03(\r\x12\x0e\n\x06result\x18\x66 \x03(\r\x12\x11\n\tex_ko_num\x18g \x02(\r\x12\x0f\n\x07wko_num\x18h \x02(\r\x12\x16\n\x0etimeup_win_num\x18i \x02(\r\x12\x13\n\x0b\x63ool_ko_num\x18j \x02(\r\x12\x16\n\x0eperfect_ko_num\x18k \x02(\r\x12\x10\n\x08use_navi\x18l \x02(\r\x12\x16\n\x0euse_navi_cloth\x18m \x02(\r\x12\x15\n\ruse_aid_skill\x18n \x02(\r\x12\x11\n\tplay_date\x18o \x02(\r\x1a\xb3\x01\n\x0bPokemonData\x12\x10\n\x07\x63har_id\x18\xc9\x01 \x02(\r\x12\x1d\n\x14illustration_book_no\x18\xca\x01 \x02(\r\x12\x18\n\x0fget_pokemon_exp\x18\xcb\x01 \x02(\r\x12\x15\n\x0c\x62p_point_atk\x18\xcc\x01 \x02(\r\x12\x15\n\x0c\x62p_point_res\x18\xcd\x01 \x02(\r\x12\x15\n\x0c\x62p_point_def\x18\xce\x01 \x02(\r\x12\x14\n\x0b\x62p_point_sp\x18\xcf\x01 \x02(\r\x1aU\n\nRewardData\x12\x18\n\x0fget_category_id\x18\xad\x02 \x02(\r\x12\x17\n\x0eget_content_id\x18\xae\x02 \x02(\r\x12\x14\n\x0bget_type_id\x18\xaf\x02 \x02(\r\"\x16\n\x14SaveUserResponseData\";\n\x19\x43heckDiagnosisRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\"\x95\x02\n\x1a\x43heckDiagnosisResponseData\x12Q\n\x0e\x64iagnosis_data\x18\x01 \x03(\x0b\x32\x39.jackal.protobuf.CheckDiagnosisResponseData.DiagnosisData\x1a\xa3\x01\n\rDiagnosisData\x12\x14\n\x0crequest_type\x18\x65 \x02(\r\x12\x17\n\x0f\x63onnect_timeout\x18\x66 \x02(\r\x12\x14\n\x0csend_timeout\x18g \x02(\r\x12\x17\n\x0freceive_timeout\x18h \x02(\r\x12\x1c\n\x14retry_time_of_number\x18i \x02(\r\x12\x16\n\x0eretry_interval\x18j \x02(\r\"\xf4\x01\n\x18SaveClientLogRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x11\n\tserial_id\x18\x03 \x02(\r\x12\x14\n\x0creq_sendtime\x18\x04 \x02(\r\x12\r\n\x05karma\x18\x05 \x02(\x05\x12\x14\n\x0crequest_type\x18\x06 \x02(\r\x12\x1f\n\x17request_number_of_times\x18\x07 \x02(\r\x12\x1f\n\x17timeout_number_of_times\x18\x08 \x02(\r\x12\x11\n\tretry_max\x18\t \x02(\r\x12\x15\n\rresponse_time\x18\n \x02(\r\"\x1b\n\x19SaveClientLogResponseData\"V\n\x1dPreLoadInformationRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x15\n\rinfo_small_id\x18\x03 \x02(\r\"\xc9\x01\n\x1ePreLoadInformationResponseData\x12\x15\n\rinfo_small_id\x18\x01 \x02(\r\x12\x13\n\x0bregion_code\x18\x02 \x02(\r\x12\x12\n\nsession_id\x18\x03 \x02(\r\x12\x13\n\x0b\x62lock_total\x18\x04 \x02(\r\x12\x12\n\nblock_size\x18\x05 \x02(\r\x12\x10\n\x08interval\x18\x06 \x02(\r\x12\x16\n\x0einfo_data_size\x18\x07 \x02(\r\x12\x14\n\x0cinfo_data_id\x18\x08 \x02(\r\"v\n\x1aLoadInformationRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x15\n\rinfo_small_id\x18\x03 \x02(\r\x12\x12\n\nsession_id\x18\x04 \x02(\r\x12\r\n\x05\x62lock\x18\x05 \x02(\r\"\x96\x01\n\x1bLoadInformationResponseData\x12\x15\n\rinfo_small_id\x18\x01 \x02(\r\x12\x12\n\nstart_date\x18\x02 \x02(\r\x12\x10\n\x08\x65nd_date\x18\x03 \x02(\r\x12\r\n\x05\x62lock\x18\x04 \x02(\r\x12\x13\n\x0b\x62lock_total\x18\x05 \x02(\r\x12\x16\n\x0einfo_data_body\x18\x06 \x02(\x0c\"\x80\x01\n\x18PreSaveReplayRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x13\n\x0bregion_code\x18\x03 \x02(\r\x12\x15\n\rcategory_code\x18\x04 \x02(\r\x12\x18\n\x10replay_data_size\x18\x05 \x02(\r\"j\n\x19PreSaveReplayResponseData\x12\x12\n\nsession_id\x18\x01 \x02(\r\x12\x13\n\x0b\x62lock_total\x18\x02 \x02(\r\x12\x12\n\nblock_size\x18\x03 \x02(\r\x12\x10\n\x08interval\x18\x04 \x02(\r\"\x82\x02\n\x15SaveReplayRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x13\n\x0b\x62\x61napass_id\x18\x03 \x02(\r\x12\x12\n\npokemon_id\x18\x04 \x02(\r\x12\x17\n\x0ftrainer_rank_id\x18\x05 \x02(\r\x12\x13\n\x0bregion_code\x18\x06 \x02(\r\x12\x12\n\nsession_id\x18\x07 \x02(\r\x12\r\n\x05\x62lock\x18\x08 \x02(\r\x12\x1b\n\x13transfer_completion\x18\t \x02(\r\x12\x18\n\x10replay_data_size\x18\n \x02(\r\x12\x18\n\x10replay_data_body\x18\x0b \x02(\x0c\"\x18\n\x16SaveReplayResponseData\"\x8d\x01\n\x15SaveChargeRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x0f\n\x07game_id\x18\x03 \x02(\t\x12\x19\n\x11\x63harge_data_index\x18\x04 \x02(\t\x12\x13\n\x0b\x63harge_type\x18\x05 \x02(\r\x12\x13\n\x0b\x63harge_time\x18\x06 \x02(\r\"N\n\x16SaveChargeResponseData\x12\x19\n\x11\x63harge_error_code\x18\x01 \x02(\r\x12\x19\n\x11\x63harge_data_index\x18\x02 \x02(\t\"u\n\x17\x43heckRankingRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x13\n\x0bregion_code\x18\x03 \x02(\r\x12\x12\n\nranking_id\x18\x04 \x02(\r\x12\x11\n\ttimestamp\x18\x05 \x02(\r\".\n\x18\x43heckRankingResponseData\x12\x12\n\nranking_id\x18\x01 \x02(\r\"a\n\x16LoadRankingRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x13\n\x0bregion_code\x18\x03 \x02(\r\x12\x12\n\nranking_id\x18\x04 \x02(\r\"\xbd\x08\n\x17LoadRankingResponseData\x12\x12\n\nranking_id\x18\x01 \x02(\r\x12\x15\n\rranking_start\x18\x02 \x02(\r\x12\x13\n\x0branking_end\x18\x03 \x02(\r\x12\x11\n\tevent_end\x18\x04 \x02(\x08\x12J\n\x0ctrainer_data\x18\x05 \x03(\x0b\x32\x34.jackal.protobuf.LoadRankingResponseData.TrainerData\x12\x13\n\x0bmodify_date\x18\x06 \x02(\r\x12\x13\n\x0b\x65vent_state\x18\x07 \x01(\r\x1a\xd8\x06\n\x0bTrainerData\x12\x14\n\x0ctrainer_name\x18\x65 \x02(\t\x12\x1a\n\x12trainer_rank_point\x18\x66 \x02(\r\x12\r\n\x05point\x18g \x02(\r\x12\x13\n\x0brecord_flag\x18h \x02(\r\x12\x18\n\x10\x66\x61vorite_pokemon\x18i \x02(\r\x12\x12\n\nwin_vs_wan\x18j \x02(\r\x12\x19\n\x11\x62\x61ttle_num_vs_wan\x18k \x02(\r\x12\x12\n\nwin_vs_cpu\x18l \x02(\r\x12\x19\n\x11\x62\x61ttle_num_vs_cpu\x18m \x02(\r\x12\x15\n\rtitle_text_id\x18n \x02(\r\x12\x16\n\x0etitle_plate_id\x18o \x02(\r\x12\x1b\n\x13title_decoration_id\x18p \x02(\r\x12\x17\n\x0f\x63omment_text_id\x18q \x02(\r\x12\x17\n\x0f\x63omment_word_id\x18r \x02(\r\x12\x10\n\x08loc_name\x18s \x02(\t\x12\x11\n\tpref_code\x18t \x02(\r\x12\x10\n\x08rank_num\x18u \x02(\r\x12\x15\n\rlast_rank_num\x18v \x02(\r\x12\x0e\n\x06updown\x18w \x02(\r\x12\x12\n\npokemon_id\x18x \x02(\r\x12\x13\n\x0bpokemon_exp\x18y \x02(\r\x12\x14\n\x0c\x62p_point_atk\x18z \x02(\r\x12\x14\n\x0c\x62p_point_res\x18{ \x02(\r\x12\x14\n\x0c\x62p_point_def\x18| \x02(\r\x12\x13\n\x0b\x62p_point_sp\x18} \x02(\r\x12\x13\n\x0b\x61vatar_body\x18~ \x02(\r\x12\x15\n\ravatar_gender\x18\x7f \x02(\r\x12\x1a\n\x11\x61vatar_background\x18\x80\x01 \x02(\r\x12\x14\n\x0b\x61vatar_head\x18\x81\x01 \x02(\r\x12\x1b\n\x12\x61vatar_battleglass\x18\x82\x01 \x02(\r\x12\x15\n\x0c\x61vatar_face0\x18\x83\x01 \x02(\r\x12\x15\n\x0c\x61vatar_face1\x18\x84\x01 \x02(\r\x12\x15\n\x0c\x61vatar_face2\x18\x85\x01 \x02(\r\x12\x17\n\x0e\x61vatar_bodyall\x18\x86\x01 \x02(\r\x12\x14\n\x0b\x61vatar_wear\x18\x87\x01 \x02(\r\x12\x19\n\x10\x61vatar_accessory\x18\x88\x01 \x02(\r\x12\x15\n\x0c\x61vatar_stamp\x18\x89\x01 \x02(\r\"h\n\x18SaveInGameLogRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x13\n\x0bin_game_log\x18\x03 \x02(\x0c\x12\x17\n\x0flog_change_time\x18\x04 \x02(\r\"\x1b\n\x19SaveInGameLogResponseData\"]\n$PreLoadInformationAttractRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x15\n\rinfo_large_id\x18\x03 \x02(\r\"\xd0\x01\n%PreLoadInformationAttractResponseData\x12\x15\n\rinfo_large_id\x18\x01 \x02(\r\x12\x13\n\x0bregion_code\x18\x02 \x02(\r\x12\x12\n\nsession_id\x18\x03 \x02(\r\x12\x13\n\x0b\x62lock_total\x18\x04 \x02(\r\x12\x12\n\nblock_size\x18\x05 \x02(\r\x12\x10\n\x08interval\x18\x06 \x02(\r\x12\x16\n\x0einfo_data_size\x18\x07 \x02(\r\x12\x14\n\x0cinfo_data_id\x18\x08 \x02(\r\"}\n!LoadInformationAttractRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x15\n\rinfo_large_id\x18\x03 \x02(\r\x12\x12\n\nsession_id\x18\x04 \x02(\r\x12\r\n\x05\x62lock\x18\x05 \x02(\r\"\x9d\x01\n\"LoadInformationAttractResponseData\x12\x15\n\rinfo_large_id\x18\x01 \x02(\r\x12\x12\n\nstart_date\x18\x02 \x02(\r\x12\x10\n\x08\x65nd_date\x18\x03 \x02(\r\x12\r\n\x05\x62lock\x18\x04 \x02(\r\x12\x13\n\x0b\x62lock_total\x18\x05 \x02(\r\x12\x16\n\x0einfo_data_body\x18\x06 \x02(\x0c\"?\n\x1dLoadClientSettingsRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\"\x8d\x10\n\x1eLoadClientSettingsResponseData\x12\x1b\n\x13money_magnification\x18\x01 \x02(\r\x12!\n\x19\x64m2_probability_single100\x18\x02 \x03(\r\x12\x1a\n\x12\x63ontinue_bonus_exp\x18\x03 \x02(\r\x12\x1c\n\x14\x63ontinue_fight_money\x18\x04 \x02(\r\x12\x17\n\x0f\x65vent_bonus_exp\x18\x05 \x02(\r\x12\x11\n\tlevel_cap\x18\x06 \x02(\r\x12\x15\n\rop_movie_flag\x18\x07 \x02(\r\x12M\n\nevent_info\x18\x08 \x03(\x0b\x32\x39.jackal.protobuf.LoadClientSettingsResponseData.EventInfo\x12O\n\x0b\x62\x61nner_info\x18\t \x03(\x0b\x32:.jackal.protobuf.LoadClientSettingsResponseData.BannerInfo\x12Q\n\x0c\x61ttract_info\x18\n \x03(\x0b\x32;.jackal.protobuf.LoadClientSettingsResponseData.AttractInfo\x12O\n\x0binfo_window\x18\x0b \x03(\x0b\x32:.jackal.protobuf.LoadClientSettingsResponseData.InfoWindow\x12\x18\n\x10lucky_bonus_rate\x18\x0c \x02(\r\x12\x18\n\x10\x66\x61il_support_num\x18\r \x02(\r\x12O\n\x0blucky_bonus\x18\x0e \x03(\x0b\x32:.jackal.protobuf.LoadClientSettingsResponseData.LuckyBonus\x12S\n\rspecial_bonus\x18\x0f \x03(\x0b\x32<.jackal.protobuf.LoadClientSettingsResponseData.SpecialBonus\x12\x17\n\x0f\x63hara_open_flag\x18\x10 \x02(\r\x12\x17\n\x0f\x63hara_open_date\x18\x11 \x02(\r\x12\x1b\n\x13\x63hara_pre_open_date\x18\x12 \x02(\r\x12\x11\n\tsearch_id\x18\x13 \x02(\r\x12\x16\n\x0e\x63lient_version\x18\x14 \x01(\t\x12!\n\x19\x63lient_version_start_date\x18\x15 \x01(\r\x1a\xe0\x01\n\tEventInfo\x12\x13\n\x0b\x65vent_state\x18\x65 \x02(\r\x12\x10\n\x08\x65vent_id\x18\x66 \x02(\r\x12\x1e\n\x16sp_bonus_category_id_1\x18g \x02(\r\x12\x1c\n\x14sp_bonus_key_value_1\x18h \x02(\r\x12\x1e\n\x16sp_bonus_category_id_2\x18i \x02(\r\x12\x1c\n\x14sp_bonus_key_value_2\x18j \x02(\r\x12\x18\n\x10\x65vent_start_date\x18k \x02(\r\x12\x16\n\x0e\x65vent_end_date\x18l \x02(\r\x1a\xa2\x01\n\nBannerInfo\x12\x1a\n\x11\x62\x61nner_start_date\x18\xc9\x01 \x02(\r\x12\x18\n\x0f\x62\x61nner_end_date\x18\xca\x01 \x02(\r\x12\x12\n\tbanner_id\x18\xcb\x01 \x02(\r\x12\x15\n\x0c\x62\x61nner_title\x18\xcc\x01 \x02(\t\x12\x18\n\x0f\x62\x61nner_sub_info\x18\xcd\x01 \x02(\t\x12\x19\n\x10\x62\x61nner_term_info\x18\xce\x01 \x02(\t\x1a\x84\x02\n\x0b\x41ttractInfo\x12 \n\x17\x61ttract_info_start_date\x18\xad\x02 \x02(\r\x12\x1e\n\x15\x61ttract_info_end_date\x18\xae\x02 \x02(\r\x12\x18\n\x0f\x61ttract_info_id\x18\xaf\x02 \x02(\r\x12\x1b\n\x12\x61ttract_info_title\x18\xb0\x02 \x02(\t\x12\x1e\n\x15\x61ttract_info_sub_info\x18\xb1\x02 \x02(\t\x12 \n\x17\x61ttract_info_start_info\x18\xb2\x02 \x02(\t\x12\x1e\n\x15\x61ttract_info_end_info\x18\xb3\x02 \x02(\t\x12\x1a\n\x11\x61ttract_info_text\x18\xb4\x02 \x02(\t\x1a\xfb\x01\n\nInfoWindow\x12\x1f\n\x16info_window_start_date\x18\x91\x03 \x02(\r\x12\x1d\n\x14info_window_end_date\x18\x92\x03 \x02(\r\x12\x17\n\x0einfo_window_id\x18\x93\x03 \x02(\r\x12\x1a\n\x11info_window_title\x18\x94\x03 \x02(\t\x12\x1d\n\x14info_window_sub_info\x18\x95\x03 \x02(\t\x12\x1f\n\x16info_window_start_info\x18\x96\x03 \x02(\t\x12\x1d\n\x14info_window_end_info\x18\x97\x03 \x02(\t\x12\x19\n\x10info_window_text\x18\x98\x03 \x02(\t\x1an\n\nLuckyBonus\x12 \n\x17lucky_bonus_category_id\x18\xf5\x03 \x02(\r\x12\x1c\n\x13lucky_bonus_data_id\x18\xf6\x03 \x02(\r\x12 \n\x17lucky_bonus_probability\x18\xf7\x03 \x02(\r\x1av\n\x0cSpecialBonus\x12\"\n\x19special_bonus_category_id\x18\xd9\x04 \x02(\r\x12\x1e\n\x15special_bonus_data_id\x18\xda\x04 \x02(\r\x12\"\n\x19special_bonus_probability\x18\xdb\x04 \x02(\r*\xb2\x03\n\x0bMessageType\x12\x08\n\x04NOOP\x10\x00\x12\x08\n\x04PING\x10\x01\x12\x10\n\x0cREGISTER_PCB\x10\x02\x12\x0c\n\x08SAVE_ADS\x10\x03\x12\x15\n\x11\x43HECK_ACCESS_CODE\x10\x04\x12\x15\n\x11SET_BNPASSID_LOCK\x10\x05\x12\r\n\tLOAD_USER\x10\x06\x12\r\n\tSAVE_USER\x10\t\x12\x13\n\x0f\x43HECK_DIAGNOSIS\x10\n\x12\x13\n\x0fSAVE_CLIENT_LOG\x10\x0b\x12\x18\n\x14PRE_LOAD_INFORMATION\x10\x0c\x12\x14\n\x10LOAD_INFORMATION\x10\r\x12\x13\n\x0fPRE_SAVE_REPLAY\x10\x0e\x12\x0f\n\x0bSAVE_REPLAY\x10\x0f\x12\x0f\n\x0bSAVE_CHARGE\x10\x10\x12\x11\n\rCHECK_RANKING\x10\x11\x12\x10\n\x0cLOAD_RANKING\x10\x12\x12\x13\n\x0fSAVE_INGAME_LOG\x10\x13\x12 \n\x1cPRE_LOAD_INFORMATION_ATTRACT\x10\x14\x12\x1c\n\x18LOAD_INFORMATION_ATTRACT\x10\x15\x12\x18\n\x14LOAD_CLIENT_SETTINGS\x10\x16') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x0cjackal.proto\x12\x0fjackal.protobuf"\xae\x0b\n\x07Request\x12*\n\x04type\x18\x01 \x02(\x0e\x32\x1c.jackal.protobuf.MessageType\x12.\n\x04noop\x18\x02 \x01(\x0b\x32 .jackal.protobuf.NoopRequestData\x12.\n\x04ping\x18\x03 \x01(\x0b\x32 .jackal.protobuf.PingRequestData\x12=\n\x0cregister_pcb\x18\x04 \x01(\x0b\x32\'.jackal.protobuf.RegisterPcbRequestData\x12\x35\n\x08save_ads\x18\x05 \x01(\x0b\x32#.jackal.protobuf.SaveAdsRequestData\x12\x46\n\x11\x63heck_access_code\x18\x06 \x01(\x0b\x32+.jackal.protobuf.CheckAccessCodeRequestData\x12\x46\n\x11set_bnpassid_lock\x18\x07 \x01(\x0b\x32+.jackal.protobuf.SetBnpassIdLockRequestData\x12\x37\n\tload_user\x18\x08 \x01(\x0b\x32$.jackal.protobuf.LoadUserRequestData\x12\x37\n\tsave_user\x18\n \x01(\x0b\x32$.jackal.protobuf.SaveUserRequestData\x12\x43\n\x0f\x63heck_diagnosis\x18\x0b \x01(\x0b\x32*.jackal.protobuf.CheckDiagnosisRequestData\x12\x42\n\x0fsave_client_log\x18\x0c \x01(\x0b\x32).jackal.protobuf.SaveClientLogRequestData\x12L\n\x14pre_load_information\x18\r \x01(\x0b\x32..jackal.protobuf.PreLoadInformationRequestData\x12\x45\n\x10load_information\x18\x0e \x01(\x0b\x32+.jackal.protobuf.LoadInformationRequestData\x12\x42\n\x0fpre_save_replay\x18\x0f \x01(\x0b\x32).jackal.protobuf.PreSaveReplayRequestData\x12;\n\x0bsave_replay\x18\x10 \x01(\x0b\x32&.jackal.protobuf.SaveReplayRequestData\x12;\n\x0bsave_charge\x18\x11 \x01(\x0b\x32&.jackal.protobuf.SaveChargeRequestData\x12?\n\rcheck_ranking\x18\x12 \x01(\x0b\x32(.jackal.protobuf.CheckRankingRequestData\x12=\n\x0cload_ranking\x18\x13 \x01(\x0b\x32\'.jackal.protobuf.LoadRankingRequestData\x12\x42\n\x0fsave_ingame_log\x18\x14 \x01(\x0b\x32).jackal.protobuf.SaveInGameLogRequestData\x12[\n\x1cpre_load_information_attract\x18\x15 \x01(\x0b\x32\x35.jackal.protobuf.PreLoadInformationAttractRequestData\x12T\n\x18load_information_attract\x18\x16 \x01(\x0b\x32\x32.jackal.protobuf.LoadInformationAttractRequestData\x12L\n\x14load_client_settings\x18\x17 \x01(\x0b\x32..jackal.protobuf.LoadClientSettingsRequestData"\xd4\x0b\n\x08Response\x12*\n\x04type\x18\x01 \x02(\x0e\x32\x1c.jackal.protobuf.MessageType\x12\x0e\n\x06result\x18\x02 \x02(\r\x12/\n\x04noop\x18\x03 \x01(\x0b\x32!.jackal.protobuf.NoopResponseData\x12/\n\x04ping\x18\x04 \x01(\x0b\x32!.jackal.protobuf.PingResponseData\x12>\n\x0cregister_pcb\x18\x05 \x01(\x0b\x32(.jackal.protobuf.RegisterPcbResponseData\x12\x36\n\x08save_ads\x18\x06 \x01(\x0b\x32$.jackal.protobuf.SaveAdsResponseData\x12G\n\x11\x63heck_access_code\x18\x07 \x01(\x0b\x32,.jackal.protobuf.CheckAccessCodeResponseData\x12G\n\x11set_bnpassid_lock\x18\x08 \x01(\x0b\x32,.jackal.protobuf.SetBnpassIdLockResponseData\x12\x38\n\tload_user\x18\t \x01(\x0b\x32%.jackal.protobuf.LoadUserResponseData\x12\x38\n\tsave_user\x18\x0b \x01(\x0b\x32%.jackal.protobuf.SaveUserResponseData\x12\x44\n\x0f\x63heck_diagnosis\x18\x0c \x01(\x0b\x32+.jackal.protobuf.CheckDiagnosisResponseData\x12\x43\n\x0fsave_client_log\x18\r \x01(\x0b\x32*.jackal.protobuf.SaveClientLogResponseData\x12M\n\x14pre_load_information\x18\x0e \x01(\x0b\x32/.jackal.protobuf.PreLoadInformationResponseData\x12\x46\n\x10load_information\x18\x0f \x01(\x0b\x32,.jackal.protobuf.LoadInformationResponseData\x12\x43\n\x0fpre_save_replay\x18\x10 \x01(\x0b\x32*.jackal.protobuf.PreSaveReplayResponseData\x12<\n\x0bsave_replay\x18\x11 \x01(\x0b\x32\'.jackal.protobuf.SaveReplayResponseData\x12<\n\x0bsave_charge\x18\x12 \x01(\x0b\x32\'.jackal.protobuf.SaveChargeResponseData\x12@\n\rcheck_ranking\x18\x13 \x01(\x0b\x32).jackal.protobuf.CheckRankingResponseData\x12>\n\x0cload_ranking\x18\x14 \x01(\x0b\x32(.jackal.protobuf.LoadRankingResponseData\x12\x43\n\x0fsave_ingame_log\x18\x15 \x01(\x0b\x32*.jackal.protobuf.SaveInGameLogResponseData\x12\\\n\x1cpre_load_information_attract\x18\x16 \x01(\x0b\x32\x36.jackal.protobuf.PreLoadInformationAttractResponseData\x12U\n\x18load_information_attract\x18\x17 \x01(\x0b\x32\x33.jackal.protobuf.LoadInformationAttractResponseData\x12M\n\x14load_client_settings\x18\x18 \x01(\x0b\x32/.jackal.protobuf.LoadClientSettingsResponseData"1\n\x0fNoopRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t"\x12\n\x10NoopResponseData"1\n\x0fPingRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t"\x12\n\x10PingResponseData"\xce\x02\n\x16RegisterPcbRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x10\n\x08pcb_type\x18\x02 \x02(\r\x12\x15\n\rlocation_name\x18\x03 \x02(\t\x12\x19\n\x11location_nickname\x18\x04 \x02(\t\x12\x11\n\tpref_code\x18\x05 \x01(\r\x12\r\n\x05\x61\x64\x64r0\x18\x06 \x01(\t\x12\r\n\x05\x61\x64\x64r1\x18\x07 \x01(\t\x12\r\n\x05\x61\x64\x64r2\x18\x08 \x01(\t\x12\r\n\x05\x61\x64\x64r3\x18\t \x01(\t\x12\x0e\n\x06loc_id\x18\n \x02(\t\x12\x14\n\x0c\x63ountry_code\x18\x0b \x02(\t\x12\x13\n\x0bregion_code\x18\x0c \x02(\r\x12\r\n\x05karma\x18\r \x02(\x05\x12\x0f\n\x07game_id\x18\x0e \x02(\t\x12\x10\n\x08game_ver\x18\x0f \x02(\t\x12\x10\n\x08\x64isk_ver\x18\x10 \x02(\t\x12\x12\n\nutc_offset\x18\x11 \x02(\x05"\x85\x01\n\x17RegisterPcbResponseData\x12\x13\n\x0bserver_time\x18\x01 \x02(\r\x12\x15\n\roperate_start\x18\x02 \x01(\t\x12\x13\n\x0boperate_end\x18\x03 \x01(\t\x12\x13\n\x0b\x62np_baseuri\x18\x04 \x01(\t\x12\x14\n\x0c\x62iwa_setting\x18\x05 \x02(\t"\xd7\x02\n\x12SaveAdsRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x17\n\x0f\x61\x64s_start_count\x18\x03 \x02(\r\x12\x16\n\x0e\x61\x64s_coin_count\x18\x04 \x02(\r\x12\x19\n\x11\x61\x64s_service_count\x18\x05 \x02(\r\x12\x1a\n\x12\x61\x64s_freeplay_count\x18\x06 \x02(\r\x12\x1a\n\x12\x61\x64s_operation_days\x18\x07 \x02(\r\x12\x1a\n\x12\x61\x64s_power_on_count\x18\x08 \x02(\r\x12\x46\n\rads_play_time\x18\t \x03(\x0b\x32/.jackal.protobuf.SaveAdsRequestData.AdsPlayTime\x1a\x39\n\x0b\x41\x64sPlayTime\x12\x12\n\npokemon_id\x18\x65 \x02(\r\x12\x16\n\x0e\x63harplay_count\x18\x66 \x02(\r"\x15\n\x13SaveAdsResponseData"M\n\x1a\x43heckAccessCodeRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x0f\n\x07\x63hip_id\x18\x03 \x02(\t"M\n\x1b\x43heckAccessCodeResponseData\x12\x19\n\x11\x63ommidserv_result\x18\x01 \x02(\r\x12\x13\n\x0b\x61\x63\x63\x65ss_code\x18\x02 \x02(\t"\xa4\x01\n\x1aSetBnpassIdLockRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x13\n\x0b\x62\x61napass_id\x18\x03 \x02(\r\x12\x13\n\x0b\x64\x65vice_type\x18\x04 \x02(\r\x12\x0f\n\x07\x63hip_id\x18\x05 \x02(\t\x12\x13\n\x0b\x61\x63\x63\x65ss_code\x18\x06 \x01(\t\x12\x16\n\x0e\x63\x61rd_lock_time\x18\x07 \x02(\r"\x1d\n\x1bSetBnpassIdLockResponseData"\x85\x01\n\x13LoadUserRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x13\n\x0b\x64\x65vice_type\x18\x03 \x02(\r\x12\x0f\n\x07\x63hip_id\x18\x04 \x02(\t\x12\x13\n\x0b\x61\x63\x63\x65ss_code\x18\x05 \x01(\t\x12\x13\n\x0b\x63\x61rd_status\x18\x06 \x02(\x08"\xfc\x12\n\x14LoadUserResponseData\x12\x19\n\x11\x63ommidserv_result\x18\x01 \x02(\r\x12\x11\n\tload_hash\x18\x02 \x02(\r\x12\x17\n\x0f\x63\x61rdlock_status\x18\x03 \x02(\x08\x12\x13\n\x0b\x62\x61napass_id\x18\x04 \x02(\r\x12\x13\n\x0b\x61\x63\x63\x65ss_code\x18\x05 \x01(\t\x12\x15\n\rnew_card_flag\x18\x06 \x02(\x08\x12\x1e\n\x16precedent_release_flag\x18\x07 \x02(\r\x12\x18\n\x10navi_newbie_flag\x18\x08 \x02(\x08\x12\x18\n\x10navi_enable_flag\x18\t \x02(\x08\x12\x18\n\x10pad_vibrate_flag\x18\n \x02(\x08\x12\x18\n\x10home_region_code\x18\x0b \x02(\r\x12\x15\n\rhome_loc_name\x18\x0c \x02(\t\x12\x11\n\tpref_code\x18\r \x02(\r\x12\x14\n\x0ctrainer_name\x18\x0e \x01(\t\x12\x1a\n\x12trainer_rank_point\x18\x0f \x02(\r\x12\x0e\n\x06wallet\x18\x10 \x02(\r\x12\x13\n\x0b\x66ight_money\x18\x11 \x02(\r\x12\x13\n\x0bscore_point\x18\x12 \x02(\r\x12\x15\n\rgrade_max_num\x18\x13 \x02(\r\x12\x15\n\rextra_counter\x18\x14 \x01(\r\x12\x1e\n\x16tutorial_progress_flag\x18\x15 \x03(\r\x12\x17\n\x0ftotal_play_days\x18\x16 \x02(\r\x12\x16\n\x0eplay_date_time\x18\x17 \x02(\r\x12\x1a\n\x12lucky_box_fail_num\x18\x18 \x02(\r\x12\x1d\n\x15\x65vent_reward_get_flag\x18\x19 \x02(\r\x12\x14\n\x0crank_pvp_all\x18\x1a \x02(\r\x12\x14\n\x0crank_pvp_loc\x18\x1b \x02(\r\x12\x14\n\x0crank_cpu_all\x18\x1c \x02(\r\x12\x14\n\x0crank_cpu_loc\x18\x1d \x02(\r\x12\x12\n\nrank_event\x18\x1e \x02(\r\x12\x11\n\tawake_num\x18\x1f \x02(\r\x12\x17\n\x0fuse_support_num\x18 \x02(\r\x12\x16\n\x0erankmatch_flag\x18! \x02(\r\x12\x1a\n\x12rankmatch_progress\x18" \x03(\r\x12\x15\n\rrankmatch_max\x18# \x01(\r\x12\x19\n\x11rankmatch_success\x18$ \x01(\r\x12\x10\n\x08\x62\x65\x61t_num\x18% \x01(\x05\x12\x15\n\rtitle_text_id\x18& \x02(\r\x12\x16\n\x0etitle_plate_id\x18\' \x02(\r\x12\x1b\n\x13title_decoration_id\x18( \x02(\r\x12\x1c\n\x14support_pokemon_list\x18) \x03(\r\x12\x15\n\rsupport_set_1\x18* \x03(\r\x12\x15\n\rsupport_set_2\x18+ \x03(\r\x12\x15\n\rsupport_set_3\x18, \x03(\r\x12\x14\n\x0cnavi_trainer\x18- \x02(\r\x12\x17\n\x0fnavi_version_id\x18. \x02(\r\x12\x16\n\x0e\x61id_skill_list\x18/ \x03(\r\x12\x11\n\taid_skill\x18\x30 \x02(\r\x12\x17\n\x0f\x63omment_text_id\x18\x31 \x02(\r\x12\x17\n\x0f\x63omment_word_id\x18\x32 \x02(\r\x12\x1a\n\x12latest_use_pokemon\x18\x33 \x02(\r\x12\x11\n\tex_ko_num\x18\x34 \x02(\r\x12\x0f\n\x07wko_num\x18\x35 \x02(\r\x12\x16\n\x0etimeup_win_num\x18\x36 \x02(\r\x12\x13\n\x0b\x63ool_ko_num\x18\x37 \x02(\r\x12\x16\n\x0eperfect_ko_num\x18\x38 \x02(\r\x12\x13\n\x0brecord_flag\x18\x39 \x02(\r\x12\x1c\n\x14site_register_status\x18: \x02(\r\x12\x14\n\x0c\x63ontinue_num\x18; \x02(\r\x12\x18\n\x10\x61\x63hievement_flag\x18< \x03(\r\x12\x13\n\x0b\x61vatar_body\x18= \x01(\r\x12\x15\n\ravatar_gender\x18> \x01(\r\x12\x19\n\x11\x61vatar_background\x18? \x01(\r\x12\x13\n\x0b\x61vatar_head\x18@ \x01(\r\x12\x1a\n\x12\x61vatar_battleglass\x18\x41 \x01(\r\x12\x14\n\x0c\x61vatar_face0\x18\x42 \x01(\r\x12\x14\n\x0c\x61vatar_face1\x18\x43 \x01(\r\x12\x14\n\x0c\x61vatar_face2\x18\x44 \x01(\r\x12\x16\n\x0e\x61vatar_bodyall\x18\x45 \x01(\r\x12\x13\n\x0b\x61vatar_wear\x18\x46 \x01(\r\x12\x18\n\x10\x61vatar_accessory\x18G \x01(\r\x12\x14\n\x0c\x61vatar_stamp\x18H \x01(\r\x12G\n\x0cpokemon_data\x18I \x03(\x0b\x32\x31.jackal.protobuf.LoadUserResponseData.PokemonData\x12\x13\n\x0b\x65vent_state\x18J \x02(\r\x12\x10\n\x08\x65vent_id\x18K \x02(\r\x12\x1e\n\x16sp_bonus_category_id_1\x18L \x02(\r\x12\x1c\n\x14sp_bonus_key_value_1\x18M \x02(\r\x12\x1e\n\x16sp_bonus_category_id_2\x18N \x02(\r\x12\x1c\n\x14sp_bonus_key_value_2\x18O \x02(\r\x12\x1a\n\x12last_play_event_id\x18P \x01(\r\x12\x1e\n\x16\x65vent_achievement_flag\x18Q \x03(\r\x12\x1f\n\x17\x65vent_achievement_param\x18R \x03(\r\x1a\xf0\x02\n\x0bPokemonData\x12\x0f\n\x07\x63har_id\x18\x65 \x02(\r\x12\x1c\n\x14illustration_book_no\x18\x66 \x02(\r\x12\x13\n\x0bpokemon_exp\x18g \x02(\r\x12\x19\n\x11\x62\x61ttle_num_vs_wan\x18h \x02(\r\x12\x12\n\nwin_vs_wan\x18i \x02(\r\x12\x19\n\x11\x62\x61ttle_num_vs_lan\x18j \x02(\r\x12\x12\n\nwin_vs_lan\x18k \x02(\r\x12\x19\n\x11\x62\x61ttle_num_vs_cpu\x18l \x02(\r\x12\x0f\n\x07win_cpu\x18m \x02(\r\x12\x1f\n\x17\x62\x61ttle_all_num_tutorial\x18n \x02(\r\x12\x1b\n\x13\x62\x61ttle_num_tutorial\x18o \x02(\r\x12\x14\n\x0c\x62p_point_atk\x18p \x02(\r\x12\x14\n\x0c\x62p_point_res\x18q \x02(\r\x12\x14\n\x0c\x62p_point_def\x18r \x02(\r\x12\x13\n\x0b\x62p_point_sp\x18s \x02(\r"\x98\x0c\n\x13SaveUserRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x13\n\x0b\x62\x61napass_id\x18\x03 \x02(\r\x12\x1e\n\x16get_trainer_rank_point\x18\x04 \x01(\x05\x12\x11\n\tget_money\x18\x05 \x02(\r\x12\x17\n\x0fget_score_point\x18\x06 \x01(\r\x12\x15\n\rgrade_max_num\x18\x07 \x01(\r\x12\x15\n\rextra_counter\x18\x08 \x01(\r\x12\x1e\n\x16tutorial_progress_flag\x18\t \x03(\r\x12\x1d\n\x15\x65vent_reward_get_flag\x18\n \x01(\r\x12\x14\n\x0c\x63ontinue_num\x18\x0b \x02(\r\x12\x17\n\x0ftotal_play_days\x18\x0c \x02(\r\x12\x18\n\x10\x61\x63hievement_flag\x18\r \x03(\r\x12\x11\n\tawake_num\x18\x0e \x02(\r\x12\x17\n\x0fuse_support_num\x18\x0f \x02(\r\x12\x16\n\x0erankmatch_flag\x18\x10 \x02(\r\x12\x1a\n\x12rank_match_process\x18\x11 \x03(\r\x12\x16\n\x0erank_match_max\x18\x12 \x01(\r\x12\x1a\n\x12rank_match_success\x18\x13 \x01(\r\x12\x10\n\x08\x62\x65\x61t_num\x18\x14 \x01(\x05\x12\x15\n\rsupport_set_1\x18\x15 \x03(\r\x12\x15\n\rsupport_set_2\x18\x16 \x03(\r\x12\x15\n\rsupport_set_3\x18\x17 \x03(\r\x12\x44\n\x0b\x62\x61ttle_data\x18\x18 \x02(\x0b\x32/.jackal.protobuf.SaveUserRequestData.BattleData\x12\x46\n\x0cpokemon_data\x18\x19 \x02(\x0b\x32\x30.jackal.protobuf.SaveUserRequestData.PokemonData\x12\x1c\n\x14trainer_name_pending\x18\x1a \x01(\t\x12\x15\n\ravatar_gender\x18\x1b \x01(\r\x12\x15\n\rcontinue_flag\x18\x1c \x02(\x08\x12\x14\n\x0creq_sendtime\x18\x1d \x02(\r\x12\x15\n\rplay_all_time\x18\x1e \x02(\r\x12\x11\n\tload_hash\x18\x1f \x02(\r\x12\x44\n\x0breward_data\x18 \x03(\x0b\x32/.jackal.protobuf.SaveUserRequestData.RewardData\x12\x13\n\x0b\x65vent_state\x18! \x01(\r\x12\x11\n\taid_skill\x18" \x01(\r\x12\x1a\n\x12last_play_event_id\x18# \x01(\r\x12\x1e\n\x16\x65vent_achievement_flag\x18$ \x03(\r\x12\x1f\n\x17\x65vent_achievement_param\x18% \x03(\r\x1a\xec\x01\n\nBattleData\x12\x11\n\tplay_mode\x18\x65 \x03(\r\x12\x0e\n\x06result\x18\x66 \x03(\r\x12\x11\n\tex_ko_num\x18g \x02(\r\x12\x0f\n\x07wko_num\x18h \x02(\r\x12\x16\n\x0etimeup_win_num\x18i \x02(\r\x12\x13\n\x0b\x63ool_ko_num\x18j \x02(\r\x12\x16\n\x0eperfect_ko_num\x18k \x02(\r\x12\x10\n\x08use_navi\x18l \x02(\r\x12\x16\n\x0euse_navi_cloth\x18m \x02(\r\x12\x15\n\ruse_aid_skill\x18n \x02(\r\x12\x11\n\tplay_date\x18o \x02(\r\x1a\xb3\x01\n\x0bPokemonData\x12\x10\n\x07\x63har_id\x18\xc9\x01 \x02(\r\x12\x1d\n\x14illustration_book_no\x18\xca\x01 \x02(\r\x12\x18\n\x0fget_pokemon_exp\x18\xcb\x01 \x02(\r\x12\x15\n\x0c\x62p_point_atk\x18\xcc\x01 \x02(\r\x12\x15\n\x0c\x62p_point_res\x18\xcd\x01 \x02(\r\x12\x15\n\x0c\x62p_point_def\x18\xce\x01 \x02(\r\x12\x14\n\x0b\x62p_point_sp\x18\xcf\x01 \x02(\r\x1aU\n\nRewardData\x12\x18\n\x0fget_category_id\x18\xad\x02 \x02(\r\x12\x17\n\x0eget_content_id\x18\xae\x02 \x02(\r\x12\x14\n\x0bget_type_id\x18\xaf\x02 \x02(\r"\x16\n\x14SaveUserResponseData";\n\x19\x43heckDiagnosisRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t"\x95\x02\n\x1a\x43heckDiagnosisResponseData\x12Q\n\x0e\x64iagnosis_data\x18\x01 \x03(\x0b\x32\x39.jackal.protobuf.CheckDiagnosisResponseData.DiagnosisData\x1a\xa3\x01\n\rDiagnosisData\x12\x14\n\x0crequest_type\x18\x65 \x02(\r\x12\x17\n\x0f\x63onnect_timeout\x18\x66 \x02(\r\x12\x14\n\x0csend_timeout\x18g \x02(\r\x12\x17\n\x0freceive_timeout\x18h \x02(\r\x12\x1c\n\x14retry_time_of_number\x18i \x02(\r\x12\x16\n\x0eretry_interval\x18j \x02(\r"\xf4\x01\n\x18SaveClientLogRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x11\n\tserial_id\x18\x03 \x02(\r\x12\x14\n\x0creq_sendtime\x18\x04 \x02(\r\x12\r\n\x05karma\x18\x05 \x02(\x05\x12\x14\n\x0crequest_type\x18\x06 \x02(\r\x12\x1f\n\x17request_number_of_times\x18\x07 \x02(\r\x12\x1f\n\x17timeout_number_of_times\x18\x08 \x02(\r\x12\x11\n\tretry_max\x18\t \x02(\r\x12\x15\n\rresponse_time\x18\n \x02(\r"\x1b\n\x19SaveClientLogResponseData"V\n\x1dPreLoadInformationRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x15\n\rinfo_small_id\x18\x03 \x02(\r"\xc9\x01\n\x1ePreLoadInformationResponseData\x12\x15\n\rinfo_small_id\x18\x01 \x02(\r\x12\x13\n\x0bregion_code\x18\x02 \x02(\r\x12\x12\n\nsession_id\x18\x03 \x02(\r\x12\x13\n\x0b\x62lock_total\x18\x04 \x02(\r\x12\x12\n\nblock_size\x18\x05 \x02(\r\x12\x10\n\x08interval\x18\x06 \x02(\r\x12\x16\n\x0einfo_data_size\x18\x07 \x02(\r\x12\x14\n\x0cinfo_data_id\x18\x08 \x02(\r"v\n\x1aLoadInformationRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x15\n\rinfo_small_id\x18\x03 \x02(\r\x12\x12\n\nsession_id\x18\x04 \x02(\r\x12\r\n\x05\x62lock\x18\x05 \x02(\r"\x96\x01\n\x1bLoadInformationResponseData\x12\x15\n\rinfo_small_id\x18\x01 \x02(\r\x12\x12\n\nstart_date\x18\x02 \x02(\r\x12\x10\n\x08\x65nd_date\x18\x03 \x02(\r\x12\r\n\x05\x62lock\x18\x04 \x02(\r\x12\x13\n\x0b\x62lock_total\x18\x05 \x02(\r\x12\x16\n\x0einfo_data_body\x18\x06 \x02(\x0c"\x80\x01\n\x18PreSaveReplayRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x13\n\x0bregion_code\x18\x03 \x02(\r\x12\x15\n\rcategory_code\x18\x04 \x02(\r\x12\x18\n\x10replay_data_size\x18\x05 \x02(\r"j\n\x19PreSaveReplayResponseData\x12\x12\n\nsession_id\x18\x01 \x02(\r\x12\x13\n\x0b\x62lock_total\x18\x02 \x02(\r\x12\x12\n\nblock_size\x18\x03 \x02(\r\x12\x10\n\x08interval\x18\x04 \x02(\r"\x82\x02\n\x15SaveReplayRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x13\n\x0b\x62\x61napass_id\x18\x03 \x02(\r\x12\x12\n\npokemon_id\x18\x04 \x02(\r\x12\x17\n\x0ftrainer_rank_id\x18\x05 \x02(\r\x12\x13\n\x0bregion_code\x18\x06 \x02(\r\x12\x12\n\nsession_id\x18\x07 \x02(\r\x12\r\n\x05\x62lock\x18\x08 \x02(\r\x12\x1b\n\x13transfer_completion\x18\t \x02(\r\x12\x18\n\x10replay_data_size\x18\n \x02(\r\x12\x18\n\x10replay_data_body\x18\x0b \x02(\x0c"\x18\n\x16SaveReplayResponseData"\x8d\x01\n\x15SaveChargeRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x0f\n\x07game_id\x18\x03 \x02(\t\x12\x19\n\x11\x63harge_data_index\x18\x04 \x02(\t\x12\x13\n\x0b\x63harge_type\x18\x05 \x02(\r\x12\x13\n\x0b\x63harge_time\x18\x06 \x02(\r"N\n\x16SaveChargeResponseData\x12\x19\n\x11\x63harge_error_code\x18\x01 \x02(\r\x12\x19\n\x11\x63harge_data_index\x18\x02 \x02(\t"u\n\x17\x43heckRankingRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x13\n\x0bregion_code\x18\x03 \x02(\r\x12\x12\n\nranking_id\x18\x04 \x02(\r\x12\x11\n\ttimestamp\x18\x05 \x02(\r".\n\x18\x43heckRankingResponseData\x12\x12\n\nranking_id\x18\x01 \x02(\r"a\n\x16LoadRankingRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x13\n\x0bregion_code\x18\x03 \x02(\r\x12\x12\n\nranking_id\x18\x04 \x02(\r"\xbd\x08\n\x17LoadRankingResponseData\x12\x12\n\nranking_id\x18\x01 \x02(\r\x12\x15\n\rranking_start\x18\x02 \x02(\r\x12\x13\n\x0branking_end\x18\x03 \x02(\r\x12\x11\n\tevent_end\x18\x04 \x02(\x08\x12J\n\x0ctrainer_data\x18\x05 \x03(\x0b\x32\x34.jackal.protobuf.LoadRankingResponseData.TrainerData\x12\x13\n\x0bmodify_date\x18\x06 \x02(\r\x12\x13\n\x0b\x65vent_state\x18\x07 \x01(\r\x1a\xd8\x06\n\x0bTrainerData\x12\x14\n\x0ctrainer_name\x18\x65 \x02(\t\x12\x1a\n\x12trainer_rank_point\x18\x66 \x02(\r\x12\r\n\x05point\x18g \x02(\r\x12\x13\n\x0brecord_flag\x18h \x02(\r\x12\x18\n\x10\x66\x61vorite_pokemon\x18i \x02(\r\x12\x12\n\nwin_vs_wan\x18j \x02(\r\x12\x19\n\x11\x62\x61ttle_num_vs_wan\x18k \x02(\r\x12\x12\n\nwin_vs_cpu\x18l \x02(\r\x12\x19\n\x11\x62\x61ttle_num_vs_cpu\x18m \x02(\r\x12\x15\n\rtitle_text_id\x18n \x02(\r\x12\x16\n\x0etitle_plate_id\x18o \x02(\r\x12\x1b\n\x13title_decoration_id\x18p \x02(\r\x12\x17\n\x0f\x63omment_text_id\x18q \x02(\r\x12\x17\n\x0f\x63omment_word_id\x18r \x02(\r\x12\x10\n\x08loc_name\x18s \x02(\t\x12\x11\n\tpref_code\x18t \x02(\r\x12\x10\n\x08rank_num\x18u \x02(\r\x12\x15\n\rlast_rank_num\x18v \x02(\r\x12\x0e\n\x06updown\x18w \x02(\r\x12\x12\n\npokemon_id\x18x \x02(\r\x12\x13\n\x0bpokemon_exp\x18y \x02(\r\x12\x14\n\x0c\x62p_point_atk\x18z \x02(\r\x12\x14\n\x0c\x62p_point_res\x18{ \x02(\r\x12\x14\n\x0c\x62p_point_def\x18| \x02(\r\x12\x13\n\x0b\x62p_point_sp\x18} \x02(\r\x12\x13\n\x0b\x61vatar_body\x18~ \x02(\r\x12\x15\n\ravatar_gender\x18\x7f \x02(\r\x12\x1a\n\x11\x61vatar_background\x18\x80\x01 \x02(\r\x12\x14\n\x0b\x61vatar_head\x18\x81\x01 \x02(\r\x12\x1b\n\x12\x61vatar_battleglass\x18\x82\x01 \x02(\r\x12\x15\n\x0c\x61vatar_face0\x18\x83\x01 \x02(\r\x12\x15\n\x0c\x61vatar_face1\x18\x84\x01 \x02(\r\x12\x15\n\x0c\x61vatar_face2\x18\x85\x01 \x02(\r\x12\x17\n\x0e\x61vatar_bodyall\x18\x86\x01 \x02(\r\x12\x14\n\x0b\x61vatar_wear\x18\x87\x01 \x02(\r\x12\x19\n\x10\x61vatar_accessory\x18\x88\x01 \x02(\r\x12\x15\n\x0c\x61vatar_stamp\x18\x89\x01 \x02(\r"h\n\x18SaveInGameLogRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x13\n\x0bin_game_log\x18\x03 \x02(\x0c\x12\x17\n\x0flog_change_time\x18\x04 \x02(\r"\x1b\n\x19SaveInGameLogResponseData"]\n$PreLoadInformationAttractRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x15\n\rinfo_large_id\x18\x03 \x02(\r"\xd0\x01\n%PreLoadInformationAttractResponseData\x12\x15\n\rinfo_large_id\x18\x01 \x02(\r\x12\x13\n\x0bregion_code\x18\x02 \x02(\r\x12\x12\n\nsession_id\x18\x03 \x02(\r\x12\x13\n\x0b\x62lock_total\x18\x04 \x02(\r\x12\x12\n\nblock_size\x18\x05 \x02(\r\x12\x10\n\x08interval\x18\x06 \x02(\r\x12\x16\n\x0einfo_data_size\x18\x07 \x02(\r\x12\x14\n\x0cinfo_data_id\x18\x08 \x02(\r"}\n!LoadInformationAttractRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t\x12\x15\n\rinfo_large_id\x18\x03 \x02(\r\x12\x12\n\nsession_id\x18\x04 \x02(\r\x12\r\n\x05\x62lock\x18\x05 \x02(\r"\x9d\x01\n"LoadInformationAttractResponseData\x12\x15\n\rinfo_large_id\x18\x01 \x02(\r\x12\x12\n\nstart_date\x18\x02 \x02(\r\x12\x10\n\x08\x65nd_date\x18\x03 \x02(\r\x12\r\n\x05\x62lock\x18\x04 \x02(\r\x12\x13\n\x0b\x62lock_total\x18\x05 \x02(\r\x12\x16\n\x0einfo_data_body\x18\x06 \x02(\x0c"?\n\x1dLoadClientSettingsRequestData\x12\x0e\n\x06pcb_id\x18\x01 \x02(\t\x12\x0e\n\x06loc_id\x18\x02 \x02(\t"\x8d\x10\n\x1eLoadClientSettingsResponseData\x12\x1b\n\x13money_magnification\x18\x01 \x02(\r\x12!\n\x19\x64m2_probability_single100\x18\x02 \x03(\r\x12\x1a\n\x12\x63ontinue_bonus_exp\x18\x03 \x02(\r\x12\x1c\n\x14\x63ontinue_fight_money\x18\x04 \x02(\r\x12\x17\n\x0f\x65vent_bonus_exp\x18\x05 \x02(\r\x12\x11\n\tlevel_cap\x18\x06 \x02(\r\x12\x15\n\rop_movie_flag\x18\x07 \x02(\r\x12M\n\nevent_info\x18\x08 \x03(\x0b\x32\x39.jackal.protobuf.LoadClientSettingsResponseData.EventInfo\x12O\n\x0b\x62\x61nner_info\x18\t \x03(\x0b\x32:.jackal.protobuf.LoadClientSettingsResponseData.BannerInfo\x12Q\n\x0c\x61ttract_info\x18\n \x03(\x0b\x32;.jackal.protobuf.LoadClientSettingsResponseData.AttractInfo\x12O\n\x0binfo_window\x18\x0b \x03(\x0b\x32:.jackal.protobuf.LoadClientSettingsResponseData.InfoWindow\x12\x18\n\x10lucky_bonus_rate\x18\x0c \x02(\r\x12\x18\n\x10\x66\x61il_support_num\x18\r \x02(\r\x12O\n\x0blucky_bonus\x18\x0e \x03(\x0b\x32:.jackal.protobuf.LoadClientSettingsResponseData.LuckyBonus\x12S\n\rspecial_bonus\x18\x0f \x03(\x0b\x32<.jackal.protobuf.LoadClientSettingsResponseData.SpecialBonus\x12\x17\n\x0f\x63hara_open_flag\x18\x10 \x02(\r\x12\x17\n\x0f\x63hara_open_date\x18\x11 \x02(\r\x12\x1b\n\x13\x63hara_pre_open_date\x18\x12 \x02(\r\x12\x11\n\tsearch_id\x18\x13 \x02(\r\x12\x16\n\x0e\x63lient_version\x18\x14 \x01(\t\x12!\n\x19\x63lient_version_start_date\x18\x15 \x01(\r\x1a\xe0\x01\n\tEventInfo\x12\x13\n\x0b\x65vent_state\x18\x65 \x02(\r\x12\x10\n\x08\x65vent_id\x18\x66 \x02(\r\x12\x1e\n\x16sp_bonus_category_id_1\x18g \x02(\r\x12\x1c\n\x14sp_bonus_key_value_1\x18h \x02(\r\x12\x1e\n\x16sp_bonus_category_id_2\x18i \x02(\r\x12\x1c\n\x14sp_bonus_key_value_2\x18j \x02(\r\x12\x18\n\x10\x65vent_start_date\x18k \x02(\r\x12\x16\n\x0e\x65vent_end_date\x18l \x02(\r\x1a\xa2\x01\n\nBannerInfo\x12\x1a\n\x11\x62\x61nner_start_date\x18\xc9\x01 \x02(\r\x12\x18\n\x0f\x62\x61nner_end_date\x18\xca\x01 \x02(\r\x12\x12\n\tbanner_id\x18\xcb\x01 \x02(\r\x12\x15\n\x0c\x62\x61nner_title\x18\xcc\x01 \x02(\t\x12\x18\n\x0f\x62\x61nner_sub_info\x18\xcd\x01 \x02(\t\x12\x19\n\x10\x62\x61nner_term_info\x18\xce\x01 \x02(\t\x1a\x84\x02\n\x0b\x41ttractInfo\x12 \n\x17\x61ttract_info_start_date\x18\xad\x02 \x02(\r\x12\x1e\n\x15\x61ttract_info_end_date\x18\xae\x02 \x02(\r\x12\x18\n\x0f\x61ttract_info_id\x18\xaf\x02 \x02(\r\x12\x1b\n\x12\x61ttract_info_title\x18\xb0\x02 \x02(\t\x12\x1e\n\x15\x61ttract_info_sub_info\x18\xb1\x02 \x02(\t\x12 \n\x17\x61ttract_info_start_info\x18\xb2\x02 \x02(\t\x12\x1e\n\x15\x61ttract_info_end_info\x18\xb3\x02 \x02(\t\x12\x1a\n\x11\x61ttract_info_text\x18\xb4\x02 \x02(\t\x1a\xfb\x01\n\nInfoWindow\x12\x1f\n\x16info_window_start_date\x18\x91\x03 \x02(\r\x12\x1d\n\x14info_window_end_date\x18\x92\x03 \x02(\r\x12\x17\n\x0einfo_window_id\x18\x93\x03 \x02(\r\x12\x1a\n\x11info_window_title\x18\x94\x03 \x02(\t\x12\x1d\n\x14info_window_sub_info\x18\x95\x03 \x02(\t\x12\x1f\n\x16info_window_start_info\x18\x96\x03 \x02(\t\x12\x1d\n\x14info_window_end_info\x18\x97\x03 \x02(\t\x12\x19\n\x10info_window_text\x18\x98\x03 \x02(\t\x1an\n\nLuckyBonus\x12 \n\x17lucky_bonus_category_id\x18\xf5\x03 \x02(\r\x12\x1c\n\x13lucky_bonus_data_id\x18\xf6\x03 \x02(\r\x12 \n\x17lucky_bonus_probability\x18\xf7\x03 \x02(\r\x1av\n\x0cSpecialBonus\x12"\n\x19special_bonus_category_id\x18\xd9\x04 \x02(\r\x12\x1e\n\x15special_bonus_data_id\x18\xda\x04 \x02(\r\x12"\n\x19special_bonus_probability\x18\xdb\x04 \x02(\r*\xb2\x03\n\x0bMessageType\x12\x08\n\x04NOOP\x10\x00\x12\x08\n\x04PING\x10\x01\x12\x10\n\x0cREGISTER_PCB\x10\x02\x12\x0c\n\x08SAVE_ADS\x10\x03\x12\x15\n\x11\x43HECK_ACCESS_CODE\x10\x04\x12\x15\n\x11SET_BNPASSID_LOCK\x10\x05\x12\r\n\tLOAD_USER\x10\x06\x12\r\n\tSAVE_USER\x10\t\x12\x13\n\x0f\x43HECK_DIAGNOSIS\x10\n\x12\x13\n\x0fSAVE_CLIENT_LOG\x10\x0b\x12\x18\n\x14PRE_LOAD_INFORMATION\x10\x0c\x12\x14\n\x10LOAD_INFORMATION\x10\r\x12\x13\n\x0fPRE_SAVE_REPLAY\x10\x0e\x12\x0f\n\x0bSAVE_REPLAY\x10\x0f\x12\x0f\n\x0bSAVE_CHARGE\x10\x10\x12\x11\n\rCHECK_RANKING\x10\x11\x12\x10\n\x0cLOAD_RANKING\x10\x12\x12\x13\n\x0fSAVE_INGAME_LOG\x10\x13\x12 \n\x1cPRE_LOAD_INFORMATION_ATTRACT\x10\x14\x12\x1c\n\x18LOAD_INFORMATION_ATTRACT\x10\x15\x12\x18\n\x14LOAD_CLIENT_SETTINGS\x10\x16' +) _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'jackal_pb2', globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "jackal_pb2", globals()) if _descriptor._USE_C_DESCRIPTORS == False: - - DESCRIPTOR._options = None - _MESSAGETYPE._serialized_start=14623 - _MESSAGETYPE._serialized_end=15057 - _REQUEST._serialized_start=34 - _REQUEST._serialized_end=1488 - _RESPONSE._serialized_start=1491 - _RESPONSE._serialized_end=2983 - _NOOPREQUESTDATA._serialized_start=2985 - _NOOPREQUESTDATA._serialized_end=3034 - _NOOPRESPONSEDATA._serialized_start=3036 - _NOOPRESPONSEDATA._serialized_end=3054 - _PINGREQUESTDATA._serialized_start=3056 - _PINGREQUESTDATA._serialized_end=3105 - _PINGRESPONSEDATA._serialized_start=3107 - _PINGRESPONSEDATA._serialized_end=3125 - _REGISTERPCBREQUESTDATA._serialized_start=3128 - _REGISTERPCBREQUESTDATA._serialized_end=3462 - _REGISTERPCBRESPONSEDATA._serialized_start=3465 - _REGISTERPCBRESPONSEDATA._serialized_end=3598 - _SAVEADSREQUESTDATA._serialized_start=3601 - _SAVEADSREQUESTDATA._serialized_end=3944 - _SAVEADSREQUESTDATA_ADSPLAYTIME._serialized_start=3887 - _SAVEADSREQUESTDATA_ADSPLAYTIME._serialized_end=3944 - _SAVEADSRESPONSEDATA._serialized_start=3946 - _SAVEADSRESPONSEDATA._serialized_end=3967 - _CHECKACCESSCODEREQUESTDATA._serialized_start=3969 - _CHECKACCESSCODEREQUESTDATA._serialized_end=4046 - _CHECKACCESSCODERESPONSEDATA._serialized_start=4048 - _CHECKACCESSCODERESPONSEDATA._serialized_end=4125 - _SETBNPASSIDLOCKREQUESTDATA._serialized_start=4128 - _SETBNPASSIDLOCKREQUESTDATA._serialized_end=4292 - _SETBNPASSIDLOCKRESPONSEDATA._serialized_start=4294 - _SETBNPASSIDLOCKRESPONSEDATA._serialized_end=4323 - _LOADUSERREQUESTDATA._serialized_start=4326 - _LOADUSERREQUESTDATA._serialized_end=4459 - _LOADUSERRESPONSEDATA._serialized_start=4462 - _LOADUSERRESPONSEDATA._serialized_end=6890 - _LOADUSERRESPONSEDATA_POKEMONDATA._serialized_start=6522 - _LOADUSERRESPONSEDATA_POKEMONDATA._serialized_end=6890 - _SAVEUSERREQUESTDATA._serialized_start=6893 - _SAVEUSERREQUESTDATA._serialized_end=8453 - _SAVEUSERREQUESTDATA_BATTLEDATA._serialized_start=7948 - _SAVEUSERREQUESTDATA_BATTLEDATA._serialized_end=8184 - _SAVEUSERREQUESTDATA_POKEMONDATA._serialized_start=8187 - _SAVEUSERREQUESTDATA_POKEMONDATA._serialized_end=8366 - _SAVEUSERREQUESTDATA_REWARDDATA._serialized_start=8368 - _SAVEUSERREQUESTDATA_REWARDDATA._serialized_end=8453 - _SAVEUSERRESPONSEDATA._serialized_start=8455 - _SAVEUSERRESPONSEDATA._serialized_end=8477 - _CHECKDIAGNOSISREQUESTDATA._serialized_start=8479 - _CHECKDIAGNOSISREQUESTDATA._serialized_end=8538 - _CHECKDIAGNOSISRESPONSEDATA._serialized_start=8541 - _CHECKDIAGNOSISRESPONSEDATA._serialized_end=8818 - _CHECKDIAGNOSISRESPONSEDATA_DIAGNOSISDATA._serialized_start=8655 - _CHECKDIAGNOSISRESPONSEDATA_DIAGNOSISDATA._serialized_end=8818 - _SAVECLIENTLOGREQUESTDATA._serialized_start=8821 - _SAVECLIENTLOGREQUESTDATA._serialized_end=9065 - _SAVECLIENTLOGRESPONSEDATA._serialized_start=9067 - _SAVECLIENTLOGRESPONSEDATA._serialized_end=9094 - _PRELOADINFORMATIONREQUESTDATA._serialized_start=9096 - _PRELOADINFORMATIONREQUESTDATA._serialized_end=9182 - _PRELOADINFORMATIONRESPONSEDATA._serialized_start=9185 - _PRELOADINFORMATIONRESPONSEDATA._serialized_end=9386 - _LOADINFORMATIONREQUESTDATA._serialized_start=9388 - _LOADINFORMATIONREQUESTDATA._serialized_end=9506 - _LOADINFORMATIONRESPONSEDATA._serialized_start=9509 - _LOADINFORMATIONRESPONSEDATA._serialized_end=9659 - _PRESAVEREPLAYREQUESTDATA._serialized_start=9662 - _PRESAVEREPLAYREQUESTDATA._serialized_end=9790 - _PRESAVEREPLAYRESPONSEDATA._serialized_start=9792 - _PRESAVEREPLAYRESPONSEDATA._serialized_end=9898 - _SAVEREPLAYREQUESTDATA._serialized_start=9901 - _SAVEREPLAYREQUESTDATA._serialized_end=10159 - _SAVEREPLAYRESPONSEDATA._serialized_start=10161 - _SAVEREPLAYRESPONSEDATA._serialized_end=10185 - _SAVECHARGEREQUESTDATA._serialized_start=10188 - _SAVECHARGEREQUESTDATA._serialized_end=10329 - _SAVECHARGERESPONSEDATA._serialized_start=10331 - _SAVECHARGERESPONSEDATA._serialized_end=10409 - _CHECKRANKINGREQUESTDATA._serialized_start=10411 - _CHECKRANKINGREQUESTDATA._serialized_end=10528 - _CHECKRANKINGRESPONSEDATA._serialized_start=10530 - _CHECKRANKINGRESPONSEDATA._serialized_end=10576 - _LOADRANKINGREQUESTDATA._serialized_start=10578 - _LOADRANKINGREQUESTDATA._serialized_end=10675 - _LOADRANKINGRESPONSEDATA._serialized_start=10678 - _LOADRANKINGRESPONSEDATA._serialized_end=11763 - _LOADRANKINGRESPONSEDATA_TRAINERDATA._serialized_start=10907 - _LOADRANKINGRESPONSEDATA_TRAINERDATA._serialized_end=11763 - _SAVEINGAMELOGREQUESTDATA._serialized_start=11765 - _SAVEINGAMELOGREQUESTDATA._serialized_end=11869 - _SAVEINGAMELOGRESPONSEDATA._serialized_start=11871 - _SAVEINGAMELOGRESPONSEDATA._serialized_end=11898 - _PRELOADINFORMATIONATTRACTREQUESTDATA._serialized_start=11900 - _PRELOADINFORMATIONATTRACTREQUESTDATA._serialized_end=11993 - _PRELOADINFORMATIONATTRACTRESPONSEDATA._serialized_start=11996 - _PRELOADINFORMATIONATTRACTRESPONSEDATA._serialized_end=12204 - _LOADINFORMATIONATTRACTREQUESTDATA._serialized_start=12206 - _LOADINFORMATIONATTRACTREQUESTDATA._serialized_end=12331 - _LOADINFORMATIONATTRACTRESPONSEDATA._serialized_start=12334 - _LOADINFORMATIONATTRACTRESPONSEDATA._serialized_end=12491 - _LOADCLIENTSETTINGSREQUESTDATA._serialized_start=12493 - _LOADCLIENTSETTINGSREQUESTDATA._serialized_end=12556 - _LOADCLIENTSETTINGSRESPONSEDATA._serialized_start=12559 - _LOADCLIENTSETTINGSRESPONSEDATA._serialized_end=14620 - _LOADCLIENTSETTINGSRESPONSEDATA_EVENTINFO._serialized_start=13482 - _LOADCLIENTSETTINGSRESPONSEDATA_EVENTINFO._serialized_end=13706 - _LOADCLIENTSETTINGSRESPONSEDATA_BANNERINFO._serialized_start=13709 - _LOADCLIENTSETTINGSRESPONSEDATA_BANNERINFO._serialized_end=13871 - _LOADCLIENTSETTINGSRESPONSEDATA_ATTRACTINFO._serialized_start=13874 - _LOADCLIENTSETTINGSRESPONSEDATA_ATTRACTINFO._serialized_end=14134 - _LOADCLIENTSETTINGSRESPONSEDATA_INFOWINDOW._serialized_start=14137 - _LOADCLIENTSETTINGSRESPONSEDATA_INFOWINDOW._serialized_end=14388 - _LOADCLIENTSETTINGSRESPONSEDATA_LUCKYBONUS._serialized_start=14390 - _LOADCLIENTSETTINGSRESPONSEDATA_LUCKYBONUS._serialized_end=14500 - _LOADCLIENTSETTINGSRESPONSEDATA_SPECIALBONUS._serialized_start=14502 - _LOADCLIENTSETTINGSRESPONSEDATA_SPECIALBONUS._serialized_end=14620 + DESCRIPTOR._options = None + _MESSAGETYPE._serialized_start = 14623 + _MESSAGETYPE._serialized_end = 15057 + _REQUEST._serialized_start = 34 + _REQUEST._serialized_end = 1488 + _RESPONSE._serialized_start = 1491 + _RESPONSE._serialized_end = 2983 + _NOOPREQUESTDATA._serialized_start = 2985 + _NOOPREQUESTDATA._serialized_end = 3034 + _NOOPRESPONSEDATA._serialized_start = 3036 + _NOOPRESPONSEDATA._serialized_end = 3054 + _PINGREQUESTDATA._serialized_start = 3056 + _PINGREQUESTDATA._serialized_end = 3105 + _PINGRESPONSEDATA._serialized_start = 3107 + _PINGRESPONSEDATA._serialized_end = 3125 + _REGISTERPCBREQUESTDATA._serialized_start = 3128 + _REGISTERPCBREQUESTDATA._serialized_end = 3462 + _REGISTERPCBRESPONSEDATA._serialized_start = 3465 + _REGISTERPCBRESPONSEDATA._serialized_end = 3598 + _SAVEADSREQUESTDATA._serialized_start = 3601 + _SAVEADSREQUESTDATA._serialized_end = 3944 + _SAVEADSREQUESTDATA_ADSPLAYTIME._serialized_start = 3887 + _SAVEADSREQUESTDATA_ADSPLAYTIME._serialized_end = 3944 + _SAVEADSRESPONSEDATA._serialized_start = 3946 + _SAVEADSRESPONSEDATA._serialized_end = 3967 + _CHECKACCESSCODEREQUESTDATA._serialized_start = 3969 + _CHECKACCESSCODEREQUESTDATA._serialized_end = 4046 + _CHECKACCESSCODERESPONSEDATA._serialized_start = 4048 + _CHECKACCESSCODERESPONSEDATA._serialized_end = 4125 + _SETBNPASSIDLOCKREQUESTDATA._serialized_start = 4128 + _SETBNPASSIDLOCKREQUESTDATA._serialized_end = 4292 + _SETBNPASSIDLOCKRESPONSEDATA._serialized_start = 4294 + _SETBNPASSIDLOCKRESPONSEDATA._serialized_end = 4323 + _LOADUSERREQUESTDATA._serialized_start = 4326 + _LOADUSERREQUESTDATA._serialized_end = 4459 + _LOADUSERRESPONSEDATA._serialized_start = 4462 + _LOADUSERRESPONSEDATA._serialized_end = 6890 + _LOADUSERRESPONSEDATA_POKEMONDATA._serialized_start = 6522 + _LOADUSERRESPONSEDATA_POKEMONDATA._serialized_end = 6890 + _SAVEUSERREQUESTDATA._serialized_start = 6893 + _SAVEUSERREQUESTDATA._serialized_end = 8453 + _SAVEUSERREQUESTDATA_BATTLEDATA._serialized_start = 7948 + _SAVEUSERREQUESTDATA_BATTLEDATA._serialized_end = 8184 + _SAVEUSERREQUESTDATA_POKEMONDATA._serialized_start = 8187 + _SAVEUSERREQUESTDATA_POKEMONDATA._serialized_end = 8366 + _SAVEUSERREQUESTDATA_REWARDDATA._serialized_start = 8368 + _SAVEUSERREQUESTDATA_REWARDDATA._serialized_end = 8453 + _SAVEUSERRESPONSEDATA._serialized_start = 8455 + _SAVEUSERRESPONSEDATA._serialized_end = 8477 + _CHECKDIAGNOSISREQUESTDATA._serialized_start = 8479 + _CHECKDIAGNOSISREQUESTDATA._serialized_end = 8538 + _CHECKDIAGNOSISRESPONSEDATA._serialized_start = 8541 + _CHECKDIAGNOSISRESPONSEDATA._serialized_end = 8818 + _CHECKDIAGNOSISRESPONSEDATA_DIAGNOSISDATA._serialized_start = 8655 + _CHECKDIAGNOSISRESPONSEDATA_DIAGNOSISDATA._serialized_end = 8818 + _SAVECLIENTLOGREQUESTDATA._serialized_start = 8821 + _SAVECLIENTLOGREQUESTDATA._serialized_end = 9065 + _SAVECLIENTLOGRESPONSEDATA._serialized_start = 9067 + _SAVECLIENTLOGRESPONSEDATA._serialized_end = 9094 + _PRELOADINFORMATIONREQUESTDATA._serialized_start = 9096 + _PRELOADINFORMATIONREQUESTDATA._serialized_end = 9182 + _PRELOADINFORMATIONRESPONSEDATA._serialized_start = 9185 + _PRELOADINFORMATIONRESPONSEDATA._serialized_end = 9386 + _LOADINFORMATIONREQUESTDATA._serialized_start = 9388 + _LOADINFORMATIONREQUESTDATA._serialized_end = 9506 + _LOADINFORMATIONRESPONSEDATA._serialized_start = 9509 + _LOADINFORMATIONRESPONSEDATA._serialized_end = 9659 + _PRESAVEREPLAYREQUESTDATA._serialized_start = 9662 + _PRESAVEREPLAYREQUESTDATA._serialized_end = 9790 + _PRESAVEREPLAYRESPONSEDATA._serialized_start = 9792 + _PRESAVEREPLAYRESPONSEDATA._serialized_end = 9898 + _SAVEREPLAYREQUESTDATA._serialized_start = 9901 + _SAVEREPLAYREQUESTDATA._serialized_end = 10159 + _SAVEREPLAYRESPONSEDATA._serialized_start = 10161 + _SAVEREPLAYRESPONSEDATA._serialized_end = 10185 + _SAVECHARGEREQUESTDATA._serialized_start = 10188 + _SAVECHARGEREQUESTDATA._serialized_end = 10329 + _SAVECHARGERESPONSEDATA._serialized_start = 10331 + _SAVECHARGERESPONSEDATA._serialized_end = 10409 + _CHECKRANKINGREQUESTDATA._serialized_start = 10411 + _CHECKRANKINGREQUESTDATA._serialized_end = 10528 + _CHECKRANKINGRESPONSEDATA._serialized_start = 10530 + _CHECKRANKINGRESPONSEDATA._serialized_end = 10576 + _LOADRANKINGREQUESTDATA._serialized_start = 10578 + _LOADRANKINGREQUESTDATA._serialized_end = 10675 + _LOADRANKINGRESPONSEDATA._serialized_start = 10678 + _LOADRANKINGRESPONSEDATA._serialized_end = 11763 + _LOADRANKINGRESPONSEDATA_TRAINERDATA._serialized_start = 10907 + _LOADRANKINGRESPONSEDATA_TRAINERDATA._serialized_end = 11763 + _SAVEINGAMELOGREQUESTDATA._serialized_start = 11765 + _SAVEINGAMELOGREQUESTDATA._serialized_end = 11869 + _SAVEINGAMELOGRESPONSEDATA._serialized_start = 11871 + _SAVEINGAMELOGRESPONSEDATA._serialized_end = 11898 + _PRELOADINFORMATIONATTRACTREQUESTDATA._serialized_start = 11900 + _PRELOADINFORMATIONATTRACTREQUESTDATA._serialized_end = 11993 + _PRELOADINFORMATIONATTRACTRESPONSEDATA._serialized_start = 11996 + _PRELOADINFORMATIONATTRACTRESPONSEDATA._serialized_end = 12204 + _LOADINFORMATIONATTRACTREQUESTDATA._serialized_start = 12206 + _LOADINFORMATIONATTRACTREQUESTDATA._serialized_end = 12331 + _LOADINFORMATIONATTRACTRESPONSEDATA._serialized_start = 12334 + _LOADINFORMATIONATTRACTRESPONSEDATA._serialized_end = 12491 + _LOADCLIENTSETTINGSREQUESTDATA._serialized_start = 12493 + _LOADCLIENTSETTINGSREQUESTDATA._serialized_end = 12556 + _LOADCLIENTSETTINGSRESPONSEDATA._serialized_start = 12559 + _LOADCLIENTSETTINGSRESPONSEDATA._serialized_end = 14620 + _LOADCLIENTSETTINGSRESPONSEDATA_EVENTINFO._serialized_start = 13482 + _LOADCLIENTSETTINGSRESPONSEDATA_EVENTINFO._serialized_end = 13706 + _LOADCLIENTSETTINGSRESPONSEDATA_BANNERINFO._serialized_start = 13709 + _LOADCLIENTSETTINGSRESPONSEDATA_BANNERINFO._serialized_end = 13871 + _LOADCLIENTSETTINGSRESPONSEDATA_ATTRACTINFO._serialized_start = 13874 + _LOADCLIENTSETTINGSRESPONSEDATA_ATTRACTINFO._serialized_end = 14134 + _LOADCLIENTSETTINGSRESPONSEDATA_INFOWINDOW._serialized_start = 14137 + _LOADCLIENTSETTINGSRESPONSEDATA_INFOWINDOW._serialized_end = 14388 + _LOADCLIENTSETTINGSRESPONSEDATA_LUCKYBONUS._serialized_start = 14390 + _LOADCLIENTSETTINGSRESPONSEDATA_LUCKYBONUS._serialized_end = 14500 + _LOADCLIENTSETTINGSRESPONSEDATA_SPECIALBONUS._serialized_start = 14502 + _LOADCLIENTSETTINGSRESPONSEDATA_SPECIALBONUS._serialized_end = 14620 # @@protoc_insertion_point(module_scope) diff --git a/titles/wacca/__init__.py b/titles/wacca/__init__.py index 55205ed..b6e06f8 100644 --- a/titles/wacca/__init__.py +++ b/titles/wacca/__init__.py @@ -9,4 +9,4 @@ database = WaccaData reader = WaccaReader frontend = WaccaFrontend game_codes = [WaccaConstants.GAME_CODE] -current_schema_version = 3 \ No newline at end of file +current_schema_version = 3 diff --git a/titles/wacca/base.py b/titles/wacca/base.py index 2e5001f..bc0c09a 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -11,13 +11,14 @@ from titles.wacca.database import WaccaData from titles.wacca.handlers import * from core.const import AllnetCountryCode -class WaccaBase(): + +class WaccaBase: def __init__(self, cfg: CoreConfig, game_cfg: WaccaConfig) -> None: - self.config = cfg # Config file - self.game_config = game_cfg # Game Config file - self.game = WaccaConstants.GAME_CODE # Game code - self.version = WaccaConstants.VER_WACCA # Game version - self.data = WaccaData(cfg) # Database + self.config = cfg # Config file + self.game_config = game_cfg # Game Config file + self.game = WaccaConstants.GAME_CODE # Game code + self.version = WaccaConstants.VER_WACCA # Game version + self.data = WaccaData(cfg) # Database self.logger = logging.getLogger("wacca") self.srvtime = datetime.now() self.season = 1 @@ -29,7 +30,6 @@ class WaccaBase(): "note_color": 203001, "bgm_volume": 10, "bg_video": 0, - "mirror": 0, "judge_display_pos": 0, "judge_detail_display": 0, @@ -57,35 +57,39 @@ class WaccaBase(): "bonus_note_vol": 8, "gate_skip": 0, "key_beam_display": 1, - "left_slide_note_color": 4, "right_slide_note_color": 3, "forward_slide_note_color": 1, "back_slide_note_color": 2, - "master_vol": 3, "set_title_id": 104001, "set_icon_id": 102001, "set_nav_id": 210001, - "set_plate_id": 211001 + "set_plate_id": 211001, } self.allowed_stages = [] - prefecture_name = inflection.underscore(game_cfg.server.prefecture_name).replace(' ', '_').upper() + prefecture_name = ( + inflection.underscore(game_cfg.server.prefecture_name) + .replace(" ", "_") + .upper() + ) if prefecture_name not in [region.name for region in WaccaConstants.Region]: - self.logger.warning(f"Invalid prefecture name {game_cfg.server.prefecture_name} in config file") + self.logger.warning( + f"Invalid prefecture name {game_cfg.server.prefecture_name} in config file" + ) self.region_id = WaccaConstants.Region.HOKKAIDO - + else: self.region_id = WaccaConstants.Region[prefecture_name] - + def handle_housing_get_request(self, data: Dict) -> Dict: req = BaseRequest(data) housing_id = 1337 self.logger.info(f"{req.chipId} -> {housing_id}") resp = HousingGetResponse(housing_id) return resp.make() - + def handle_advertise_GetRanking_request(self, data: Dict) -> Dict: req = AdvertiseGetRankingRequest(data) return AdvertiseGetRankingResponse().make() @@ -100,25 +104,27 @@ class WaccaBase(): if req.appVersion.country == AllnetCountryCode.JAPAN.value: if allnet_region_id is not None: - region = WaccaConstants.allnet_region_id_to_wacca_region(allnet_region_id) - + region = WaccaConstants.allnet_region_id_to_wacca_region( + allnet_region_id + ) + if region is None: region_id = self.region_id else: region_id = region - + else: region_id = self.region_id - + elif req.appVersion.country in WaccaConstants.VALID_COUNTRIES: region_id = WaccaConstants.Region[req.appVersion.country] - + else: region_id = WaccaConstants.Region.NONE resp = HousingStartResponseV1(region_id) return resp.make() - + def handle_advertise_GetNews_request(self, data: Dict) -> Dict: resp = GetNewsResponseV1() return resp.make() @@ -128,7 +134,7 @@ class WaccaBase(): self.logger.info(f"Log out user {req.userId} from {req.chipId}") return BaseResponse().make() - 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() @@ -137,13 +143,13 @@ class WaccaBase(): self.logger.info(f"No user exists for aime id {req.aimeId}") resp.profileStatus = ProfileStatus.ProfileRegister return resp.make() - + self.logger.info(f"User preview for {req.aimeId} from {req.chipId}") if profile["last_game_ver"] is None: resp.lastGameVersion = ShortVersion(str(req.appVersion)) else: resp.lastGameVersion = ShortVersion(profile["last_game_ver"]) - + resp.userStatus.userId = profile["id"] resp.userStatus.username = profile["username"] resp.userStatus.xp = profile["xp"] @@ -152,25 +158,29 @@ class WaccaBase(): resp.userStatus.wp = profile["wp"] resp.userStatus.useCount = profile["login_count"] - set_title_id = self.data.profile.get_options(WaccaConstants.OPTIONS["set_title_id"], profile["user"]) + set_title_id = self.data.profile.get_options( + WaccaConstants.OPTIONS["set_title_id"], profile["user"] + ) if set_title_id is None: set_title_id = self.OPTIONS_DEFAULTS["set_title_id"] resp.setTitleId = set_title_id - set_icon_id = self.data.profile.get_options(WaccaConstants.OPTIONS["set_title_id"], profile["user"]) + set_icon_id = self.data.profile.get_options( + WaccaConstants.OPTIONS["set_title_id"], profile["user"] + ) if set_icon_id is None: set_icon_id = self.OPTIONS_DEFAULTS["set_icon_id"] - resp.setIconId = set_icon_id - + resp.setIconId = set_icon_id + if req.appVersion > resp.lastGameVersion: resp.versionStatus = PlayVersionStatus.VersionUpgrade - + elif req.appVersion < resp.lastGameVersion: resp.versionStatus = PlayVersionStatus.VersionTooNew - + return resp.make() - 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_new_day = False @@ -180,66 +190,98 @@ class WaccaBase(): if req.userId == 0: self.logger.info(f"Guest login on {req.chipId}") resp.lastLoginDate = 0 - + else: profile = self.data.profile.get_profile(req.userId) if profile is None: - self.logger.warn(f"Unknown user id {req.userId} attempted login from {req.chipId}") + self.logger.warn( + f"Unknown user id {req.userId} attempted login from {req.chipId}" + ) return resp.make() self.logger.info(f"User {req.userId} login on {req.chipId}") last_login_time = int(profile["last_login_date"].timestamp()) resp.lastLoginDate = last_login_time - - # If somebodies login timestamp < midnight of current day, then they are logging in for the first time today - if last_login_time < int(datetime.now().replace(hour=0,minute=0,second=0,microsecond=0).timestamp()): + + # If somebodies login timestamp < midnight of current day, then they are logging in for the first time today + if last_login_time < int( + datetime.now() + .replace(hour=0, minute=0, second=0, microsecond=0) + .timestamp() + ): is_new_day = True is_consec_day = True # If somebodies login timestamp > midnight of current day + 1 day, then they broke their daily login streak - elif last_login_time > int((datetime.now().replace(hour=0,minute=0,second=0,microsecond=0) + timedelta(days=1)).timestamp()): + elif last_login_time > int( + ( + datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) + + timedelta(days=1) + ).timestamp() + ): is_consec_day = False # else, they are simply logging in again on the same day, and we don't need to do anything for that - + self.data.profile.session_login(req.userId, is_new_day, is_consec_day) resp.firstLoginDaily = int(is_new_day) - + return resp.make() - - def handle_user_status_create_request(self, data: Dict)-> Dict: + + def handle_user_status_create_request(self, data: Dict) -> Dict: req = UserStatusCreateRequest(data) - profileId = self.data.profile.create_profile(req.aimeId, req.username, self.version) + profileId = self.data.profile.create_profile( + req.aimeId, req.username, self.version + ) - if profileId is None: return BaseResponse().make() + if profileId is None: + return BaseResponse().make() # Insert starting items self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["title"], 104001) self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["title"], 104002) self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["title"], 104003) self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["title"], 104005) - + self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["icon"], 102001) self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["icon"], 102002) - - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["note_color"], 103001) - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["note_color"], 203001) - - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["note_sound"], 105001) - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["note_sound"], 205005) # Added lily - - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210001) - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["user_plate"], 211001) # Added lily + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["note_color"], 103001 + ) + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["note_color"], 203001 + ) + + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["note_sound"], 105001 + ) + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["note_sound"], 205005 + ) # Added lily + + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210001 + ) + + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["user_plate"], 211001 + ) # Added lily + + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["touch_effect"], 312000 + ) # Added reverse + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["touch_effect"], 312001 + ) # Added reverse + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["touch_effect"], 312002 + ) # Added reverse - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["touch_effect"], 312000) # Added reverse - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["touch_effect"], 312001) # Added reverse - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["touch_effect"], 312002) # Added reverse - return UserStatusCreateResponseV2(profileId, req.username).make() - def handle_user_status_getDetail_request(self, data: Dict)-> Dict: + def handle_user_status_getDetail_request(self, data: Dict) -> Dict: req = UserStatusGetDetailRequest(data) resp = UserStatusGetDetailResponseV1() @@ -256,7 +298,7 @@ class WaccaBase(): 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) + profile_tickets = self.data.item.get_tickets(user_id) resp.songUpdateTime = int(profile["last_login_date"].timestamp()) resp.songPlayStatus = [profile["last_song_id"], 1] @@ -271,29 +313,41 @@ class WaccaBase(): if self.game_config.mods.infinite_wp: resp.userStatus.wp = 999999 - + if profile["friend_view_1"] is not None: pass if profile["friend_view_2"] is not None: pass if profile["friend_view_3"] is not None: pass - - resp.seasonalPlayModeCounts.append(PlayModeCounts(self.season, 1, profile["playcount_single"])) - resp.seasonalPlayModeCounts.append(PlayModeCounts(self.season, 2, profile["playcount_multi_vs"])) - resp.seasonalPlayModeCounts.append(PlayModeCounts(self.season, 3, profile["playcount_multi_coop"])) - resp.seasonalPlayModeCounts.append(PlayModeCounts(self.season, 4, profile["playcount_stageup"])) - + + resp.seasonalPlayModeCounts.append( + PlayModeCounts(self.season, 1, profile["playcount_single"]) + ) + resp.seasonalPlayModeCounts.append( + PlayModeCounts(self.season, 2, profile["playcount_multi_vs"]) + ) + resp.seasonalPlayModeCounts.append( + PlayModeCounts(self.season, 3, profile["playcount_multi_coop"]) + ) + resp.seasonalPlayModeCounts.append( + PlayModeCounts(self.season, 4, profile["playcount_stageup"]) + ) + for opt in profile_options: resp.options.append(UserOption(opt["opt_id"], opt["value"])) - + for unlock in profile_song_unlocks: for x in range(1, unlock["highest_difficulty"] + 1): - resp.userItems.songUnlocks.append(SongUnlock(unlock["song_id"], x, 0, int(unlock["acquire_date"].timestamp()))) - + resp.userItems.songUnlocks.append( + SongUnlock( + unlock["song_id"], x, 0, int(unlock["acquire_date"].timestamp()) + ) + ) + for song in profile_scores: resp.seasonInfo.cumulativeScore += song["score"] - + clear_cts = SongDetailClearCounts( song["play_ct"], song["clear_ct"], @@ -303,13 +357,20 @@ class WaccaBase(): ) grade_cts = SongDetailGradeCountsV1( - song["grade_d_ct"], song["grade_c_ct"], song["grade_b_ct"], song["grade_a_ct"], song["grade_aa_ct"], - song["grade_aaa_ct"], song["grade_s_ct"], song["grade_ss_ct"], song["grade_sss_ct"], - song["grade_master_ct"] + song["grade_d_ct"], + song["grade_c_ct"], + song["grade_b_ct"], + song["grade_a_ct"], + song["grade_aa_ct"], + song["grade_aaa_ct"], + song["grade_s_ct"], + song["grade_ss_ct"], + song["grade_sss_ct"], + song["grade_master_ct"], ) deets = BestScoreDetailV1(song["song_id"], song["chart_id"]) - deets.clearCounts = clear_cts + deets.clearCounts = clear_cts deets.clearCountsSeason = clear_cts deets.gradeCounts = grade_cts deets.score = song["score"] @@ -318,9 +379,16 @@ class WaccaBase(): deets.rating = song["rating"] resp.scores.append(deets) - + for trophy in profile_trophies: - resp.userItems.trophies.append(TrophyItem(trophy["trophy_id"], trophy["season"], trophy["progress"], trophy["badge_type"])) + resp.userItems.trophies.append( + TrophyItem( + trophy["trophy_id"], + trophy["season"], + trophy["progress"], + trophy["badge_type"], + ) + ) if self.game_config.mods.infinite_tickets: for x in range(5): @@ -332,21 +400,31 @@ class WaccaBase(): else: expire = int(ticket["expire_date"].timestamp()) - resp.userItems.tickets.append(TicketItem(ticket["id"], ticket["ticket_id"], expire)) + resp.userItems.tickets.append( + TicketItem(ticket["id"], ticket["ticket_id"], expire) + ) if profile_items: for item in profile_items: try: - if item["type"] == WaccaConstants.ITEM_TYPES["icon"]: - resp.userItems.icons.append(IconItem(item["item_id"], 1, item["use_count"], int(item["acquire_date"].timestamp()))) + resp.userItems.icons.append( + IconItem( + item["item_id"], + 1, + item["use_count"], + int(item["acquire_date"].timestamp()), + ) + ) else: - itm_send = GenericItemSend(item["item_id"], 1, int(item["acquire_date"].timestamp())) + itm_send = GenericItemSend( + item["item_id"], 1, int(item["acquire_date"].timestamp()) + ) if item["type"] == WaccaConstants.ITEM_TYPES["title"]: resp.userItems.titles.append(itm_send) - + elif item["type"] == WaccaConstants.ITEM_TYPES["note_color"]: resp.userItems.noteColors.append(itm_send) @@ -354,7 +432,9 @@ class WaccaBase(): resp.userItems.noteSounds.append(itm_send) except: - self.logger.error(f"{__name__} Failed to load item {item['item_id']} for user {profile['user']}") + self.logger.error( + f"{__name__} Failed to load item {item['item_id']} for user {profile['user']}" + ) resp.seasonInfo.level = profile["xp"] resp.seasonInfo.wpObtained = profile["wp_total"] @@ -365,26 +445,28 @@ class WaccaBase(): resp.seasonInfo.noteSoundsObtained = len(resp.userItems.noteSounds) return resp.make() - - 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 = self.data.profile.profile_to_aime_user(req.profileId) if user_id is None: - self.logger.error(f"handle_user_trial_get_request: No profile with id {req.profileId}") + self.logger.error( + f"handle_user_trial_get_request: No profile with id {req.profileId}" + ) return resp.make() self.logger.info(f"Get trial info for user {req.profileId}") stages = self.data.score.get_stageup(user_id, self.version) if stages is None: stages = [] - + tmp: List[StageInfo] = [] for d in self.allowed_stages: stage_info = StageInfo(d[0], d[1]) - + for score in stages: if score["stage_id"] == stage_info.danId: stage_info.clearStatus = score["clear_status"] @@ -393,38 +475,55 @@ class WaccaBase(): stage_info.song2BestScore = score["song2_score"] stage_info.song3BestScore = score["song3_score"] break - + tmp.append(stage_info) for x in range(len(tmp)): - if tmp[x].danLevel >= 10 and (tmp[x + 1].clearStatus >= 1 or tmp[x].clearStatus >= 1): + if tmp[x].danLevel >= 10 and ( + tmp[x + 1].clearStatus >= 1 or tmp[x].clearStatus >= 1 + ): resp.stageList.append(tmp[x]) elif tmp[x].danLevel < 10: resp.stageList.append(tmp[x]) return resp.make() - def handle_user_trial_update_request(self, data: Dict)-> Dict: + def handle_user_trial_update_request(self, data: Dict) -> Dict: req = UserTrialUpdateRequest(data) total_score = 0 for score in req.songScores: total_score += score - + while len(req.songScores) < 3: req.songScores.append(0) profile = self.data.profile.get_profile(req.profileId) - - user_id = profile["user"] - old_stage = self.data.score.get_stageup_stage(user_id, self.version, req.stageId) - if old_stage is None: - self.data.score.put_stageup(user_id, self.version, req.stageId, req.clearType.value, req.numSongsCleared, req.songScores[0], req.songScores[1], req.songScores[2]) - + user_id = profile["user"] + old_stage = self.data.score.get_stageup_stage( + user_id, self.version, req.stageId + ) + + if old_stage is None: + self.data.score.put_stageup( + user_id, + self.version, + req.stageId, + req.clearType.value, + req.numSongsCleared, + req.songScores[0], + req.songScores[1], + req.songScores[2], + ) + else: # We only care about total score for best of, even if one score happens to be lower (I think) - if total_score > (old_stage["song1_score"] + old_stage["song2_score"] + old_stage["song3_score"]): + if total_score > ( + old_stage["song1_score"] + + old_stage["song2_score"] + + old_stage["song3_score"] + ): best_score1 = req.songScores[0] best_score2 = req.songScores[1] best_score3 = req.songScores[2] @@ -433,18 +532,37 @@ class WaccaBase(): best_score2 = old_stage["song2_score"] best_score3 = old_stage["song3_score"] - self.data.score.put_stageup(user_id, self.version, req.stageId, req.clearType.value, req.numSongsCleared, - best_score1, best_score2, best_score3) - - if req.stageLevel > 0 and req.stageLevel <= 14 and req.clearType.value > 0: # For some reason, special stages send dan level 1001... - if req.stageLevel > profile["dan_level"] or (req.stageLevel == profile["dan_level"] and req.clearType.value > profile["dan_type"]): - self.data.profile.update_profile_dan(req.profileId, req.stageLevel, req.clearType.value) + self.data.score.put_stageup( + user_id, + self.version, + req.stageId, + req.clearType.value, + req.numSongsCleared, + best_score1, + best_score2, + best_score3, + ) + + if ( + req.stageLevel > 0 and req.stageLevel <= 14 and req.clearType.value > 0 + ): # For some reason, special stages send dan level 1001... + if req.stageLevel > profile["dan_level"] or ( + req.stageLevel == profile["dan_level"] + and req.clearType.value > profile["dan_type"] + ): + self.data.profile.update_profile_dan( + req.profileId, req.stageLevel, req.clearType.value + ) self.util_put_items(req.profileId, user_id, req.itemsObtained) # user/status/update isn't called after stageup so we have to do some things now - current_icon = self.data.profile.get_options(user_id, WaccaConstants.OPTIONS["set_icon_id"]) - current_nav = self.data.profile.get_options(user_id, WaccaConstants.OPTIONS["set_nav_id"]) + current_icon = self.data.profile.get_options( + user_id, WaccaConstants.OPTIONS["set_icon_id"] + ) + current_nav = self.data.profile.get_options( + user_id, WaccaConstants.OPTIONS["set_nav_id"] + ) if current_icon is None: current_icon = self.OPTIONS_DEFAULTS["set_icon_id"] @@ -455,56 +573,74 @@ class WaccaBase(): else: current_nav = current_nav["value"] - self.data.item.put_item(user_id, WaccaConstants.ITEM_TYPES["icon"], current_icon) - self.data.item.put_item(user_id, WaccaConstants.ITEM_TYPES["navigator"], current_nav) - self.data.profile.update_profile_playtype(req.profileId, 4, data["appVersion"][:7]) + self.data.item.put_item( + user_id, WaccaConstants.ITEM_TYPES["icon"], current_icon + ) + self.data.item.put_item( + user_id, WaccaConstants.ITEM_TYPES["navigator"], current_nav + ) + self.data.profile.update_profile_playtype( + req.profileId, 4, data["appVersion"][:7] + ) return BaseResponse().make() - - def handle_user_sugoroku_update_request(self, data: Dict)-> Dict: + + def handle_user_sugoroku_update_request(self, data: Dict) -> Dict: ver_split = data["appVersion"].split(".") resp = BaseResponse() if int(ver_split[0]) <= 2 and int(ver_split[1]) < 53: req = UserSugarokuUpdateRequestV1(data) mission_flg = 0 - + else: req = UserSugarokuUpdateRequestV2(data) mission_flg = req.mission_flag user_id = self.data.profile.profile_to_aime_user(req.profileId) - if user_id is None: - self.logger.info(f"handle_user_sugoroku_update_request unknwon profile ID {req.profileId}") + if user_id is None: + self.logger.info( + f"handle_user_sugoroku_update_request unknwon profile ID {req.profileId}" + ) return resp.make() self.util_put_items(req.profileId, user_id, req.itemsObtainted) - self.data.profile.update_gate(user_id, req.gateId, req.page, req.progress, req.loops, mission_flg, req.totalPts) + self.data.profile.update_gate( + user_id, + req.gateId, + req.page, + req.progress, + req.loops, + mission_flg, + req.totalPts, + ) return resp.make() - - def handle_user_info_getMyroom_request(self, data: Dict)-> Dict: + + def handle_user_info_getMyroom_request(self, data: Dict) -> Dict: return UserInfogetMyroomResponseV1().make() - - def handle_user_music_unlock_request(self, data: Dict)-> Dict: + + def handle_user_music_unlock_request(self, data: Dict) -> Dict: req = UserMusicUnlockRequest(data) profile = self.data.profile.get_profile(req.profileId) - if profile is None: return BaseResponse().make() + if profile is None: + return BaseResponse().make() user_id = profile["user"] current_wp = profile["wp"] tickets = self.data.item.get_tickets(user_id) new_tickets = [] - + for ticket in tickets: new_tickets.append([ticket["id"], ticket["ticket_id"], 9999999999]) - + for item in req.itemsUsed: if item.itemType == WaccaConstants.ITEM_TYPES["wp"]: if current_wp >= item.quantity: current_wp -= item.quantity self.data.profile.spend_wp(req.profileId, item.quantity) - else: return BaseResponse().make() + else: + return BaseResponse().make() elif item.itemType == WaccaConstants.ITEM_TYPES["ticket"]: for x in range(len(new_tickets)): @@ -515,12 +651,22 @@ class WaccaBase(): # wp, ticket info if req.difficulty > WaccaConstants.Difficulty.HARD.value: - old_score = self.data.score.get_best_score(user_id, req.songId, req.difficulty) + old_score = self.data.score.get_best_score( + user_id, req.songId, req.difficulty + ) if not old_score: - self.data.score.put_best_score(user_id, req.songId, req.difficulty, 0, [0] * 5, [0] * 13, 0, 0) - - self.data.item.unlock_song(user_id, req.songId, req.difficulty if req.difficulty > WaccaConstants.Difficulty.HARD.value else WaccaConstants.Difficulty.HARD.value) - + self.data.score.put_best_score( + user_id, req.songId, req.difficulty, 0, [0] * 5, [0] * 13, 0, 0 + ) + + self.data.item.unlock_song( + user_id, + req.songId, + req.difficulty + if req.difficulty > WaccaConstants.Difficulty.HARD.value + else WaccaConstants.Difficulty.HARD.value, + ) + if self.game_config.mods.infinite_tickets: new_tickets = [ [0, 106002, 0], @@ -529,18 +675,18 @@ class WaccaBase(): [3, 106002, 0], [4, 106002, 0], ] - + if self.game_config.mods.infinite_wp: current_wp = 999999 return UserMusicUnlockResponse(current_wp, new_tickets).make() - - def handle_user_info_getRanking_request(self, data: Dict)-> 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() - - 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() @@ -556,27 +702,49 @@ class WaccaBase(): resp.songDetail.difficulty = req.songDetail.difficulty if req.profileId == 0: - self.logger.info(f"Guest score for song {req.songDetail.songId} difficulty {req.songDetail.difficulty}") + self.logger.info( + f"Guest score for song {req.songDetail.songId} difficulty {req.songDetail.difficulty}" + ) return resp.make() - + profile = self.data.profile.get_profile(req.profileId) - + if profile is None: - self.logger.warn(f"handle_user_music_update_request: No profile for game_id {req.profileId}") + self.logger.warn( + f"handle_user_music_update_request: No profile for game_id {req.profileId}" + ) return resp.make() - + user_id = profile["user"] self.util_put_items(req.profileId, user_id, req.itemsObtained) - playlog_clear_status = req.songDetail.flagCleared + req.songDetail.flagMissless + req.songDetail.flagFullcombo + \ - req.songDetail.flagAllMarvelous - - self.data.score.put_playlog(user_id, req.songDetail.songId, req.songDetail.difficulty, req.songDetail.score, - playlog_clear_status, req.songDetail.grade.value, req.songDetail.maxCombo, req.songDetail.judgements.marvCt, - req.songDetail.judgements.greatCt, req.songDetail.judgements.goodCt, req.songDetail.judgements.missCt, - req.songDetail.fastCt, req.songDetail.slowCt, self.season) + playlog_clear_status = ( + req.songDetail.flagCleared + + req.songDetail.flagMissless + + req.songDetail.flagFullcombo + + req.songDetail.flagAllMarvelous + ) - old_score = self.data.score.get_best_score(user_id, req.songDetail.songId, req.songDetail.difficulty) + self.data.score.put_playlog( + user_id, + req.songDetail.songId, + req.songDetail.difficulty, + req.songDetail.score, + playlog_clear_status, + req.songDetail.grade.value, + req.songDetail.maxCombo, + req.songDetail.judgements.marvCt, + req.songDetail.judgements.greatCt, + req.songDetail.judgements.goodCt, + req.songDetail.judgements.missCt, + req.songDetail.fastCt, + req.songDetail.slowCt, + self.season, + ) + + old_score = self.data.score.get_best_score( + user_id, req.songDetail.songId, req.songDetail.difficulty + ) if not old_score: grades = [0] * 13 @@ -590,9 +758,17 @@ class WaccaBase(): grades[req.songDetail.grade.value - 1] = 1 - self.data.score.put_best_score(user_id, req.songDetail.songId, req.songDetail.difficulty, req.songDetail.score, - clears, grades, req.songDetail.maxCombo, req.songDetail.judgements.missCt) - + self.data.score.put_best_score( + user_id, + req.songDetail.songId, + req.songDetail.difficulty, + req.songDetail.score, + clears, + grades, + req.songDetail.maxCombo, + req.songDetail.judgements.missCt, + ) + resp.songDetail.score = req.songDetail.score resp.songDetail.lowestMissCount = req.songDetail.judgements.missCt @@ -630,60 +806,76 @@ class WaccaBase(): best_score = max(req.songDetail.score, old_score["score"]) best_max_combo = max(req.songDetail.maxCombo, old_score["best_combo"]) - lowest_miss_ct = min(req.songDetail.judgements.missCt, old_score["lowest_miss_ct"]) - best_rating = max(self.util_calc_song_rating(req.songDetail.score, req.songDetail.level), old_score["rating"]) - - self.data.score.put_best_score(user_id, req.songDetail.songId, req.songDetail.difficulty, best_score, clears, - grades, best_max_combo, lowest_miss_ct) - + lowest_miss_ct = min( + req.songDetail.judgements.missCt, old_score["lowest_miss_ct"] + ) + best_rating = max( + self.util_calc_song_rating(req.songDetail.score, req.songDetail.level), + old_score["rating"], + ) + + self.data.score.put_best_score( + user_id, + req.songDetail.songId, + req.songDetail.difficulty, + best_score, + clears, + grades, + best_max_combo, + lowest_miss_ct, + ) + resp.songDetail.score = best_score resp.songDetail.lowestMissCount = lowest_miss_ct resp.songDetail.rating = best_rating resp.songDetail.clearCounts = SongDetailClearCounts(counts=clears) resp.songDetail.clearCountsSeason = SongDetailClearCounts(counts=clears) - + if int(ver_split[0]) >= 3: resp.songDetail.grades = SongDetailGradeCountsV2(counts=grades) else: resp.songDetail.grades = SongDetailGradeCountsV1(counts=grades) return resp.make() - - #TODO: Coop and vs data - def handle_user_music_updateCoop_request(self, data: Dict)-> Dict: + + # TODO: Coop and vs data + def handle_user_music_updateCoop_request(self, data: Dict) -> Dict: coop_info = data["params"][4] return self.handle_user_music_update_request(data) - def handle_user_music_updateVersus_request(self, data: Dict)-> Dict: + def handle_user_music_updateVersus_request(self, data: Dict) -> Dict: vs_info = data["params"][4] return self.handle_user_music_update_request(data) - - def handle_user_music_updateTrial_request(self, data: Dict)-> Dict: + + def handle_user_music_updateTrial_request(self, data: Dict) -> Dict: return self.handle_user_music_update_request(data) - def handle_user_mission_update_request(self, data: Dict)-> Dict: + def handle_user_mission_update_request(self, data: Dict) -> Dict: req = UserMissionUpdateRequest(data) page_status = req.params[1][1] profile = self.data.profile.get_profile(req.profileId) if profile is None: return BaseResponse().make() - + if len(req.itemsObtained) > 0: self.util_put_items(req.profileId, profile["user"], req.itemsObtained) - - self.data.profile.update_bingo(profile["user"], req.bingoDetail.pageNumber, page_status) + + self.data.profile.update_bingo( + profile["user"], req.bingoDetail.pageNumber, page_status + ) self.data.profile.update_tutorial_flags(req.profileId, req.params[3]) return BaseResponse().make() - def handle_user_goods_purchase_request(self, data: Dict)-> Dict: + def handle_user_goods_purchase_request(self, data: Dict) -> Dict: req = UserGoodsPurchaseRequest(data) resp = UserGoodsPurchaseResponse() profile = self.data.profile.get_profile(req.profileId) - if profile is None: return BaseResponse().make() + if profile is None: + return BaseResponse().make() user_id = profile["user"] resp.currentWp = profile["wp"] @@ -691,61 +883,81 @@ class WaccaBase(): if req.purchaseType == PurchaseType.PurchaseTypeWP: resp.currentWp -= req.cost self.data.profile.spend_wp(req.profileId, req.cost) - - elif req.purchaseType == PurchaseType.PurchaseTypeCredit: - self.logger.info(f"User {req.profileId} Purchased item {req.itemObtained.itemType} id {req.itemObtained.itemId} for {req.cost} credits on machine {req.chipId}") - self.util_put_items(req.profileId ,user_id, [req.itemObtained]) + elif req.purchaseType == PurchaseType.PurchaseTypeCredit: + self.logger.info( + f"User {req.profileId} Purchased item {req.itemObtained.itemType} id {req.itemObtained.itemId} for {req.cost} credits on machine {req.chipId}" + ) + + self.util_put_items(req.profileId, user_id, [req.itemObtained]) if self.game_config.mods.infinite_tickets: for x in range(5): resp.tickets.append(TicketItem(x, 106002, 0)) else: tickets = self.data.item.get_tickets(user_id) - + for ticket in tickets: - resp.tickets.append(TicketItem(ticket["id"], ticket["ticket_id"], int((self.srvtime + timedelta(days=30)).timestamp()))) - + resp.tickets.append( + TicketItem( + ticket["id"], + ticket["ticket_id"], + int((self.srvtime + timedelta(days=30)).timestamp()), + ) + ) + if self.game_config.mods.infinite_wp: resp.currentWp = 999999 return resp.make() - def handle_competition_status_login_request(self, data: Dict)-> Dict: + def handle_competition_status_login_request(self, data: Dict) -> Dict: return BaseResponse().make() - def handle_competition_status_update_request(self, data: Dict)-> Dict: + def handle_competition_status_update_request(self, data: Dict) -> Dict: return BaseResponse().make() - 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 = self.data.profile.profile_to_aime_user(req.profileId) if user_id is None: - self.logger.error(f"handle_user_rating_update_request: No profild with ID {req.profileId}") + self.logger.error( + f"handle_user_rating_update_request: No profild with ID {req.profileId}" + ) return BaseResponse().make() for song in req.songs: - self.data.score.update_song_rating(user_id, song.songId, song.difficulty, song.rating) - + self.data.score.update_song_rating( + user_id, song.songId, song.difficulty, song.rating + ) + self.data.profile.update_user_rating(req.profileId, req.totalRating) return BaseResponse().make() - - def handle_user_status_update_request(self, data: Dict)-> Dict: + + def handle_user_status_update_request(self, data: Dict) -> Dict: req = UserStatusUpdateRequestV1(data) user_id = self.data.profile.profile_to_aime_user(req.profileId) if user_id is None: - self.logger.info(f"handle_user_status_update_request: No profile with ID {req.profileId}") + self.logger.info( + f"handle_user_status_update_request: No profile with ID {req.profileId}" + ) return BaseResponse().make() - + self.util_put_items(req.profileId, user_id, req.itemsRecieved) - self.data.profile.update_profile_playtype(req.profileId, req.playType.value, data["appVersion"][:7]) - - current_icon = self.data.profile.get_options(user_id, WaccaConstants.OPTIONS["set_icon_id"]) - current_nav = self.data.profile.get_options(user_id, WaccaConstants.OPTIONS["set_nav_id"]) + self.data.profile.update_profile_playtype( + req.profileId, req.playType.value, data["appVersion"][:7] + ) + + current_icon = self.data.profile.get_options( + user_id, WaccaConstants.OPTIONS["set_icon_id"] + ) + current_nav = self.data.profile.get_options( + user_id, WaccaConstants.OPTIONS["set_nav_id"] + ) if current_icon is None: current_icon = self.OPTIONS_DEFAULTS["set_icon_id"] @@ -755,12 +967,16 @@ class WaccaBase(): current_nav = self.OPTIONS_DEFAULTS["set_nav_id"] else: current_nav = current_nav["value"] - - self.data.item.put_item(user_id, WaccaConstants.ITEM_TYPES["icon"], current_icon) - self.data.item.put_item(user_id, WaccaConstants.ITEM_TYPES["navigator"], current_nav) + + self.data.item.put_item( + user_id, WaccaConstants.ITEM_TYPES["icon"], current_icon + ) + self.data.item.put_item( + user_id, WaccaConstants.ITEM_TYPES["navigator"], current_nav + ) return BaseResponse().make() - def handle_user_info_update_request(self, data: Dict)-> Dict: + def handle_user_info_update_request(self, data: Dict) -> Dict: req = UserInfoUpdateRequest(data) user_id = self.data.profile.profile_to_aime_user(req.profileId) @@ -769,7 +985,7 @@ class WaccaBase(): self.data.profile.update_option(user_id, opt.opt_id, opt.opt_val) for update in req.datesUpdated: - pass + pass for fav in req.favoritesAdded: self.data.profile.add_favorite_song(user_id, fav) @@ -778,64 +994,96 @@ class WaccaBase(): self.data.profile.remove_favorite_song(user_id, unfav) return BaseResponse().make() - - 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 = self.data.profile.get_profile(req.profileId) if profile is None: - self.logger.warn(f"handle_user_vip_get_request no profile with ID {req.profileId}") + self.logger.warn( + f"handle_user_vip_get_request no profile with ID {req.profileId}" + ) return BaseResponse().make() - - if "vip_expire_time" in profile and profile["vip_expire_time"] is not None and profile["vip_expire_time"].timestamp() > int(self.srvtime.timestamp()): - resp.vipDays = int((profile["vip_expire_time"].timestamp() - int(self.srvtime.timestamp())) / 86400) - + + if ( + "vip_expire_time" in profile + and profile["vip_expire_time"] is not None + and profile["vip_expire_time"].timestamp() > int(self.srvtime.timestamp()) + ): + resp.vipDays = int( + (profile["vip_expire_time"].timestamp() - int(self.srvtime.timestamp())) + / 86400 + ) + resp.vipDays += 30 - - resp.presents.append(VipLoginBonus(1,0,16,211025,1)) - resp.presents.append(VipLoginBonus(2,0,6,202086,1)) - resp.presents.append(VipLoginBonus(3,0,11,205008,1)) - resp.presents.append(VipLoginBonus(4,0,10,203009,1)) - resp.presents.append(VipLoginBonus(5,0,16,211026,1)) - resp.presents.append(VipLoginBonus(6,0,9,206001,1)) - + + resp.presents.append(VipLoginBonus(1, 0, 16, 211025, 1)) + resp.presents.append(VipLoginBonus(2, 0, 6, 202086, 1)) + resp.presents.append(VipLoginBonus(3, 0, 11, 205008, 1)) + resp.presents.append(VipLoginBonus(4, 0, 10, 203009, 1)) + resp.presents.append(VipLoginBonus(5, 0, 16, 211026, 1)) + resp.presents.append(VipLoginBonus(6, 0, 9, 206001, 1)) + return resp.make() - - def handle_user_vip_start_request(self, data: Dict)-> Dict: + + def handle_user_vip_start_request(self, data: Dict) -> Dict: req = UserVipStartRequest(data) profile = self.data.profile.get_profile(req.profileId) - if profile is None: return BaseResponse().make() + if profile is None: + return BaseResponse().make() # This should never happen because wacca stops you from buying VIP # if you have more then 10 days remaining, but this IS wacca we're dealing with... - if "always_vip" in profile and profile["always_vip"] or self.game_config.mods.always_vip: - return UserVipStartResponse(int((self.srvtime + timedelta(days=req.days)).timestamp())).make() + if ( + "always_vip" in profile + and profile["always_vip"] + or self.game_config.mods.always_vip + ): + return UserVipStartResponse( + int((self.srvtime + timedelta(days=req.days)).timestamp()) + ).make() - vip_exp_time = (self.srvtime + timedelta(days=req.days)) + vip_exp_time = self.srvtime + timedelta(days=req.days) self.data.profile.update_vip_time(req.profileId, vip_exp_time) return UserVipStartResponse(int(vip_exp_time.timestamp())).make() - def util_put_items(self, profile_id: int, user_id: int, items_obtained: List[GenericItemRecv]) -> None: + def util_put_items( + self, profile_id: int, user_id: int, items_obtained: List[GenericItemRecv] + ) -> None: if user_id is None or profile_id <= 0: return None if items_obtained: for item in items_obtained: - if item.itemType == WaccaConstants.ITEM_TYPES["xp"]: self.data.profile.add_xp(profile_id, item.quantity) elif item.itemType == WaccaConstants.ITEM_TYPES["wp"]: self.data.profile.add_wp(profile_id, item.quantity) - - elif item.itemType == WaccaConstants.ITEM_TYPES["music_difficulty_unlock"] or item.itemType == WaccaConstants.ITEM_TYPES["music_unlock"]: + + elif ( + item.itemType + == WaccaConstants.ITEM_TYPES["music_difficulty_unlock"] + or item.itemType == WaccaConstants.ITEM_TYPES["music_unlock"] + ): if item.quantity > WaccaConstants.Difficulty.HARD.value: - old_score = self.data.score.get_best_score(user_id, item.itemId, item.quantity) + old_score = self.data.score.get_best_score( + user_id, item.itemId, item.quantity + ) if not old_score: - self.data.score.put_best_score(user_id, item.itemId, item.quantity, 0, [0] * 5, [0] * 13, 0, 0) - + self.data.score.put_best_score( + user_id, + item.itemId, + item.quantity, + 0, + [0] * 5, + [0] * 13, + 0, + 0, + ) + if item.quantity == 0: item.quantity = WaccaConstants.Difficulty.HARD.value self.data.item.unlock_song(user_id, item.itemId, item.quantity) @@ -844,7 +1092,9 @@ class WaccaBase(): self.data.item.add_ticket(user_id, item.itemId) elif item.itemType == WaccaConstants.ITEM_TYPES["trophy"]: - self.data.item.update_trophy(user_id, item.itemId, self.season, item.quantity, 0) + self.data.item.update_trophy( + user_id, item.itemId, self.season, item.quantity, 0 + ) else: self.data.item.put_item(user_id, item.itemType, item.itemId) @@ -870,6 +1120,7 @@ class WaccaBase(): const = 2.00 elif score >= 900000 and score < 910000: const = 1.00 - else: const = 0.00 + else: + const = 0.00 return floor((difficulty * const) * 10) diff --git a/titles/wacca/config.py b/titles/wacca/config.py index fc03dd8..e96c3f4 100644 --- a/titles/wacca/config.py +++ b/titles/wacca/config.py @@ -1,45 +1,65 @@ from typing import Dict, List from core.config import CoreConfig -class WaccaServerConfig(): + +class WaccaServerConfig: def __init__(self, parent_config: "WaccaConfig") -> None: self.__config = parent_config - + @property def enable(self) -> bool: - return CoreConfig.get_config_field(self.__config, 'wacca', 'server', 'enable', default=True) - + return CoreConfig.get_config_field( + self.__config, "wacca", "server", "enable", default=True + ) + @property def loglevel(self) -> int: - return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'wacca', 'server', 'loglevel', default="info")) + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "wacca", "server", "loglevel", default="info" + ) + ) @property def prefecture_name(self) -> str: - return CoreConfig.get_config_field(self.__config, 'wacca', 'server', 'prefecture_name', default="Hokkaido") + return CoreConfig.get_config_field( + self.__config, "wacca", "server", "prefecture_name", default="Hokkaido" + ) -class WaccaModsConfig(): + +class WaccaModsConfig: def __init__(self, parent_config: "WaccaConfig") -> None: self.__config = parent_config - + @property def always_vip(self) -> bool: - return CoreConfig.get_config_field(self.__config, 'wacca', 'mods', 'always_vip', default=True) - + return CoreConfig.get_config_field( + self.__config, "wacca", "mods", "always_vip", default=True + ) + @property def infinite_tickets(self) -> bool: - return CoreConfig.get_config_field(self.__config, 'wacca', 'mods', 'infinite_tickets', default=True) + return CoreConfig.get_config_field( + self.__config, "wacca", "mods", "infinite_tickets", default=True + ) @property def infinite_wp(self) -> bool: - return CoreConfig.get_config_field(self.__config, 'wacca', 'mods', 'infinite_wp', default=True) + return CoreConfig.get_config_field( + self.__config, "wacca", "mods", "infinite_wp", default=True + ) -class WaccaGateConfig(): + +class WaccaGateConfig: def __init__(self, parent_config: "WaccaConfig") -> None: self.__config = parent_config @property def enabled_gates(self) -> List[int]: - return CoreConfig.get_config_field(self.__config, 'wacca', 'gates', 'enabled_gates', default=[]) + return CoreConfig.get_config_field( + self.__config, "wacca", "gates", "enabled_gates", default=[] + ) + class WaccaConfig(dict): def __init__(self) -> None: diff --git a/titles/wacca/const.py b/titles/wacca/const.py index f072143..284d236 100644 --- a/titles/wacca/const.py +++ b/titles/wacca/const.py @@ -3,7 +3,8 @@ from typing import Optional from core.const import AllnetJapanRegionId -class WaccaConstants(): + +class WaccaConstants: CONFIG_NAME = "wacca.yaml" GAME_CODE = "SDFE" @@ -51,51 +52,48 @@ class WaccaConstants(): } OPTIONS = { - "note_speed": 1, # 1.0 - 6.0 - "field_mask": 2, # 0-4 - "note_sound": 3, # ID - "note_color": 4, # ID - "bgm_volume": 5, # 0-100 incremements of 10 - "bg_video": 7, # ask, on, or off - - "mirror": 101, # none or left+right swap - "judge_display_pos": 102, # center, under, over, top or off - "judge_detail_display": 103, # on or off - "measure_guidelines": 105, # on or off - "guideline_mask": 106, # 0 - 5 - "judge_line_timing_adjust": 108, # -10 - 10 - "note_design": 110, # 1 - 5 - "bonus_effect": 114, # on or off - "chara_voice": 115, # "usually" or none - "score_display_method": 116, # add or subtract - "give_up": 117, # off, no touch, can't achieve s, ss, sss, pb - "guideline_spacing": 118, # none, or a-g type - "center_display": 119, # none, combo, score add, score sub, s ss sss pb boarder - "ranking_display": 120, # on or off - "stage_up_icon_display": 121, # on or off - "rating_display": 122, # on or off - "player_level_display": 123, # on or off - "touch_effect": 124, # on or off - "guide_sound_vol": 125, # 0-100 incremements of 10 - "touch_note_vol": 126, # 0-100 incremements of 10 - "hold_note_vol": 127, # 0-100 incremements of 10 - "slide_note_vol": 128, # 0-100 incremements of 10 - "snap_note_vol": 129, # 0-100 incremements of 10 - "chain_note_vol": 130, # 0-100 incremements of 10 - "bonus_note_vol": 131, # 0-100 incremements of 10 - "gate_skip": 132, # on or off - "key_beam_display": 133, # on or off - - "left_slide_note_color": 201, # red blue green or orange - "right_slide_note_color": 202, # red blue green or orange - "forward_slide_note_color": 203, # red blue green or orange - "back_slide_note_color": 204, # red blue green or orange - - "master_vol": 1001, # 0-100 incremements of 10 - "set_title_id": 1002, # ID - "set_icon_id": 1003, # ID - "set_nav_id": 1004, # ID - "set_plate_id": 1005, # ID + "note_speed": 1, # 1.0 - 6.0 + "field_mask": 2, # 0-4 + "note_sound": 3, # ID + "note_color": 4, # ID + "bgm_volume": 5, # 0-100 incremements of 10 + "bg_video": 7, # ask, on, or off + "mirror": 101, # none or left+right swap + "judge_display_pos": 102, # center, under, over, top or off + "judge_detail_display": 103, # on or off + "measure_guidelines": 105, # on or off + "guideline_mask": 106, # 0 - 5 + "judge_line_timing_adjust": 108, # -10 - 10 + "note_design": 110, # 1 - 5 + "bonus_effect": 114, # on or off + "chara_voice": 115, # "usually" or none + "score_display_method": 116, # add or subtract + "give_up": 117, # off, no touch, can't achieve s, ss, sss, pb + "guideline_spacing": 118, # none, or a-g type + "center_display": 119, # none, combo, score add, score sub, s ss sss pb boarder + "ranking_display": 120, # on or off + "stage_up_icon_display": 121, # on or off + "rating_display": 122, # on or off + "player_level_display": 123, # on or off + "touch_effect": 124, # on or off + "guide_sound_vol": 125, # 0-100 incremements of 10 + "touch_note_vol": 126, # 0-100 incremements of 10 + "hold_note_vol": 127, # 0-100 incremements of 10 + "slide_note_vol": 128, # 0-100 incremements of 10 + "snap_note_vol": 129, # 0-100 incremements of 10 + "chain_note_vol": 130, # 0-100 incremements of 10 + "bonus_note_vol": 131, # 0-100 incremements of 10 + "gate_skip": 132, # on or off + "key_beam_display": 133, # on or off + "left_slide_note_color": 201, # red blue green or orange + "right_slide_note_color": 202, # red blue green or orange + "forward_slide_note_color": 203, # red blue green or orange + "back_slide_note_color": 204, # red blue green or orange + "master_vol": 1001, # 0-100 incremements of 10 + "set_title_id": 1002, # ID + "set_icon_id": 1003, # ID + "set_nav_id": 1004, # ID + "set_plate_id": 1005, # ID } class Difficulty(Enum): @@ -103,7 +101,7 @@ class WaccaConstants(): HARD = 2 EXPERT = 3 INFERNO = 4 - + class Region(Enum): NONE = 0 HOKKAIDO = 1 @@ -163,7 +161,7 @@ class WaccaConstants(): SGP = 51 KOREA = 52 KOR = 52 - + VALID_COUNTRIES = set(["JPN", "USA", "KOR", "HKG", "SGP"]) @classmethod @@ -174,16 +172,54 @@ class WaccaConstants(): def allnet_region_id_to_wacca_region(cls, region: int) -> Optional[Region]: try: return [ - cls.Region.NONE, cls.Region.AICHI, cls.Region.AOMORI, cls.Region.AKITA, cls.Region.ISHIKAWA, - cls.Region.IBARAKI, cls.Region.IWATE, cls.Region.EHIME, cls.Region.OITA, cls.Region.OSAKA, - cls.Region.OKAYAMA, cls.Region.OKINAWA, cls.Region.KAGAWA, cls.Region.KAGOSHIMA, cls.Region.KANAGAWA, - cls.Region.GIFU, cls.Region.KYOTO, cls.Region.KUMAMOTO, cls.Region.GUNMA, cls.Region.KOCHI, - cls.Region.SAITAMA, cls.Region.SAGA, cls.Region.SHIGA, cls.Region.SHIZUOKA, cls.Region.SHIMANE, - cls.Region.CHIBA, cls.Region.TOKYO, cls.Region.TOKUSHIMA, cls.Region.TOCHIGI, cls.Region.TOTTORI, - cls.Region.TOYAMA, cls.Region.NAGASAKI, cls.Region.NAGANO, cls.Region.NARA, cls.Region.NIIGATA, - cls.Region.HYOGO, cls.Region.HIROSHIMA, cls.Region.FUKUI, cls.Region.FUKUOKA, cls.Region.FUKUSHIMA, - cls.Region.HOKKAIDO, cls.Region.MIE, cls.Region.MIYAGI, cls.Region.MIYAZAKI, cls.Region.YAMAGATA, - cls.Region.YAMAGUCHI, cls.Region.YAMANASHI, cls.Region.WAKAYAMA, + cls.Region.NONE, + cls.Region.AICHI, + cls.Region.AOMORI, + cls.Region.AKITA, + cls.Region.ISHIKAWA, + cls.Region.IBARAKI, + cls.Region.IWATE, + cls.Region.EHIME, + cls.Region.OITA, + cls.Region.OSAKA, + cls.Region.OKAYAMA, + cls.Region.OKINAWA, + cls.Region.KAGAWA, + cls.Region.KAGOSHIMA, + cls.Region.KANAGAWA, + cls.Region.GIFU, + cls.Region.KYOTO, + cls.Region.KUMAMOTO, + cls.Region.GUNMA, + cls.Region.KOCHI, + cls.Region.SAITAMA, + cls.Region.SAGA, + cls.Region.SHIGA, + cls.Region.SHIZUOKA, + cls.Region.SHIMANE, + cls.Region.CHIBA, + cls.Region.TOKYO, + cls.Region.TOKUSHIMA, + cls.Region.TOCHIGI, + cls.Region.TOTTORI, + cls.Region.TOYAMA, + cls.Region.NAGASAKI, + cls.Region.NAGANO, + cls.Region.NARA, + cls.Region.NIIGATA, + cls.Region.HYOGO, + cls.Region.HIROSHIMA, + cls.Region.FUKUI, + cls.Region.FUKUOKA, + cls.Region.FUKUSHIMA, + cls.Region.HOKKAIDO, + cls.Region.MIE, + cls.Region.MIYAGI, + cls.Region.MIYAZAKI, + cls.Region.YAMAGATA, + cls.Region.YAMAGUCHI, + cls.Region.YAMANASHI, + cls.Region.WAKAYAMA, ][region] - except: return None - + except: + return None diff --git a/titles/wacca/database.py b/titles/wacca/database.py index 8d4c8a5..133e22f 100644 --- a/titles/wacca/database.py +++ b/titles/wacca/database.py @@ -2,6 +2,7 @@ from core.data import Data from core.config import CoreConfig from titles.wacca.schema import * + class WaccaData(Data): def __init__(self, cfg: CoreConfig) -> None: super().__init__(cfg) @@ -9,4 +10,4 @@ class WaccaData(Data): self.profile = WaccaProfileData(self.config, self.session) self.score = WaccaScoreData(self.config, self.session) self.item = WaccaItemData(self.config, self.session) - self.static = WaccaStaticData(self.config, self.session) \ No newline at end of file + self.static = WaccaStaticData(self.config, self.session) diff --git a/titles/wacca/frontend.py b/titles/wacca/frontend.py index b7232b8..e4f2be0 100644 --- a/titles/wacca/frontend.py +++ b/titles/wacca/frontend.py @@ -8,17 +8,22 @@ from titles.wacca.database import WaccaData from titles.wacca.config import WaccaConfig from titles.wacca.const import WaccaConstants + class WaccaFrontend(FE_Base): - def __init__(self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str) -> None: + def __init__( + self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str + ) -> None: super().__init__(cfg, environment) self.data = WaccaData(cfg) self.game_cfg = WaccaConfig() self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/wacca.yaml"))) self.nav_name = "Wacca" - + def render_GET(self, request: Request) -> bytes: - template = self.environment.get_template("titles/wacca/frontend/wacca_index.jinja") + template = self.environment.get_template( + "titles/wacca/frontend/wacca_index.jinja" + ) return template.render( title=f"{self.core_config.server.name} | {self.nav_name}", - game_list=self.environment.globals["game_list"] + game_list=self.environment.globals["game_list"], ).encode("utf-16") diff --git a/titles/wacca/handlers/__init__.py b/titles/wacca/handlers/__init__.py index a59c7c1..f084682 100644 --- a/titles/wacca/handlers/__init__.py +++ b/titles/wacca/handlers/__init__.py @@ -6,4 +6,4 @@ from titles.wacca.handlers.user_misc import * from titles.wacca.handlers.user_music import * from titles.wacca.handlers.user_status import * from titles.wacca.handlers.user_trial import * -from titles.wacca.handlers.user_vip import * \ No newline at end of file +from titles.wacca.handlers.user_vip import * diff --git a/titles/wacca/handlers/advertise.py b/titles/wacca/handlers/advertise.py index a0d8d90..56186eb 100644 --- a/titles/wacca/handlers/advertise.py +++ b/titles/wacca/handlers/advertise.py @@ -3,6 +3,7 @@ from typing import List, Dict from titles.wacca.handlers.base import BaseResponse, BaseRequest from titles.wacca.handlers.helpers import Notice + # ---advertise/GetNews--- class GetNewsResponseV1(BaseResponse): def __init__(self) -> None: @@ -19,27 +20,29 @@ class GetNewsResponseV1(BaseResponse): for notice in self.notices: note.append(notice.make()) - - self.params = [ - note, - self.copywrightListings, - self.stoppedSongs, - self.stoppedJackets, - self.stoppedMovies, - self.stoppedIcons + + self.params = [ + note, + self.copywrightListings, + self.stoppedSongs, + self.stoppedJackets, + self.stoppedMovies, + self.stoppedIcons, ] return super().make() -class GetNewsResponseV2(GetNewsResponseV1): + +class GetNewsResponseV2(GetNewsResponseV1): stoppedProducts: list[int] = [] def make(self) -> Dict: super().make() self.params.append(self.stoppedProducts) - + return super(GetNewsResponseV1, self).make() + class GetNewsResponseV3(GetNewsResponseV2): stoppedNavs: list[int] = [] stoppedNavVoices: list[int] = [] @@ -48,18 +51,20 @@ class GetNewsResponseV3(GetNewsResponseV2): super().make() self.params.append(self.stoppedNavs) self.params.append(self.stoppedNavVoices) - + return super(GetNewsResponseV1, self).make() + # ---advertise/GetRanking--- class AdvertiseGetRankingRequest(BaseRequest): def __init__(self, data: Dict) -> None: super().__init__(data) self.resourceVer: int = self.params[0] + class AdvertiseGetRankingResponse(BaseResponse): def __init__(self) -> None: super().__init__() - + def make(self) -> Dict: - return super().make() \ No newline at end of file + return super().make() diff --git a/titles/wacca/handlers/base.py b/titles/wacca/handlers/base.py index d7a2fb2..abfed5f 100644 --- a/titles/wacca/handlers/base.py +++ b/titles/wacca/handlers/base.py @@ -2,7 +2,8 @@ from typing import Dict, List from titles.wacca.handlers.helpers import Version from datetime import datetime -class BaseRequest(): + +class BaseRequest: def __init__(self, data: Dict) -> None: self.requestNo: int = data["requestNo"] self.appVersion: Version = Version(data["appVersion"]) @@ -10,7 +11,8 @@ class BaseRequest(): self.chipId: str = data["chipId"] self.params: List = data["params"] -class BaseResponse(): + +class BaseResponse: def __init__(self) -> None: self.status: int = 0 self.message: str = "" @@ -28,5 +30,5 @@ class BaseResponse(): "maintNoticeTime": self.maintNoticeTime, "maintNotPlayableTime": self.maintNotPlayableTime, "maintStartTime": self.maintStartTime, - "params": self.params + "params": self.params, } diff --git a/titles/wacca/handlers/helpers.py b/titles/wacca/handlers/helpers.py index b96b3dd..9e9847a 100644 --- a/titles/wacca/handlers/helpers.py +++ b/titles/wacca/handlers/helpers.py @@ -3,28 +3,33 @@ from enum import Enum from titles.wacca.const import WaccaConstants + class ShortVersion: - def __init__(self, version: str = "", major = 1, minor = 0, patch = 0) -> None: + def __init__(self, version: str = "", major=1, minor=0, patch=0) -> None: split = version.split(".") if len(split) >= 3: self.major = int(split[0]) self.minor = int(split[1]) self.patch = int(split[2]) - - else: + + else: self.major = major self.minor = minor self.patch = patch - + def __str__(self) -> str: return f"{self.major}.{self.minor}.{self.patch}" - + def __int__(self) -> int: return (self.major * 10000) + (self.minor * 100) + self.patch - + def __eq__(self, other: "ShortVersion"): - return self.major == other.major and self.minor == other.minor and self.patch == other.patch - + return ( + self.major == other.major + and self.minor == other.minor + and self.patch == other.patch + ) + def __gt__(self, other: "ShortVersion"): if self.major > other.major: return True @@ -34,9 +39,9 @@ class ShortVersion: elif self.minor == other.minor: if self.patch > other.patch: return True - + return False - + def __ge__(self, other: "ShortVersion"): if self.major > other.major: return True @@ -46,9 +51,9 @@ class ShortVersion: elif self.minor == other.minor: if self.patch > other.patch or self.patch == other.patch: return True - + return False - + def __lt__(self, other: "ShortVersion"): if self.major < other.major: return True @@ -58,9 +63,9 @@ class ShortVersion: elif self.minor == other.minor: if self.patch < other.patch: return True - + return False - + def __le__(self, other: "ShortVersion"): if self.major < other.major: return True @@ -70,39 +75,45 @@ class ShortVersion: elif self.minor == other.minor: if self.patch < other.patch or self.patch == other.patch: return True - + return False + class Version(ShortVersion): - def __init__(self, version = "", major = 1, minor = 0, patch = 0, country = "JPN", build = 0, role = "C") -> None: + def __init__( + self, version="", major=1, minor=0, patch=0, country="JPN", build=0, role="C" + ) -> None: super().__init__(version, major, minor, patch) split = version.split(".") if len(split) >= 6: - self.country: str = split[3] + self.country: str = split[3] self.build = int(split[4]) self.role: str = split[5] - + else: self.country = country self.build = build self.role = role - + def __str__(self) -> str: return f"{self.major}.{self.minor}.{self.patch}.{self.country}.{self.role}.{self.build}" + class HousingInfo: """ 1 is lan install role, 2 is country """ + id: int = 0 val: str = "" def __init__(self, id: int = 0, val: str = "") -> None: self.id = id self.val = val - + def make(self) -> List: - return [ self.id, self.val ] + return [self.id, self.val] + class Notice: name: str = "" @@ -116,25 +127,44 @@ class Notice: endTime: int = 0 voiceline: int = 0 - def __init__(self, name: str = "", title: str = "", message: str = "", start: int = 0, end: int = 0) -> None: + def __init__( + self, + name: str = "", + title: str = "", + message: str = "", + start: int = 0, + end: int = 0, + ) -> None: self.name = name self.title = title self.message = message self.startTime = start self.endTime = end - + def make(self) -> List: - return [ self.name, self.title, self.message, self.unknown3, self.unknown4, int(self.showTitleScreen), - int(self.showWelcomeScreen), self.startTime, self.endTime, self.voiceline] + return [ + self.name, + self.title, + self.message, + self.unknown3, + self.unknown4, + int(self.showTitleScreen), + int(self.showWelcomeScreen), + self.startTime, + self.endTime, + self.voiceline, + ] + class UserOption: def __init__(self, opt_id: int = 0, opt_val: Any = 0) -> None: self.opt_id = opt_id self.opt_val = opt_val - + def make(self) -> List: return [self.opt_id, self.opt_val] + class UserStatusV1: def __init__(self) -> None: self.userId: int = 0 @@ -160,19 +190,20 @@ class UserStatusV1: self.useCount, ] + class UserStatusV2(UserStatusV1): def __init__(self) -> None: - super().__init__() + super().__init__() self.loginDays: int = 0 self.loginConsecutive: int = 0 self.loginConsecutiveDays: int = 0 self.loginsToday: int = 0 - self.rating: int = 0 + self.rating: int = 0 self.vipExpireTime: int = 0 def make(self) -> List: ret = super().make() - + ret.append(self.loginDays) ret.append(self.loginConsecutive) ret.append(self.loginConsecutiveDays) @@ -182,17 +213,20 @@ class UserStatusV2(UserStatusV1): return ret + class ProfileStatus(Enum): ProfileGood = 0 ProfileRegister = 1 ProfileInUse = 2 ProfileWrongRegion = 3 + class PlayVersionStatus(Enum): VersionGood = 0 VersionTooNew = 1 VersionUpgrade = 2 + class PlayModeCounts: seasonId: int = 0 modeId: int = 0 @@ -202,13 +236,10 @@ class PlayModeCounts: self.seasonId = seasonId self.modeId = modeId self.playNum = playNum - + def make(self) -> List: - return [ - self.seasonId, - self.modeId, - self.playNum - ] + return [self.seasonId, self.modeId, self.playNum] + class SongUnlock: songId: int = 0 @@ -216,76 +247,72 @@ class SongUnlock: whenAppeared: int = 0 whenUnlocked: int = 0 - def __init__(self, song_id: int = 0, difficulty: int = 1, whenAppered: int = 0, whenUnlocked: int = 0) -> None: + def __init__( + self, + song_id: int = 0, + difficulty: int = 1, + whenAppered: int = 0, + whenUnlocked: int = 0, + ) -> None: self.songId = song_id self.difficulty = difficulty self.whenAppeared = whenAppered self.whenUnlocked = whenUnlocked def make(self) -> List: - return [ - self.songId, - self.difficulty, - self.whenAppeared, - self.whenUnlocked - ] + return [self.songId, self.difficulty, self.whenAppeared, self.whenUnlocked] + class GenericItemRecv: def __init__(self, item_type: int = 1, item_id: int = 1, quantity: int = 1) -> None: self.itemId = item_id self.itemType = item_type self.quantity = quantity - + def make(self) -> List: - return [ self.itemType, self.itemId, self.quantity ] + return [self.itemType, self.itemId, self.quantity] + class GenericItemSend: def __init__(self, itemId: int, itemType: int, whenAcquired: int) -> None: self.itemId = itemId self.itemType = itemType self.whenAcquired = whenAcquired - + def make(self) -> List: - return [ - self.itemId, - self.itemType, - self.whenAcquired - ] + return [self.itemId, self.itemType, self.whenAcquired] + class IconItem(GenericItemSend): uses: int = 0 - def __init__(self, itemId: int, itemType: int, uses: int, whenAcquired: int) -> None: + def __init__( + self, itemId: int, itemType: int, uses: int, whenAcquired: int + ) -> None: super().__init__(itemId, itemType, whenAcquired) self.uses = uses - + def make(self) -> List: - return [ - self.itemId, - self.itemType, - self.uses, - self.whenAcquired - ] + return [self.itemId, self.itemType, self.uses, self.whenAcquired] + class TrophyItem: - trophyId: int = 0 + trophyId: int = 0 season: int = 1 progress: int = 0 badgeType: int = 0 - def __init__(self, trophyId: int, season: int, progress: int, badgeType: int) -> None: + def __init__( + self, trophyId: int, season: int, progress: int, badgeType: int + ) -> None: self.trophyId = trophyId - self.season = season + self.season = season self.progress = progress self.badgeType = badgeType - + def make(self) -> List: - return [ - self.trophyId, - self.season, - self.progress, - self.badgeType - ] + return [self.trophyId, self.season, self.progress, self.badgeType] + class TicketItem: userTicketId: int = 0 @@ -296,18 +323,17 @@ class TicketItem: self.userTicketId = userTicketId self.ticketId = ticketId self.whenExpires = whenExpires - + def make(self) -> List: - return [ - self.userTicketId, - self.ticketId, - self.whenExpires - ] + return [self.userTicketId, self.ticketId, self.whenExpires] + class NavigatorItem(IconItem): usesToday: int = 0 - def __init__(self, itemId: int, itemType: int, whenAcquired: int, uses: int, usesToday: int) -> None: + def __init__( + self, itemId: int, itemType: int, whenAcquired: int, uses: int, usesToday: int + ) -> None: super().__init__(itemId, itemType, uses, whenAcquired) self.usesToday = usesToday @@ -317,9 +343,10 @@ class NavigatorItem(IconItem): self.itemType, self.whenAcquired, self.uses, - self.usesToday + self.usesToday, ] + class SkillItem: skill_type: int level: int @@ -327,12 +354,8 @@ class SkillItem: badge: int def make(self) -> List: - return [ - self.skill_type, - self.level, - self.flag, - self.badge - ] + return [self.skill_type, self.level, self.flag, self.badge] + class UserItemInfoV1: def __init__(self) -> None: @@ -383,6 +406,7 @@ class UserItemInfoV1: sounds, ] + class UserItemInfoV2(UserItemInfoV1): def __init__(self) -> None: super().__init__() @@ -391,18 +415,19 @@ class UserItemInfoV2(UserItemInfoV1): def make(self) -> List: ret = super().make() - plates = [] + plates = [] navs = [] - + for x in self.navigators: navs.append(x.make()) for x in self.plates: plates.append(x.make()) - + ret.append(navs) ret.append(plates) return ret + class UserItemInfoV3(UserItemInfoV2): def __init__(self) -> None: super().__init__() @@ -414,29 +439,44 @@ class UserItemInfoV3(UserItemInfoV2): for x in self.touchEffect: effect.append(x.make()) - + ret.append(effect) return ret -class SongDetailClearCounts: - def __init__(self, play_ct: int = 0, clear_ct: int = 0, ml_ct: int = 0, fc_ct: int = 0, - am_ct: int = 0, counts: Optional[List[int]] = None) -> None: + +class SongDetailClearCounts: + def __init__( + self, + play_ct: int = 0, + clear_ct: int = 0, + ml_ct: int = 0, + fc_ct: int = 0, + am_ct: int = 0, + counts: Optional[List[int]] = None, + ) -> None: if counts is None: self.playCt = play_ct self.clearCt = clear_ct self.misslessCt = ml_ct self.fullComboCt = fc_ct self.allMarvelousCt = am_ct - + else: self.playCt = counts[0] self.clearCt = counts[1] self.misslessCt = counts[2] self.fullComboCt = counts[3] self.allMarvelousCt = counts[4] - + def make(self) -> List: - return [self.playCt, self.clearCt, self.misslessCt, self.fullComboCt, self.allMarvelousCt] + return [ + self.playCt, + self.clearCt, + self.misslessCt, + self.fullComboCt, + self.allMarvelousCt, + ] + class SongDetailGradeCountsV1: dCt: int @@ -450,8 +490,20 @@ class SongDetailGradeCountsV1: sssCt: int masterCt: int - def __init__(self, d: int = 0, c: int = 0, b: int = 0, a: int = 0, aa: int = 0, aaa: int = 0, s: int = 0, - ss: int = 0, sss: int = 0, master: int = 0, counts: Optional[List[int]] = None) -> None: + def __init__( + self, + d: int = 0, + c: int = 0, + b: int = 0, + a: int = 0, + aa: int = 0, + aaa: int = 0, + s: int = 0, + ss: int = 0, + sss: int = 0, + master: int = 0, + counts: Optional[List[int]] = None, + ) -> None: if counts is None: self.dCt = d self.cCt = c @@ -463,7 +515,7 @@ class SongDetailGradeCountsV1: self.ssCt = ss self.sssCt = sss self.masterCt = master - + else: self.dCt = counts[0] self.cCt = counts[1] @@ -474,24 +526,51 @@ class SongDetailGradeCountsV1: self.sCt = counts[6] self.ssCt = counts[7] self.sssCt = counts[8] - self.masterCt =counts[9] + self.masterCt = counts[9] def make(self) -> List: - return [self.dCt, self.cCt, self.bCt, self.aCt, self.aaCt, self.aaaCt, self.sCt, self.ssCt, self.sssCt, self.masterCt] + return [ + self.dCt, + self.cCt, + self.bCt, + self.aCt, + self.aaCt, + self.aaaCt, + self.sCt, + self.ssCt, + self.sssCt, + self.masterCt, + ] + class SongDetailGradeCountsV2(SongDetailGradeCountsV1): spCt: int sspCt: int ssspCt: int - def __init__(self, d: int = 0, c: int = 0, b: int = 0, a: int = 0, aa: int = 0, aaa: int = 0, s: int = 0, - ss: int = 0, sss: int = 0, master: int = 0, sp: int = 0, ssp: int = 0, sssp: int = 0, counts: Optional[List[int]] = None) -> None: + def __init__( + self, + d: int = 0, + c: int = 0, + b: int = 0, + a: int = 0, + aa: int = 0, + aaa: int = 0, + s: int = 0, + ss: int = 0, + sss: int = 0, + master: int = 0, + sp: int = 0, + ssp: int = 0, + sssp: int = 0, + counts: Optional[List[int]] = None, + ) -> None: super().__init__(d, c, b, a, aa, aaa, s, ss, sss, master, counts) if counts is None: self.spCt = sp self.sspCt = ssp self.ssspCt = sssp - + else: self.spCt = counts[10] self.sspCt = counts[11] @@ -500,6 +579,7 @@ class SongDetailGradeCountsV2(SongDetailGradeCountsV1): def make(self) -> List: return super().make() + [self.spCt, self.sspCt, self.ssspCt] + class BestScoreDetailV1: songId: int = 0 difficulty: int = 1 @@ -527,49 +607,59 @@ class BestScoreDetailV1: self.bestCombo, self.lowestMissCtMaybe, self.isUnlock, - self.rating + self.rating, ] + class BestScoreDetailV2(BestScoreDetailV1): gradeCounts: SongDetailGradeCountsV2 = SongDetailGradeCountsV2() + class SongUpdateJudgementCounts: marvCt: int greatCt: int goodCt: int missCt: int - def __init__(self, marvs: int = 0, greats: int = 0, goods: int = 0, misses: int = 0) -> None: + def __init__( + self, marvs: int = 0, greats: int = 0, goods: int = 0, misses: int = 0 + ) -> None: self.marvCt = marvs self.greatCt = greats self.goodCt = goods self.missCt = misses - + def make(self) -> List: return [self.marvCt, self.greatCt, self.goodCt, self.missCt] + class SongUpdateDetailV1: - def __init__(self, data: List) -> None: + def __init__(self, data: List) -> None: if data is not None: self.songId = data[0] self.difficulty = data[1] self.level = data[2] self.score = data[3] - - self.judgements = SongUpdateJudgementCounts(data[4][0], data[4][1], data[4][2], data[4][3]) + + self.judgements = SongUpdateJudgementCounts( + data[4][0], data[4][1], data[4][2], data[4][3] + ) self.maxCombo = data[5] - self.grade = WaccaConstants.GRADES(data[6]) # .value to get number, .name to get letter + self.grade = WaccaConstants.GRADES( + data[6] + ) # .value to get number, .name to get letter self.flagCleared = False if data[7] == 0 else True self.flagMissless = False if data[8] == 0 else True self.flagFullcombo = False if data[9] == 0 else True self.flagAllMarvelous = False if data[10] == 0 else True self.flagGiveUp = False if data[11] == 0 else True - self.skillPt = data[12] + self.skillPt = data[12] self.fastCt = 0 self.slowCt = 0 self.flagNewRecord = False + class SongUpdateDetailV2(SongUpdateDetailV1): def __init__(self, data: List) -> None: super().__init__(data) @@ -578,6 +668,7 @@ class SongUpdateDetailV2(SongUpdateDetailV1): self.slowCt = data[14] self.flagNewRecord = False if data[15] == 0 else True + class SeasonalInfoV1: def __init__(self) -> None: self.level: int = 0 @@ -586,7 +677,7 @@ class SeasonalInfoV1: self.cumulativeScore: int = 0 self.titlesObtained: int = 0 self.iconsObtained: int = 0 - self.skillPts: int = 0 + self.skillPts: int = 0 self.noteColorsObtained: int = 0 self.noteSoundsObtained: int = 0 @@ -600,9 +691,10 @@ class SeasonalInfoV1: self.iconsObtained, self.skillPts, self.noteColorsObtained, - self.noteSoundsObtained + self.noteSoundsObtained, ] + class SeasonalInfoV2(SeasonalInfoV1): def __init__(self) -> None: super().__init__() @@ -612,6 +704,7 @@ class SeasonalInfoV2(SeasonalInfoV1): def make(self) -> List: return super().make() + [self.platesObtained, self.cumulativeGatePts] + class BingoPageStatus: id = 0 location = 1 @@ -625,23 +718,30 @@ class BingoPageStatus: def make(self) -> List: return [self.id, self.location, self.progress] + class BingoDetail: def __init__(self, pageNumber: int) -> None: self.pageNumber = pageNumber self.pageStatus: List[BingoPageStatus] = [] - + def make(self) -> List: status = [] for x in self.pageStatus: status.append(x.make()) - return [ - self.pageNumber, - status - ] + return [self.pageNumber, status] + class GateDetailV1: - def __init__(self, gate_id: int = 1, page: int = 1, progress: int = 0, loops: int = 0, last_used: int = 0, mission_flg = 0) -> None: + def __init__( + self, + gate_id: int = 1, + page: int = 1, + progress: int = 0, + loops: int = 0, + last_used: int = 0, + mission_flg=0, + ) -> None: self.id = gate_id self.page = page self.progress = progress @@ -652,14 +752,17 @@ class GateDetailV1: def make(self) -> List: return [self.id, 1, self.page, self.progress, self.loops, self.lastUsed] + class GateDetailV2(GateDetailV1): def make(self) -> List: return super().make() + [self.missionFlg] + class GachaInfo: def make(self) -> List: return [] + class LastSongDetail: lastSongId = 90 lastSongDiff = 1 @@ -667,8 +770,14 @@ class LastSongDetail: lastFolderId = 1 lastSongOrd = 1 - def __init__(self, last_song: int = 90, last_diff: int = 1, last_folder_ord: int = 1, - last_folder_id: int = 1, last_song_ord: int = 1) -> None: + def __init__( + self, + last_song: int = 90, + last_diff: int = 1, + last_folder_ord: int = 1, + last_folder_id: int = 1, + last_song_ord: int = 1, + ) -> None: self.lastSongId = last_song self.lastSongDiff = last_diff self.lastFolderOrd = last_folder_ord @@ -676,13 +785,20 @@ class LastSongDetail: self.lastSongOrd = last_song_ord def make(self) -> List: - return [self.lastSongId, self.lastSongDiff, self.lastFolderOrd, self.lastFolderId, - self.lastSongOrd] + return [ + self.lastSongId, + self.lastSongDiff, + self.lastFolderOrd, + self.lastFolderId, + self.lastSongOrd, + ] + class FriendDetail: def make(self) -> List: return [] + class LoginBonusInfo: def __init__(self) -> None: self.tickets: List[TicketItem] = [] @@ -695,27 +811,38 @@ class LoginBonusInfo: for ticket in self.tickets: tks.append(ticket.make()) - + for item in self.items: itms.append(item.make()) - - return [ tks, itms, self.message ] + + return [tks, itms, self.message] + class VipLoginBonus: id = 1 unknown = 0 item: GenericItemRecv - def __init__(self, id: int = 1, unk: int = 0, item_type: int = 1, item_id: int = 1, item_qt: int = 1) -> None: + def __init__( + self, + id: int = 1, + unk: int = 0, + item_type: int = 1, + item_id: int = 1, + item_qt: int = 1, + ) -> None: self.id = id self.unknown = unk self.item = GenericItemRecv(item_type, item_id, item_qt) def make(self) -> List: - return [ self.id, self.unknown, self.item.make() ] + return [self.id, self.unknown, self.item.make()] + class VipInfo: - def __init__(self, year: int = 2019, month: int = 1, day: int = 1, num_item: int = 1) -> None: + def __init__( + self, year: int = 2019, month: int = 1, day: int = 1, num_item: int = 1 + ) -> None: self.pageYear = year self.pageMonth = month self.pageDay = day @@ -729,22 +856,32 @@ class VipInfo: for present in self.presentInfo: pres.append(present.make()) - + for b in self.vipLoginBonus: vipBonus.append(b.make()) - return [ self.pageYear, self.pageMonth, self.pageDay, self.numItem, pres, vipBonus ] + return [ + self.pageYear, + self.pageMonth, + self.pageDay, + self.numItem, + pres, + vipBonus, + ] + class PurchaseType(Enum): PurchaseTypeCredit = 1 PurchaseTypeWP = 2 + class PlayType(Enum): PlayTypeSingle = 1 PlayTypeVs = 2 PlayTypeCoop = 3 PlayTypeStageup = 4 + class StageInfo: danId: int = 0 danLevel: int = 0 @@ -770,15 +907,17 @@ class StageInfo: self.song2BestScore, self.song3BestScore, ], - self.unk5 + self.unk5, ] + class StageupClearType(Enum): FAIL = 0 CLEAR_BLUE = 1 CLEAR_SILVER = 2 CLEAR_GOLD = 3 + class MusicUpdateDetailV1: def __init__(self) -> None: self.songId = 0 @@ -790,7 +929,7 @@ class MusicUpdateDetailV1: self.lowestMissCount = 0 self.maxSkillPts = 0 self.locked = 0 - + def make(self) -> List: return [ self.songId, @@ -804,25 +943,30 @@ class MusicUpdateDetailV1: self.locked, ] + class MusicUpdateDetailV2(MusicUpdateDetailV1): def __init__(self) -> None: super().__init__() self.rating = 0 - + def make(self) -> List: return super().make() + [self.rating] + class MusicUpdateDetailV3(MusicUpdateDetailV2): def __init__(self) -> None: super().__init__() self.grades = SongDetailGradeCountsV2() + class SongRatingUpdate: - def __init__(self, song_id: int = 0, difficulty: int = 1, new_rating: int = 0) -> None: + def __init__( + self, song_id: int = 0, difficulty: int = 1, new_rating: int = 0 + ) -> None: self.songId = song_id self.difficulty = difficulty self.rating = new_rating - + def make(self) -> List: return [ self.songId, @@ -830,21 +974,20 @@ class SongRatingUpdate: self.rating, ] + class GateTutorialFlag: def __init__(self, tutorial_id: int = 1, flg_watched: bool = False) -> None: self.tutorialId = tutorial_id self.flagWatched = flg_watched - + def make(self) -> List: - return [ - self.tutorialId, - int(self.flagWatched) - ] + return [self.tutorialId, int(self.flagWatched)] + class DateUpdate: def __init__(self, date_id: int = 0, timestamp: int = 0) -> None: self.id = date_id self.timestamp = timestamp - + def make(self) -> List: return [self.id, self.timestamp] diff --git a/titles/wacca/handlers/housing.py b/titles/wacca/handlers/housing.py index 1669cac..8ffa910 100644 --- a/titles/wacca/handlers/housing.py +++ b/titles/wacca/handlers/housing.py @@ -4,6 +4,7 @@ from titles.wacca.handlers.base import BaseRequest, BaseResponse from titles.wacca.handlers.helpers import HousingInfo from titles.wacca.const import WaccaConstants + # ---housing/get---- class HousingGetResponse(BaseResponse): def __init__(self, housingId: int) -> None: @@ -15,6 +16,7 @@ class HousingGetResponse(BaseResponse): self.params = [self.housingId, self.regionId] return super().make() + # ---housing/start---- class HousingStartRequestV1(BaseRequest): def __init__(self, data: Dict) -> None: @@ -26,6 +28,7 @@ class HousingStartRequestV1(BaseRequest): for info in self.params[2]: self.info.append(HousingInfo(info[0], info[1])) + class HousingStartRequestV2(HousingStartRequestV1): def __init__(self, data: Dict) -> None: super(HousingStartRequestV1, self).__init__(data) @@ -37,20 +40,84 @@ class HousingStartRequestV2(HousingStartRequestV1): for info in self.params[3]: self.info.append(HousingInfo(info[0], info[1])) + class HousingStartResponseV1(BaseResponse): - def __init__(self, regionId: WaccaConstants.Region = WaccaConstants.Region.HOKKAIDO, songList: List[int] = []) -> None: + def __init__( + self, + regionId: WaccaConstants.Region = WaccaConstants.Region.HOKKAIDO, + songList: List[int] = [], + ) -> None: super().__init__() self.regionId = regionId - self.songList = songList # Recomended songs + self.songList = songList # Recomended songs if not self.songList: - self.songList = [ - 1269,1007,1270,1002,1020,1003,1008,1211,1018,1092,1056,32, - 1260,1230,1258,1251,2212,1264,1125,1037,2001,1272,1126,1119, - 1104,1070,1047,1044,1027,1004,1001,24,2068,2062,2021,1275, - 1249,1207,1203,1107,1021,1009,9,4,3,23,22,2014,13,1276,1247, - 1240,1237,1128,1114,1110,1109,1102,1045,1043,1036,1035,1030, - 1023,1015 + self.songList = [ + 1269, + 1007, + 1270, + 1002, + 1020, + 1003, + 1008, + 1211, + 1018, + 1092, + 1056, + 32, + 1260, + 1230, + 1258, + 1251, + 2212, + 1264, + 1125, + 1037, + 2001, + 1272, + 1126, + 1119, + 1104, + 1070, + 1047, + 1044, + 1027, + 1004, + 1001, + 24, + 2068, + 2062, + 2021, + 1275, + 1249, + 1207, + 1203, + 1107, + 1021, + 1009, + 9, + 4, + 3, + 23, + 22, + 2014, + 13, + 1276, + 1247, + 1240, + 1237, + 1128, + 1114, + 1110, + 1109, + 1102, + 1045, + 1043, + 1036, + 1035, + 1030, + 1023, + 1015, ] def make(self) -> Dict: diff --git a/titles/wacca/handlers/user_info.py b/titles/wacca/handlers/user_info.py index d30da39..8c67db9 100644 --- a/titles/wacca/handlers/user_info.py +++ b/titles/wacca/handlers/user_info.py @@ -3,6 +3,7 @@ from typing import List, Dict from titles.wacca.handlers.base import BaseRequest, BaseResponse from titles.wacca.handlers.helpers import UserOption, DateUpdate + # ---user/info/update--- class UserInfoUpdateRequest(BaseRequest): def __init__(self, data: Dict) -> None: @@ -16,17 +17,20 @@ class UserInfoUpdateRequest(BaseRequest): for x in self.params[1]: self.optsUpdated.append(UserOption(x[0], x[1])) - + for x in self.params[3]: self.datesUpdated.append(DateUpdate(x[0], x[1])) + # ---user/info/getMyroom--- TODO: Understand this better class UserInfogetMyroomRequest(BaseRequest): game_id = 0 + def __init__(self, data: Dict) -> None: super().__init__(data) self.game_id = int(self.params[0]) + class UserInfogetMyroomResponseV1(BaseResponse): def __init__(self) -> None: super().__init__() @@ -49,6 +53,7 @@ class UserInfogetMyroomResponseV1(BaseResponse): return super().make() + class UserInfogetMyroomResponseV2(UserInfogetMyroomResponseV1): def __init__(self) -> None: super().__init__() @@ -58,13 +63,16 @@ class UserInfogetMyroomResponseV2(UserInfogetMyroomResponseV1): self.params += [0, 0, 0] return super(UserInfogetMyroomResponseV1, self).make() + # ---user/info/getRanking--- class UserInfogetRankingRequest(BaseRequest): game_id = 0 + def __init__(self, data: Dict) -> None: super().__init__(data) self.game_id = int(self.params[0]) - + + class UserInfogetRankingResponse(BaseResponse): def __init__(self) -> None: super().__init__() @@ -85,4 +93,4 @@ class UserInfogetRankingResponse(BaseResponse): self.wacca_points_ranking, ] - return super().make() \ No newline at end of file + return super().make() diff --git a/titles/wacca/handlers/user_misc.py b/titles/wacca/handlers/user_misc.py index 4dea019..eb03802 100644 --- a/titles/wacca/handlers/user_misc.py +++ b/titles/wacca/handlers/user_misc.py @@ -5,6 +5,7 @@ from titles.wacca.handlers.helpers import PurchaseType, GenericItemRecv from titles.wacca.handlers.helpers import TicketItem, SongRatingUpdate, BingoDetail from titles.wacca.handlers.helpers import BingoPageStatus, GateTutorialFlag + # ---user/goods/purchase--- class UserGoodsPurchaseRequest(BaseRequest): def __init__(self, data: Dict) -> None: @@ -14,14 +15,17 @@ class UserGoodsPurchaseRequest(BaseRequest): self.purchaseCount = int(self.params[2]) self.purchaseType = PurchaseType(self.params[3]) self.cost = int(self.params[4]) - self.itemObtained: GenericItemRecv = GenericItemRecv(self.params[5][0], self.params[5][1], self.params[5][2]) + self.itemObtained: GenericItemRecv = GenericItemRecv( + self.params[5][0], self.params[5][1], self.params[5][2] + ) + class UserGoodsPurchaseResponse(BaseResponse): def __init__(self, wp: int = 0, tickets: List = []) -> None: super().__init__() self.currentWp = wp self.tickets: List[TicketItem] = [] - + for ticket in tickets: self.tickets.append(TicketItem(ticket[0], ticket[1], ticket[2])) @@ -34,6 +38,7 @@ class UserGoodsPurchaseResponse(BaseResponse): return super().make() + # ---user/sugaroku/update--- class UserSugarokuUpdateRequestV1(BaseRequest): def __init__(self, data: Dict) -> None: @@ -44,17 +49,19 @@ class UserSugarokuUpdateRequestV1(BaseRequest): self.progress = int(self.params[3]) self.loops = int(self.params[4]) self.boostsUsed = self.params[5] - self.totalPts = int(self.params[7]) + self.totalPts = int(self.params[7]) self.itemsObtainted: List[GenericItemRecv] = [] for item in self.params[6]: self.itemsObtainted.append(GenericItemRecv(item[0], item[1], item[2])) + class UserSugarokuUpdateRequestV2(UserSugarokuUpdateRequestV1): def __init__(self, data: Dict) -> None: super().__init__(data) self.mission_flag = int(self.params[8]) + # ---user/rating/update--- class UserRatingUpdateRequest(BaseRequest): def __init__(self, data: Dict) -> None: @@ -66,8 +73,9 @@ class UserRatingUpdateRequest(BaseRequest): for x in self.params[2]: self.songs.append(SongRatingUpdate(x[0], x[1], x[2])) + # ---user/mission/update--- -class UserMissionUpdateRequest(BaseRequest): +class UserMissionUpdateRequest(BaseRequest): def __init__(self, data: Dict) -> None: super().__init__(data) self.profileId = self.params[0] diff --git a/titles/wacca/handlers/user_music.py b/titles/wacca/handlers/user_music.py index deeda3d..a8c80bf 100644 --- a/titles/wacca/handlers/user_music.py +++ b/titles/wacca/handlers/user_music.py @@ -1,11 +1,20 @@ from typing import List, Dict from titles.wacca.handlers.base import BaseRequest, BaseResponse -from titles.wacca.handlers.helpers import GenericItemRecv, SongUpdateDetailV2, TicketItem +from titles.wacca.handlers.helpers import ( + GenericItemRecv, + SongUpdateDetailV2, + TicketItem, +) from titles.wacca.handlers.helpers import MusicUpdateDetailV2, MusicUpdateDetailV3 -from titles.wacca.handlers.helpers import SeasonalInfoV2, SeasonalInfoV1, SongUpdateDetailV1 +from titles.wacca.handlers.helpers import ( + SeasonalInfoV2, + SeasonalInfoV1, + SongUpdateDetailV1, +) from titles.wacca.handlers.helpers import MusicUpdateDetailV1 + # ---user/music/update--- class UserMusicUpdateRequestV1(BaseRequest): def __init__(self, data: Dict) -> None: @@ -18,82 +27,86 @@ class UserMusicUpdateRequestV1(BaseRequest): for itm in data["params"][3]: self.itemsObtained.append(GenericItemRecv(itm[0], itm[1], itm[2])) + class UserMusicUpdateRequestV2(UserMusicUpdateRequestV1): def __init__(self, data: Dict) -> None: super().__init__(data) self.songDetail = SongUpdateDetailV2(self.params[2]) + class UserMusicUpdateResponseV1(BaseResponse): def __init__(self) -> None: super().__init__() self.songDetail = MusicUpdateDetailV1() self.seasonInfo = SeasonalInfoV1() self.rankingInfo: List[List[int]] = [] - + def make(self) -> Dict: self.params = [ self.songDetail.make(), [self.songDetail.songId, self.songDetail.clearCounts.playCt], self.seasonInfo.make(), - self.rankingInfo + self.rankingInfo, ] return super().make() + class UserMusicUpdateResponseV2(UserMusicUpdateResponseV1): def __init__(self) -> None: super().__init__() self.songDetail = MusicUpdateDetailV2() self.seasonInfo = SeasonalInfoV2() + class UserMusicUpdateResponseV3(UserMusicUpdateResponseV2): def __init__(self) -> None: super().__init__() self.songDetail = MusicUpdateDetailV3() + # ---user/music/updateCoop--- class UserMusicUpdateCoopRequest(UserMusicUpdateRequestV2): def __init__(self, data: Dict) -> None: super().__init__(data) self.coopData = self.params[4] + # ---user/music/updateVs--- class UserMusicUpdateVsRequest(UserMusicUpdateRequestV2): def __init__(self, data: Dict) -> None: super().__init__(data) self.vsData = self.params[4] + # ---user/music/unlock--- class UserMusicUnlockRequest(BaseRequest): def __init__(self, data: Dict) -> None: super().__init__(data) self.profileId = self.params[0] self.songId = self.params[1] - self.difficulty = self.params[2] + self.difficulty = self.params[2] self.itemsUsed: List[GenericItemRecv] = [] for itm in self.params[3]: self.itemsUsed.append(GenericItemRecv(itm[0], itm[1], itm[2])) + class UserMusicUnlockResponse(BaseResponse): def __init__(self, current_wp: int = 0, tickets_remaining: List = []) -> None: super().__init__() - self.wp = current_wp + self.wp = current_wp self.tickets: List[TicketItem] = [] for ticket in tickets_remaining: self.tickets.append(TicketItem(ticket[0], ticket[1], ticket[2])) - - def make(self)-> Dict: + + def make(self) -> Dict: tickets = [] for ticket in self.tickets: tickets.append(ticket.make()) - self.params = [ - self.wp, - tickets - ] + self.params = [self.wp, tickets] return super().make() - diff --git a/titles/wacca/handlers/user_status.py b/titles/wacca/handlers/user_status.py index 5874fec..0e3819d 100644 --- a/titles/wacca/handlers/user_status.py +++ b/titles/wacca/handlers/user_status.py @@ -3,6 +3,7 @@ from typing import List, Dict, Optional from titles.wacca.handlers.base import BaseRequest, BaseResponse from titles.wacca.handlers.helpers import * + # ---user/status/get---- class UserStatusGetRequest(BaseRequest): aimeId: int = 0 @@ -11,6 +12,7 @@ class UserStatusGetRequest(BaseRequest): super().__init__(data) self.aimeId = int(data["params"][0]) + class UserStatusGetV1Response(BaseResponse): def __init__(self) -> None: super().__init__() @@ -27,14 +29,12 @@ class UserStatusGetV1Response(BaseResponse): self.setTitleId, self.setIconId, self.profileStatus.value, - [ - self.versionStatus.value, - str(self.lastGameVersion) - ] + [self.versionStatus.value, str(self.lastGameVersion)], ] - + return super().make() + class UserStatusGetV2Response(UserStatusGetV1Response): def __init__(self) -> None: super().__init__() @@ -48,6 +48,7 @@ class UserStatusGetV2Response(UserStatusGetV1Response): return super(UserStatusGetV1Response, self).make() + # ---user/status/getDetail---- class UserStatusGetDetailRequest(BaseRequest): userId: int = 0 @@ -56,6 +57,7 @@ class UserStatusGetDetailRequest(BaseRequest): super().__init__(data) self.userId = data["params"][0] + class UserStatusGetDetailResponseV1(BaseResponse): def __init__(self) -> None: super().__init__() @@ -64,22 +66,32 @@ class UserStatusGetDetailResponseV1(BaseResponse): self.seasonalPlayModeCounts: List[PlayModeCounts] = [] self.userItems: UserItemInfoV1 = UserItemInfoV1() self.scores: List[BestScoreDetailV1] = [] - self.songPlayStatus: List[int] = [0,0] + self.songPlayStatus: List[int] = [0, 0] self.seasonInfo: SeasonalInfoV1 = SeasonalInfoV1() - self.playAreaList: List = [ [0],[0,0,0,0,0,0],[0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0],[0,0,0,0],[0,0,0,0,0,0,0],[0] ] + self.playAreaList: List = [ + [0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0], + ] self.songUpdateTime: int = 0 - def make(self)-> Dict: + def make(self) -> Dict: opts = [] play_modes = [] scores = [] for x in self.seasonalPlayModeCounts: play_modes.append(x.make()) - + for x in self.scores: scores.append(x.make()) - + for x in self.options: opts.append(x.make()) @@ -92,21 +104,31 @@ class UserStatusGetDetailResponseV1(BaseResponse): self.songPlayStatus, self.seasonInfo.make(), self.playAreaList, - self.songUpdateTime + self.songUpdateTime, ] return super().make() - - def find_score_idx(self, song_id: int, difficulty: int = 1, start_idx: int = 0, stop_idx: Optional[int] = None) -> Optional[int]: + + def find_score_idx( + self, + song_id: int, + difficulty: int = 1, + start_idx: int = 0, + stop_idx: Optional[int] = None, + ) -> Optional[int]: if stop_idx is None or stop_idx > len(self.scores): stop_idx = len(self.scores) for x in range(start_idx, stop_idx): - if self.scores[x].songId == song_id and self.scores[x].difficulty == difficulty: + if ( + self.scores[x].songId == song_id + and self.scores[x].difficulty == difficulty + ): return x - + return None + class UserStatusGetDetailResponseV2(UserStatusGetDetailResponseV1): def __init__(self) -> None: super().__init__() @@ -122,7 +144,7 @@ class UserStatusGetDetailResponseV2(UserStatusGetDetailResponseV1): self.gatchaInfo: List[GachaInfo] = [] self.friendList: List[FriendDetail] = [] - def make(self)-> Dict: + def make(self) -> Dict: super().make() gates = [] friends = [] @@ -130,13 +152,13 @@ class UserStatusGetDetailResponseV2(UserStatusGetDetailResponseV1): for x in self.gateInfo: gates.append(x.make()) - + for x in self.friendList: friends.append(x.make()) - + for x in self.gateTutorialFlags: tut_flg.append(x.make()) - + while len(tut_flg) < 5: flag_id = len(tut_flg) + 1 tut_flg.append([flag_id, 0]) @@ -152,11 +174,13 @@ class UserStatusGetDetailResponseV2(UserStatusGetDetailResponseV1): return super(UserStatusGetDetailResponseV1, self).make() + class UserStatusGetDetailResponseV3(UserStatusGetDetailResponseV2): def __init__(self) -> None: super().__init__() self.gateInfo: List[GateDetailV2] = [] + class UserStatusGetDetailResponseV4(UserStatusGetDetailResponseV3): def __init__(self) -> None: super().__init__() @@ -164,12 +188,13 @@ class UserStatusGetDetailResponseV4(UserStatusGetDetailResponseV3): self.bingoStatus: BingoDetail = BingoDetail(0) self.scores: List[BestScoreDetailV2] = [] - def make(self)-> Dict: + def make(self) -> Dict: super().make() self.params.append(self.bingoStatus.make()) return super(UserStatusGetDetailResponseV1, self).make() + # ---user/status/login---- class UserStatusLoginRequest(BaseRequest): userId: int = 0 @@ -178,16 +203,19 @@ class UserStatusLoginRequest(BaseRequest): super().__init__(data) self.userId = data["params"][0] + class UserStatusLoginResponseV1(BaseResponse): - def __init__(self, is_first_login_daily: bool = False, last_login_date: int = 0) -> None: + def __init__( + self, is_first_login_daily: bool = False, last_login_date: int = 0 + ) -> None: super().__init__() self.dailyBonus: List[LoginBonusInfo] = [] self.consecBonus: List[LoginBonusInfo] = [] - self.otherBonus: List[LoginBonusInfo] = [] + self.otherBonus: List[LoginBonusInfo] = [] self.firstLoginDaily = is_first_login_daily self.lastLoginDate = last_login_date - def make(self)-> Dict: + def make(self) -> Dict: super().make() daily = [] consec = [] @@ -202,32 +230,39 @@ class UserStatusLoginResponseV1(BaseResponse): for bonus in self.otherBonus: other.append(bonus.make()) - self.params = [ daily, consec, other, int(self.firstLoginDaily)] + self.params = [daily, consec, other, int(self.firstLoginDaily)] return super().make() + class UserStatusLoginResponseV2(UserStatusLoginResponseV1): - def __init__(self, is_first_login_daily: bool = False, last_login_date: int = 0) -> None: + def __init__( + self, is_first_login_daily: bool = False, last_login_date: int = 0 + ) -> None: super().__init__(is_first_login_daily) self.lastLoginDate = last_login_date self.vipInfo = VipInfo() - - def make(self)-> Dict: + + def make(self) -> Dict: super().make() self.params.append(self.vipInfo.make()) self.params.append(self.lastLoginDate) return super(UserStatusLoginResponseV1, self).make() + class UserStatusLoginResponseV3(UserStatusLoginResponseV2): - def __init__(self, is_first_login_daily: bool = False, last_login_date: int = 0) -> None: + def __init__( + self, is_first_login_daily: bool = False, last_login_date: int = 0 + ) -> None: super().__init__(is_first_login_daily, last_login_date) self.unk: List = [] - def make(self)-> Dict: + def make(self) -> Dict: super().make() self.params.append(self.unk) return super(UserStatusLoginResponseV1, self).make() + # ---user/status/create--- class UserStatusCreateRequest(BaseRequest): def __init__(self, data: Dict) -> None: @@ -235,26 +270,27 @@ class UserStatusCreateRequest(BaseRequest): self.aimeId = data["params"][0] self.username = data["params"][1] + class UserStatusCreateResponseV1(BaseResponse): def __init__(self, userId: int, username: str) -> None: super().__init__() self.userStatus = UserStatusV1() self.userStatus.userId = userId self.userStatus.username = username - - def make(self)-> Dict: - self.params = [ - self.userStatus.make() - ] + + def make(self) -> Dict: + self.params = [self.userStatus.make()] return super().make() + class UserStatusCreateResponseV2(UserStatusCreateResponseV1): def __init__(self, userId: int, username: str) -> None: super().__init__(userId, username) - self.userStatus: UserStatusV2 = UserStatusV2() + self.userStatus: UserStatusV2 = UserStatusV2() self.userStatus.userId = userId self.userStatus.username = username + # ---user/status/logout--- class UserStatusLogoutRequest(BaseRequest): userId: int @@ -263,6 +299,7 @@ class UserStatusLogoutRequest(BaseRequest): super().__init__(data) self.userId = data["params"][0] + # ---user/status/update--- class UserStatusUpdateRequestV1(BaseRequest): def __init__(self, data: Dict) -> None: @@ -274,11 +311,17 @@ class UserStatusUpdateRequestV1(BaseRequest): for itm in data["params"][2]: self.itemsRecieved.append(GenericItemRecv(itm[0], itm[1], itm[2])) + class UserStatusUpdateRequestV2(UserStatusUpdateRequestV1): def __init__(self, data: Dict) -> None: super().__init__(data) self.isContinue = bool(data["params"][3]) self.isFirstPlayFree = bool(data["params"][4]) self.itemsUsed = data["params"][5] - self.lastSongInfo = LastSongDetail(data["params"][6][0], data["params"][6][1], - data["params"][6][2], data["params"][6][3], data["params"][6][4]) + self.lastSongInfo = LastSongDetail( + data["params"][6][0], + data["params"][6][1], + data["params"][6][2], + data["params"][6][3], + data["params"][6][4], + ) diff --git a/titles/wacca/handlers/user_trial.py b/titles/wacca/handlers/user_trial.py index 6fb75a8..ba6ea50 100644 --- a/titles/wacca/handlers/user_trial.py +++ b/titles/wacca/handlers/user_trial.py @@ -2,6 +2,7 @@ from typing import Dict, List from titles.wacca.handlers.base import BaseRequest, BaseResponse from titles.wacca.handlers.helpers import StageInfo, StageupClearType, GenericItemRecv + # --user/trial/get-- class UserTrialGetRequest(BaseRequest): profileId: int = 0 @@ -10,20 +11,22 @@ class UserTrialGetRequest(BaseRequest): super().__init__(data) self.profileId = self.params[0] + class UserTrialGetResponse(BaseResponse): def __init__(self) -> None: super().__init__() - + self.stageList: List[StageInfo] = [] def make(self) -> Dict: dans = [] for x in self.stageList: dans.append(x.make()) - + self.params = [dans] return super().make() + # --user/trial/update-- class UserTrialUpdateRequest(BaseRequest): def __init__(self, data: Dict) -> None: @@ -43,9 +46,10 @@ class UserTrialUpdateRequest(BaseRequest): if len(self.params) == 8: self.unk7 = self.params[7] + class UserTrialUpdateResponse(BaseResponse): def __init__(self) -> None: super().__init__() - + def make(self) -> Dict: - return super().make() \ No newline at end of file + return super().make() diff --git a/titles/wacca/handlers/user_vip.py b/titles/wacca/handlers/user_vip.py index c48c9fa..bc418b5 100644 --- a/titles/wacca/handlers/user_vip.py +++ b/titles/wacca/handlers/user_vip.py @@ -2,12 +2,14 @@ from typing import Dict, List from titles.wacca.handlers.base import BaseRequest, BaseResponse from titles.wacca.handlers.helpers import VipLoginBonus + # --user/vip/get-- class UserVipGetRequest(BaseRequest): def __init__(self, data: Dict) -> None: super().__init__(data) self.profileId = self.params[0] + class UserVipGetResponse(BaseResponse): def __init__(self) -> None: super().__init__() @@ -15,22 +17,16 @@ class UserVipGetResponse(BaseResponse): self.unknown1: int = 1 self.unknown2: int = 1 self.presents: List[VipLoginBonus] = [] - + def make(self) -> Dict: pres = [] for x in self.presents: pres.append(x.make()) - self.params = [ - self.vipDays, - [ - self.unknown1, - self.unknown2, - pres - ] - ] + self.params = [self.vipDays, [self.unknown1, self.unknown2, pres]] return super().make() + # --user/vip/start-- class UserVipStartRequest(BaseRequest): def __init__(self, data: Dict) -> None: @@ -39,6 +35,7 @@ class UserVipStartRequest(BaseRequest): self.cost = self.params[1] self.days = self.params[2] + class UserVipStartResponse(BaseResponse): def __init__(self, expires: int = 0) -> None: super().__init__() @@ -46,9 +43,6 @@ class UserVipStartResponse(BaseResponse): self.presents = [] def make(self) -> Dict: - self.params = [ - self.whenExpires, - self.presents - ] + self.params = [self.whenExpires, self.presents] - return super().make() \ No newline at end of file + return super().make() diff --git a/titles/wacca/index.py b/titles/wacca/index.py index b4a4aea..2a4a0cf 100644 --- a/titles/wacca/index.py +++ b/titles/wacca/index.py @@ -20,12 +20,15 @@ from titles.wacca.base import WaccaBase from titles.wacca.handlers.base import BaseResponse from titles.wacca.handlers.helpers import Version -class WaccaServlet(): + +class WaccaServlet: def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: self.core_cfg = core_cfg self.game_cfg = WaccaConfig() if path.exists(f"{cfg_dir}/{WaccaConstants.CONFIG_NAME}"): - self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{WaccaConstants.CONFIG_NAME}"))) + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{WaccaConstants.CONFIG_NAME}")) + ) self.versions = [ WaccaBase(core_cfg, self.game_cfg), @@ -38,32 +41,46 @@ class WaccaServlet(): self.logger = logging.getLogger("wacca") log_fmt_str = "[%(asctime)s] Wacca | %(levelname)s | %(message)s" log_fmt = logging.Formatter(log_fmt_str) - fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.core_cfg.server.log_dir, "wacca"), encoding='utf8', - when="d", backupCount=10) + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.core_cfg.server.log_dir, "wacca"), + encoding="utf8", + when="d", + backupCount=10, + ) fileHandler.setFormatter(log_fmt) - + consoleHandler = logging.StreamHandler() consoleHandler.setFormatter(log_fmt) self.logger.addHandler(fileHandler) self.logger.addHandler(consoleHandler) - + self.logger.setLevel(self.game_cfg.server.loglevel) - coloredlogs.install(level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str) - + coloredlogs.install( + level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str + ) + @classmethod - def get_allnet_info(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str, str]: + def get_allnet_info( + cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str + ) -> Tuple[bool, str, str]: game_cfg = WaccaConfig() if path.exists(f"{cfg_dir}/{WaccaConstants.CONFIG_NAME}"): - game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{WaccaConstants.CONFIG_NAME}"))) + game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{WaccaConstants.CONFIG_NAME}")) + ) if not game_cfg.server.enable: return (False, "", "") - + if core_cfg.server.is_develop: - return (True, f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v", "") - + return ( + True, + f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v", + "", + ) + return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v", "") def render_POST(self, request: Request, version: int, url_path: str) -> bytes: @@ -71,12 +88,14 @@ class WaccaServlet(): hash = md5(json.dumps(resp, ensure_ascii=False).encode()).digest() request.responseHeaders.addRawHeader(b"X-Wacca-Hash", hash.hex().encode()) return json.dumps(resp).encode() - + try: req_json = json.loads(request.content.getvalue()) version_full = Version(req_json["appVersion"]) except: - self.logger.error(f"Failed to parse request toi {request.uri} -> {request.content.getvalue()}") + self.logger.error( + f"Failed to parse request toi {request.uri} -> {request.content.getvalue()}" + ) resp = BaseResponse() resp.status = 1 resp.message = "不正なリクエスト エラーです" @@ -94,7 +113,7 @@ class WaccaServlet(): if ver_search < 15000: internal_ver = WaccaConstants.VER_WACCA - + elif ver_search >= 15000 and ver_search < 20000: internal_ver = WaccaConstants.VER_WACCA_S @@ -103,37 +122,45 @@ class WaccaServlet(): elif ver_search >= 25000 and ver_search < 30000: internal_ver = WaccaConstants.VER_WACCA_LILY_R - + elif ver_search >= 30000: internal_ver = WaccaConstants.VER_WACCA_REVERSE else: - self.logger.warning(f"Unsupported version ({req_json['appVersion']}) request {url_path} - {req_json}") + self.logger.warning( + f"Unsupported version ({req_json['appVersion']}) request {url_path} - {req_json}" + ) resp = BaseResponse() resp.status = 1 resp.message = "不正なアプリバージョンエラーです" return end(resp.make()) - - self.logger.info(f"v{req_json['appVersion']} {url_path} request from {request.getClientAddress().host} with chipId {req_json['chipId']}") + + self.logger.info( + f"v{req_json['appVersion']} {url_path} request from {request.getClientAddress().host} with chipId {req_json['chipId']}" + ) self.logger.debug(req_json) if not hasattr(self.versions[internal_ver], func_to_find): - self.logger.warn(f"{req_json['appVersion']} has no handler for {func_to_find}") + self.logger.warn( + f"{req_json['appVersion']} has no handler for {func_to_find}" + ) resp = BaseResponse().make() return end(resp) try: handler = getattr(self.versions[internal_ver], func_to_find) resp = handler(req_json) - + self.logger.debug(f"{req_json['appVersion']} response {resp}") return end(resp) except Exception as e: - self.logger.error(f"{req_json['appVersion']} Error handling method {url_path} -> {e}") + self.logger.error( + f"{req_json['appVersion']} Error handling method {url_path} -> {e}" + ) if self.core_cfg.server.is_develop: raise - + resp = BaseResponse() resp.status = 1 resp.message = "A server error occoured." diff --git a/titles/wacca/lily.py b/titles/wacca/lily.py index e67b25c..c3b6eb4 100644 --- a/titles/wacca/lily.py +++ b/titles/wacca/lily.py @@ -9,6 +9,7 @@ from titles.wacca.const import WaccaConstants from titles.wacca.handlers import * + class WaccaLily(WaccaS): def __init__(self, cfg: CoreConfig, game_cfg: WaccaConfig) -> None: super().__init__(cfg, game_cfg) @@ -35,31 +36,35 @@ class WaccaLily(WaccaS): (210002, 0), (210003, 0), ] - - def handle_advertise_GetNews_request(self, data: Dict)-> Dict: + + def handle_advertise_GetNews_request(self, data: Dict) -> Dict: resp = GetNewsResponseV3() return resp.make() def handle_housing_start_request(self, data: Dict) -> Dict: req = HousingStartRequestV2(data) - - if req.appVersion.country != "JPN" and req.appVersion.country in [region.name for region in WaccaConstants.Region]: + + if req.appVersion.country != "JPN" and req.appVersion.country in [ + region.name for region in WaccaConstants.Region + ]: region_id = WaccaConstants.Region[req.appVersion.country] else: region_id = self.region_id resp = HousingStartResponseV1(region_id) return resp.make() - - def handle_user_status_create_request(self, data: Dict)-> Dict: + + def handle_user_status_create_request(self, data: Dict) -> Dict: req = UserStatusCreateRequest(data) resp = super().handle_user_status_create_request(data) - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210002) # Lily, Added Lily + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210002 + ) # Lily, Added Lily return resp - 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() @@ -74,7 +79,7 @@ class WaccaLily(WaccaS): resp.lastGameVersion = ShortVersion(str(req.appVersion)) else: resp.lastGameVersion = ShortVersion(profile["last_game_ver"]) - + resp.userStatus.userId = profile["id"] resp.userStatus.username = profile["username"] resp.userStatus.xp = profile["xp"] @@ -87,40 +92,55 @@ class WaccaLily(WaccaS): resp.userStatus.loginsToday = profile["login_count_today"] resp.userStatus.rating = profile["rating"] - set_title_id = self.data.profile.get_options(WaccaConstants.OPTIONS["set_title_id"], profile["user"]) + set_title_id = self.data.profile.get_options( + WaccaConstants.OPTIONS["set_title_id"], profile["user"] + ) if set_title_id is None: set_title_id = self.OPTIONS_DEFAULTS["set_title_id"] resp.setTitleId = set_title_id - set_icon_id = self.data.profile.get_options(WaccaConstants.OPTIONS["set_title_id"], profile["user"]) + set_icon_id = self.data.profile.get_options( + WaccaConstants.OPTIONS["set_title_id"], profile["user"] + ) if set_icon_id is None: set_icon_id = self.OPTIONS_DEFAULTS["set_icon_id"] resp.setIconId = set_icon_id - if profile["last_login_date"].timestamp() < int(datetime.now().replace(hour=0,minute=0,second=0,microsecond=0).timestamp()): + if profile["last_login_date"].timestamp() < int( + datetime.now() + .replace(hour=0, minute=0, second=0, microsecond=0) + .timestamp() + ): resp.userStatus.loginsToday = 0 - if profile["last_login_date"].timestamp() < int((datetime.now().replace(hour=0,minute=0,second=0,microsecond=0) - timedelta(days=1)).timestamp()): + if profile["last_login_date"].timestamp() < int( + ( + datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) + - timedelta(days=1) + ).timestamp() + ): resp.userStatus.loginConsecutiveDays = 0 if req.appVersion > resp.lastGameVersion: resp.versionStatus = PlayVersionStatus.VersionUpgrade - + elif req.appVersion < resp.lastGameVersion: resp.versionStatus = PlayVersionStatus.VersionTooNew - + if profile["vip_expire_time"] is not None: resp.userStatus.vipExpireTime = int(profile["vip_expire_time"].timestamp()) - + if profile["always_vip"] or self.game_config.mods.always_vip: - resp.userStatus.vipExpireTime = int((datetime.now() + timedelta(days=30)).timestamp()) - + resp.userStatus.vipExpireTime = int( + (datetime.now() + timedelta(days=30)).timestamp() + ) + if self.game_config.mods.infinite_wp: resp.userStatus.wp = 999999 - + return resp.make() - 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_new_day = False @@ -130,27 +150,38 @@ class WaccaLily(WaccaS): if req.userId == 0: self.logger.info(f"Guest login on {req.chipId}") resp.lastLoginDate = 0 - + else: profile = self.data.profile.get_profile(req.userId) if profile is None: - self.logger.warn(f"Unknown user id {req.userId} attempted login from {req.chipId}") + self.logger.warn( + f"Unknown user id {req.userId} attempted login from {req.chipId}" + ) return resp.make() self.logger.info(f"User {req.userId} login on {req.chipId}") last_login_time = int(profile["last_login_date"].timestamp()) resp.lastLoginDate = last_login_time - - # If somebodies login timestamp < midnight of current day, then they are logging in for the first time today - if last_login_time < int(datetime.now().replace(hour=0,minute=0,second=0,microsecond=0).timestamp()): + + # If somebodies login timestamp < midnight of current day, then they are logging in for the first time today + if last_login_time < int( + datetime.now() + .replace(hour=0, minute=0, second=0, microsecond=0) + .timestamp() + ): is_new_day = True is_consec_day = True # If somebodies login timestamp > midnight of current day + 1 day, then they broke their daily login streak - elif last_login_time > int((datetime.now().replace(hour=0,minute=0,second=0,microsecond=0) + timedelta(days=1)).timestamp()): + elif last_login_time > int( + ( + datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) + + timedelta(days=1) + ).timestamp() + ): is_consec_day = False # else, they are simply logging in again on the same day, and we don't need to do anything for that - + self.data.profile.session_login(req.userId, is_new_day, is_consec_day) resp.vipInfo.pageYear = datetime.now().year resp.vipInfo.pageMonth = datetime.now().month @@ -158,10 +189,10 @@ class WaccaLily(WaccaS): resp.vipInfo.numItem = 1 resp.firstLoginDaily = int(is_new_day) - + return resp.make() - - 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() @@ -187,15 +218,23 @@ class WaccaLily(WaccaS): if profile["vip_expire_time"] is None: resp.userStatus.vipExpireTime = 0 - + else: - resp.userStatus.vipExpireTime = int(profile["vip_expire_time"].timestamp()) - + resp.userStatus.vipExpireTime = int(profile["vip_expire_time"].timestamp()) + if profile["always_vip"] or self.game_config.mods.always_vip: - resp.userStatus.vipExpireTime = int((self.srvtime + timedelta(days=31)).timestamp()) + resp.userStatus.vipExpireTime = int( + (self.srvtime + timedelta(days=31)).timestamp() + ) resp.songUpdateTime = int(profile["last_login_date"].timestamp()) - resp.lastSongInfo = LastSongDetail(profile["last_song_id"],profile["last_song_difficulty"],profile["last_folder_order"],profile["last_folder_id"],profile["last_song_order"]) + resp.lastSongInfo = LastSongDetail( + profile["last_song_id"], + profile["last_song_difficulty"], + profile["last_folder_order"], + profile["last_folder_id"], + profile["last_song_order"], + ) resp.songPlayStatus = [resp.lastSongInfo.lastSongId, 1] resp.userStatus.userId = profile["id"] @@ -208,41 +247,65 @@ class WaccaLily(WaccaS): resp.userStatus.loginDays = profile["login_count_days"] resp.userStatus.loginConsecutiveDays = profile["login_count_days_consec"] resp.userStatus.loginsToday = profile["login_count_today"] - resp.userStatus.rating = profile['rating'] + resp.userStatus.rating = profile["rating"] if self.game_config.mods.infinite_wp: resp.userStatus.wp = 999999 for fav in profile_favorites: resp.favorites.append(fav["song_id"]) - + if profile["friend_view_1"] is not None: pass if profile["friend_view_2"] is not None: pass if profile["friend_view_3"] is not None: pass - - resp.seasonalPlayModeCounts.append(PlayModeCounts(self.season, 1, profile["playcount_single"])) - resp.seasonalPlayModeCounts.append(PlayModeCounts(self.season, 2, profile["playcount_multi_vs"])) - resp.seasonalPlayModeCounts.append(PlayModeCounts(self.season, 3, profile["playcount_multi_coop"])) - resp.seasonalPlayModeCounts.append(PlayModeCounts(self.season, 4, profile["playcount_stageup"])) - + + resp.seasonalPlayModeCounts.append( + PlayModeCounts(self.season, 1, profile["playcount_single"]) + ) + resp.seasonalPlayModeCounts.append( + PlayModeCounts(self.season, 2, profile["playcount_multi_vs"]) + ) + resp.seasonalPlayModeCounts.append( + PlayModeCounts(self.season, 3, profile["playcount_multi_coop"]) + ) + resp.seasonalPlayModeCounts.append( + PlayModeCounts(self.season, 4, profile["playcount_stageup"]) + ) + for opt in profile_options: resp.options.append(UserOption(opt["opt_id"], opt["value"])) - + for gate in self.game_config.gates.enabled_gates: added_gate = False for user_gate in profile_gates: if user_gate["gate_id"] == gate: if req.appVersion.minor >= 53: - resp.gateInfo.append(GateDetailV2(user_gate["gate_id"],user_gate["page"],user_gate["progress"], - user_gate["loops"],int(user_gate["last_used"].timestamp()),user_gate["mission_flag"])) - + resp.gateInfo.append( + GateDetailV2( + user_gate["gate_id"], + user_gate["page"], + user_gate["progress"], + user_gate["loops"], + int(user_gate["last_used"].timestamp()), + user_gate["mission_flag"], + ) + ) + else: - resp.gateInfo.append(GateDetailV1(user_gate["gate_id"],user_gate["page"],user_gate["progress"], - user_gate["loops"],int(user_gate["last_used"].timestamp()),user_gate["mission_flag"])) + resp.gateInfo.append( + GateDetailV1( + user_gate["gate_id"], + user_gate["page"], + user_gate["progress"], + user_gate["loops"], + int(user_gate["last_used"].timestamp()), + user_gate["mission_flag"], + ) + ) resp.seasonInfo.cumulativeGatePts += user_gate["total_points"] @@ -252,17 +315,21 @@ class WaccaLily(WaccaS): if not added_gate: if req.appVersion.minor >= 53: resp.gateInfo.append(GateDetailV2(gate)) - + else: resp.gateInfo.append(GateDetailV1(gate)) - + for unlock in profile_song_unlocks: for x in range(1, unlock["highest_difficulty"] + 1): - resp.userItems.songUnlocks.append(SongUnlock(unlock["song_id"], x, 0, int(unlock["acquire_date"].timestamp()))) - + resp.userItems.songUnlocks.append( + SongUnlock( + unlock["song_id"], x, 0, int(unlock["acquire_date"].timestamp()) + ) + ) + for song in profile_scores: resp.seasonInfo.cumulativeScore += song["score"] - + clear_cts = SongDetailClearCounts( song["play_ct"], song["clear_ct"], @@ -272,13 +339,20 @@ class WaccaLily(WaccaS): ) grade_cts = SongDetailGradeCountsV1( - song["grade_d_ct"], song["grade_c_ct"], song["grade_b_ct"], song["grade_a_ct"], song["grade_aa_ct"], - song["grade_aaa_ct"], song["grade_s_ct"], song["grade_ss_ct"], song["grade_sss_ct"], - song["grade_master_ct"] + song["grade_d_ct"], + song["grade_c_ct"], + song["grade_b_ct"], + song["grade_a_ct"], + song["grade_aa_ct"], + song["grade_aaa_ct"], + song["grade_s_ct"], + song["grade_ss_ct"], + song["grade_sss_ct"], + song["grade_master_ct"], ) deets = BestScoreDetailV1(song["song_id"], song["chart_id"]) - deets.clearCounts = clear_cts + deets.clearCounts = clear_cts deets.clearCountsSeason = clear_cts deets.gradeCounts = grade_cts deets.score = song["score"] @@ -287,9 +361,16 @@ class WaccaLily(WaccaS): deets.rating = song["rating"] resp.scores.append(deets) - + for trophy in profile_trophies: - resp.userItems.trophies.append(TrophyItem(trophy["trophy_id"], trophy["season"], trophy["progress"], trophy["badge_type"])) + resp.userItems.trophies.append( + TrophyItem( + trophy["trophy_id"], + trophy["season"], + trophy["progress"], + trophy["badge_type"], + ) + ) if self.game_config.mods.infinite_tickets: for x in range(5): @@ -301,27 +382,45 @@ class WaccaLily(WaccaS): else: expire = int(ticket["expire_date"].timestamp()) - resp.userItems.tickets.append(TicketItem(ticket["id"], ticket["ticket_id"], expire)) + resp.userItems.tickets.append( + TicketItem(ticket["id"], ticket["ticket_id"], expire) + ) if profile_items: for item in profile_items: try: - if item["type"] == WaccaConstants.ITEM_TYPES["icon"]: - resp.userItems.icons.append(IconItem(item["item_id"], 1, item["use_count"], int(item["acquire_date"].timestamp()))) + resp.userItems.icons.append( + IconItem( + item["item_id"], + 1, + item["use_count"], + int(item["acquire_date"].timestamp()), + ) + ) elif item["type"] == WaccaConstants.ITEM_TYPES["navigator"]: - resp.userItems.navigators.append(NavigatorItem(item["item_id"], 1, int(item["acquire_date"].timestamp()), item["use_count"], item["use_count"])) + resp.userItems.navigators.append( + NavigatorItem( + item["item_id"], + 1, + int(item["acquire_date"].timestamp()), + item["use_count"], + item["use_count"], + ) + ) else: - itm_send = GenericItemSend(item["item_id"], 1, int(item["acquire_date"].timestamp())) + itm_send = GenericItemSend( + item["item_id"], 1, int(item["acquire_date"].timestamp()) + ) if item["type"] == WaccaConstants.ITEM_TYPES["title"]: resp.userItems.titles.append(itm_send) - + elif item["type"] == WaccaConstants.ITEM_TYPES["user_plate"]: resp.userItems.plates.append(itm_send) - + elif item["type"] == WaccaConstants.ITEM_TYPES["note_color"]: resp.userItems.noteColors.append(itm_send) @@ -329,7 +428,9 @@ class WaccaLily(WaccaS): resp.userItems.noteSounds.append(itm_send) except: - self.logger.error(f"{__name__} Failed to load item {item['item_id']} for user {profile['user']}") + self.logger.error( + f"{__name__} Failed to load item {item['item_id']} for user {profile['user']}" + ) resp.seasonInfo.level = profile["xp"] resp.seasonInfo.wpObtained = profile["wp_total"] @@ -342,12 +443,18 @@ class WaccaLily(WaccaS): return resp.make() - def handle_user_info_getMyroom_request(self, data: Dict)-> Dict: + def handle_user_info_getMyroom_request(self, data: Dict) -> Dict: return UserInfogetMyroomResponseV2().make() - def handle_user_status_update_request(self, data: Dict)-> Dict: + def handle_user_status_update_request(self, data: Dict) -> Dict: super().handle_user_status_update_request(data) - req = UserStatusUpdateRequestV2(data) - self.data.profile.update_profile_lastplayed(req.profileId, req.lastSongInfo.lastSongId, req.lastSongInfo.lastSongDiff, - req.lastSongInfo.lastFolderOrd, req.lastSongInfo.lastFolderId, req.lastSongInfo.lastSongOrd) - return BaseResponse().make() \ No newline at end of file + req = UserStatusUpdateRequestV2(data) + self.data.profile.update_profile_lastplayed( + req.profileId, + req.lastSongInfo.lastSongId, + req.lastSongInfo.lastSongDiff, + req.lastSongInfo.lastFolderOrd, + req.lastSongInfo.lastFolderId, + req.lastSongInfo.lastSongOrd, + ) + return BaseResponse().make() diff --git a/titles/wacca/lilyr.py b/titles/wacca/lilyr.py index 893a8f3..0bb12ed 100644 --- a/titles/wacca/lilyr.py +++ b/titles/wacca/lilyr.py @@ -8,6 +8,7 @@ from titles.wacca.config import WaccaConfig from titles.wacca.const import WaccaConstants from titles.wacca.handlers import * + class WaccaLilyR(WaccaLily): def __init__(self, cfg: CoreConfig, game_cfg: WaccaConfig) -> None: super().__init__(cfg, game_cfg) @@ -35,20 +36,36 @@ class WaccaLilyR(WaccaLily): (210003, 0), ] - def handle_user_status_create_request(self, data: Dict)-> Dict: + def handle_user_status_create_request(self, data: Dict) -> Dict: req = UserStatusCreateRequest(data) resp = super().handle_user_status_create_request(data) - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210054) # Added lily r - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210055) # Added lily r - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210056) # Added lily r - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210057) # Added lily r - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210058) # Added lily r - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210059) # Added lily r - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210060) # Added lily r - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210061) # Added lily r - + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210054 + ) # Added lily r + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210055 + ) # Added lily r + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210056 + ) # Added lily r + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210057 + ) # Added lily r + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210058 + ) # Added lily r + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210059 + ) # Added lily r + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210060 + ) # Added lily r + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210061 + ) # Added lily r + return resp - def handle_user_status_logout_request(self, data: Dict)-> 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 1c8e7f8..109d7ff 100644 --- a/titles/wacca/read.py +++ b/titles/wacca/read.py @@ -8,42 +8,57 @@ from core.config import CoreConfig from titles.wacca.database import WaccaData from titles.wacca.const import WaccaConstants + class WaccaReader(BaseReader): - def __init__(self, config: CoreConfig, version: int, bin_dir: Optional[str], opt_dir: Optional[str], extra: Optional[str]) -> None: + def __init__( + self, + config: CoreConfig, + version: int, + bin_dir: Optional[str], + opt_dir: Optional[str], + extra: Optional[str], + ) -> None: super().__init__(config, version, bin_dir, opt_dir, extra) self.data = WaccaData(config) try: - self.logger.info(f"Start importer for {WaccaConstants.game_ver_to_string(version)}") + self.logger.info( + f"Start importer for {WaccaConstants.game_ver_to_string(version)}" + ) except IndexError: self.logger.error(f"Invalid wacca version {version}") exit(1) - + def read(self) -> None: - if not (path.exists(f"{self.bin_dir}/Table") and path.exists(f"{self.bin_dir}/Message")): + if not ( + path.exists(f"{self.bin_dir}/Table") + and path.exists(f"{self.bin_dir}/Message") + ): self.logger.error("Could not find Table or Message folder, nothing to read") return - + self.read_music(f"{self.bin_dir}/Table", "MusicParameterTable") - + def read_music(self, base_dir: str, table: str) -> None: if not self.check_valid_pair(base_dir, table): - self.logger.warn(f"Cannot find {table} uasset/uexp pair at {base_dir}, music will not be read") + self.logger.warn( + f"Cannot find {table} uasset/uexp pair at {base_dir}, music will not be read" + ) return - - uasset=open(f"{base_dir}/{table}.uasset", "rb") - uexp=open(f"{base_dir}/{table}.uexp", "rb") - package = wacky.jsonify(uasset,uexp) + uasset = open(f"{base_dir}/{table}.uasset", "rb") + uexp = open(f"{base_dir}/{table}.uexp", "rb") + + package = wacky.jsonify(uasset, uexp) package_json = json.dumps(package, indent=4, sort_keys=True) - data=json.loads(package_json) + data = json.loads(package_json) first_elem = data[0] - wacca_data = first_elem['rows'] + wacca_data = first_elem["rows"] for i, key in enumerate(wacca_data): song_id = int(key) - title = wacca_data[str(key)]["MusicMessage"] + title = wacca_data[str(key)]["MusicMessage"] artist = wacca_data[str(key)]["ArtistMessage"] bpm = wacca_data[str(key)]["Bpm"] jacket_asset_name = wacca_data[str(key)]["JacketAssetName"] @@ -52,29 +67,69 @@ class WaccaReader(BaseReader): designer = wacca_data[str(key)]["NotesDesignerNormal"] if diff > 0: - self.data.static.put_music(self.version, song_id, 1, title, artist, bpm, diff, designer, jacket_asset_name) + self.data.static.put_music( + self.version, + song_id, + 1, + title, + artist, + bpm, + diff, + designer, + jacket_asset_name, + ) self.logger.info(f"Read song {song_id} chart 1") diff = float(wacca_data[str(key)]["DifficultyHardLv"]) designer = wacca_data[str(key)]["NotesDesignerHard"] - + if diff > 0: - self.data.static.put_music(self.version, song_id, 2, title, artist, bpm, diff, designer, jacket_asset_name) + self.data.static.put_music( + self.version, + song_id, + 2, + title, + artist, + bpm, + diff, + designer, + jacket_asset_name, + ) self.logger.info(f"Read song {song_id} chart 2") diff = float(wacca_data[str(key)]["DifficultyExtremeLv"]) designer = wacca_data[str(key)]["NotesDesignerExpert"] if diff > 0: - self.data.static.put_music(self.version, song_id, 3, title, artist, bpm, diff, designer, jacket_asset_name) + self.data.static.put_music( + self.version, + song_id, + 3, + title, + artist, + bpm, + diff, + designer, + jacket_asset_name, + ) self.logger.info(f"Read song {song_id} chart 3") diff = float(wacca_data[str(key)]["DifficultyInfernoLv"]) designer = wacca_data[str(key)]["NotesDesignerInferno"] if diff > 0: - self.data.static.put_music(self.version, song_id, 4, title, artist, bpm, diff, designer, jacket_asset_name) + self.data.static.put_music( + self.version, + song_id, + 4, + title, + artist, + bpm, + diff, + designer, + jacket_asset_name, + ) self.logger.info(f"Read song {song_id} chart 4") - + def check_valid_pair(self, dir: str, file: str) -> bool: - return path.exists(f"{dir}/{file}.uasset") and path.exists(f"{dir}/{file}.uexp") \ No newline at end of file + return path.exists(f"{dir}/{file}.uasset") and path.exists(f"{dir}/{file}.uexp") diff --git a/titles/wacca/reverse.py b/titles/wacca/reverse.py index 100ffb1..f32b0c4 100644 --- a/titles/wacca/reverse.py +++ b/titles/wacca/reverse.py @@ -9,6 +9,7 @@ from titles.wacca.const import WaccaConstants from titles.wacca.handlers import * + class WaccaReverse(WaccaLilyR): def __init__(self, cfg: CoreConfig, game_cfg: WaccaConfig) -> None: super().__init__(cfg, game_cfg) @@ -46,12 +47,12 @@ class WaccaReverse(WaccaLilyR): (310006, 0), ] - def handle_user_status_login_request(self, data: Dict)-> Dict: + def handle_user_status_login_request(self, data: Dict) -> Dict: resp = super().handle_user_status_login_request(data) resp["params"].append([]) return resp - def handle_user_status_getDetail_request(self, data: Dict)-> Dict: + def handle_user_status_getDetail_request(self, data: Dict) -> Dict: req = UserStatusGetDetailRequest(data) resp = UserStatusGetDetailResponseV4() @@ -79,15 +80,23 @@ class WaccaReverse(WaccaLilyR): if profile["vip_expire_time"] is None: resp.userStatus.vipExpireTime = 0 - + else: - resp.userStatus.vipExpireTime = int(profile["vip_expire_time"].timestamp()) - + resp.userStatus.vipExpireTime = int(profile["vip_expire_time"].timestamp()) + if profile["always_vip"] or self.game_config.mods.always_vip: - resp.userStatus.vipExpireTime = int((self.srvtime + timedelta(days=31)).timestamp()) + resp.userStatus.vipExpireTime = int( + (self.srvtime + timedelta(days=31)).timestamp() + ) resp.songUpdateTime = int(profile["last_login_date"].timestamp()) - resp.lastSongInfo = LastSongDetail(profile["last_song_id"],profile["last_song_difficulty"],profile["last_folder_order"],profile["last_folder_id"],profile["last_song_order"]) + resp.lastSongInfo = LastSongDetail( + profile["last_song_id"], + profile["last_song_difficulty"], + profile["last_folder_order"], + profile["last_folder_id"], + profile["last_song_order"], + ) resp.songPlayStatus = [resp.lastSongInfo.lastSongId, 1] resp.userStatus.userId = profile["id"] @@ -100,42 +109,57 @@ class WaccaReverse(WaccaLilyR): resp.userStatus.loginDays = profile["login_count_days"] resp.userStatus.loginConsecutiveDays = profile["login_count_days_consec"] resp.userStatus.loginsToday = profile["login_count_today"] - resp.userStatus.rating = profile['rating'] + resp.userStatus.rating = profile["rating"] if self.game_config.mods.infinite_wp: resp.userStatus.wp = 999999 for fav in profile_favorites: resp.favorites.append(fav["song_id"]) - + if profile["friend_view_1"] is not None: pass if profile["friend_view_2"] is not None: pass if profile["friend_view_3"] is not None: pass - - resp.seasonalPlayModeCounts.append(PlayModeCounts(self.season, 1, profile["playcount_single"])) - resp.seasonalPlayModeCounts.append(PlayModeCounts(self.season, 2, profile["playcount_multi_vs"])) - resp.seasonalPlayModeCounts.append(PlayModeCounts(self.season, 3, profile["playcount_multi_coop"])) - resp.seasonalPlayModeCounts.append(PlayModeCounts(self.season, 4, profile["playcount_stageup"])) - + + resp.seasonalPlayModeCounts.append( + PlayModeCounts(self.season, 1, profile["playcount_single"]) + ) + resp.seasonalPlayModeCounts.append( + PlayModeCounts(self.season, 2, profile["playcount_multi_vs"]) + ) + resp.seasonalPlayModeCounts.append( + PlayModeCounts(self.season, 3, profile["playcount_multi_coop"]) + ) + resp.seasonalPlayModeCounts.append( + PlayModeCounts(self.season, 4, profile["playcount_stageup"]) + ) + for opt in profile_options: resp.options.append(UserOption(opt["opt_id"], opt["value"])) - + if profile_bingo is not None: resp.bingoStatus = BingoDetail(profile_bingo["page_number"]) for x in profile_bingo["page_progress"]: resp.bingoStatus.pageStatus.append(BingoPageStatus(x[0], x[1], x[2])) - + for gate in self.game_config.gates.enabled_gates: added_gate = False for user_gate in profile_gates: if user_gate["gate_id"] == gate: - - resp.gateInfo.append(GateDetailV2(user_gate["gate_id"],user_gate["page"],user_gate["progress"], - user_gate["loops"],int(user_gate["last_used"].timestamp()),user_gate["mission_flag"])) + resp.gateInfo.append( + GateDetailV2( + user_gate["gate_id"], + user_gate["page"], + user_gate["progress"], + user_gate["loops"], + int(user_gate["last_used"].timestamp()), + user_gate["mission_flag"], + ) + ) resp.seasonInfo.cumulativeGatePts += user_gate["total_points"] @@ -144,14 +168,18 @@ class WaccaReverse(WaccaLilyR): if not added_gate: resp.gateInfo.append(GateDetailV2(gate)) - + for unlock in profile_song_unlocks: for x in range(1, unlock["highest_difficulty"] + 1): - resp.userItems.songUnlocks.append(SongUnlock(unlock["song_id"], x, 0, int(unlock["acquire_date"].timestamp()))) - + resp.userItems.songUnlocks.append( + SongUnlock( + unlock["song_id"], x, 0, int(unlock["acquire_date"].timestamp()) + ) + ) + for song in profile_scores: resp.seasonInfo.cumulativeScore += song["score"] - + clear_cts = SongDetailClearCounts( song["play_ct"], song["clear_ct"], @@ -161,13 +189,23 @@ class WaccaReverse(WaccaLilyR): ) grade_cts = SongDetailGradeCountsV2( - song["grade_d_ct"], song["grade_c_ct"], song["grade_b_ct"], song["grade_a_ct"], song["grade_aa_ct"], - song["grade_aaa_ct"], song["grade_s_ct"], song["grade_ss_ct"], song["grade_sss_ct"], - song["grade_master_ct"], song["grade_sp_ct"], song["grade_ssp_ct"], song["grade_sssp_ct"] + song["grade_d_ct"], + song["grade_c_ct"], + song["grade_b_ct"], + song["grade_a_ct"], + song["grade_aa_ct"], + song["grade_aaa_ct"], + song["grade_s_ct"], + song["grade_ss_ct"], + song["grade_sss_ct"], + song["grade_master_ct"], + song["grade_sp_ct"], + song["grade_ssp_ct"], + song["grade_sssp_ct"], ) deets = BestScoreDetailV2(song["song_id"], song["chart_id"]) - deets.clearCounts = clear_cts + deets.clearCounts = clear_cts deets.clearCountsSeason = clear_cts deets.gradeCounts = grade_cts deets.score = song["score"] @@ -176,9 +214,16 @@ class WaccaReverse(WaccaLilyR): deets.rating = song["rating"] resp.scores.append(deets) - + for trophy in profile_trophies: - resp.userItems.trophies.append(TrophyItem(trophy["trophy_id"], trophy["season"], trophy["progress"], trophy["badge_type"])) + resp.userItems.trophies.append( + TrophyItem( + trophy["trophy_id"], + trophy["season"], + trophy["progress"], + trophy["badge_type"], + ) + ) if self.game_config.mods.infinite_tickets: for x in range(5): @@ -190,30 +235,48 @@ class WaccaReverse(WaccaLilyR): else: expire = int(ticket["expire_date"].timestamp()) - resp.userItems.tickets.append(TicketItem(ticket["id"], ticket["ticket_id"], expire)) + resp.userItems.tickets.append( + TicketItem(ticket["id"], ticket["ticket_id"], expire) + ) if profile_items: for item in profile_items: try: - if item["type"] == WaccaConstants.ITEM_TYPES["icon"]: - resp.userItems.icons.append(IconItem(item["item_id"], 1, item["use_count"], int(item["acquire_date"].timestamp()))) + resp.userItems.icons.append( + IconItem( + item["item_id"], + 1, + item["use_count"], + int(item["acquire_date"].timestamp()), + ) + ) elif item["type"] == WaccaConstants.ITEM_TYPES["navigator"]: - resp.userItems.navigators.append(NavigatorItem(item["item_id"], 1, int(item["acquire_date"].timestamp()), item["use_count"], item["use_count"])) + resp.userItems.navigators.append( + NavigatorItem( + item["item_id"], + 1, + int(item["acquire_date"].timestamp()), + item["use_count"], + item["use_count"], + ) + ) else: - itm_send = GenericItemSend(item["item_id"], 1, int(item["acquire_date"].timestamp())) + itm_send = GenericItemSend( + item["item_id"], 1, int(item["acquire_date"].timestamp()) + ) if item["type"] == WaccaConstants.ITEM_TYPES["title"]: resp.userItems.titles.append(itm_send) - + elif item["type"] == WaccaConstants.ITEM_TYPES["user_plate"]: resp.userItems.plates.append(itm_send) elif item["type"] == WaccaConstants.ITEM_TYPES["touch_effect"]: resp.userItems.touchEffect.append(itm_send) - + elif item["type"] == WaccaConstants.ITEM_TYPES["note_color"]: resp.userItems.noteColors.append(itm_send) @@ -221,7 +284,9 @@ class WaccaReverse(WaccaLilyR): resp.userItems.noteSounds.append(itm_send) except: - self.logger.error(f"{__name__} Failed to load item {item['item_id']} for user {profile['user']}") + self.logger.error( + f"{__name__} Failed to load item {item['item_id']} for user {profile['user']}" + ) resp.seasonInfo.level = profile["xp"] resp.seasonInfo.wpObtained = profile["wp_total"] @@ -234,12 +299,15 @@ class WaccaReverse(WaccaLilyR): return resp.make() - def handle_user_status_create_request(self, data: Dict)-> Dict: + def handle_user_status_create_request(self, data: Dict) -> Dict: req = UserStatusCreateRequest(data) resp = super().handle_user_status_create_request(data) - - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 310001) # Added reverse - self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 310002) # Added reverse - - return resp + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 310001 + ) # Added reverse + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 310002 + ) # Added reverse + + return resp diff --git a/titles/wacca/s.py b/titles/wacca/s.py index d4c2881..4b1e997 100644 --- a/titles/wacca/s.py +++ b/titles/wacca/s.py @@ -9,6 +9,7 @@ from titles.wacca.const import WaccaConstants from titles.wacca.handlers import * + class WaccaS(WaccaBase): allowed_stages = [ (1513, 13), @@ -25,11 +26,11 @@ class WaccaS(WaccaBase): (1512, 2), (1511, 1), ] - + def __init__(self, cfg: CoreConfig, game_cfg: WaccaConfig) -> None: super().__init__(cfg, game_cfg) self.version = WaccaConstants.VER_WACCA_S - + def handle_advertise_GetNews_request(self, data: Dict) -> Dict: resp = GetNewsResponseV2() return resp.make() diff --git a/titles/wacca/schema/__init__.py b/titles/wacca/schema/__init__.py index 1addd87..2ccb661 100644 --- a/titles/wacca/schema/__init__.py +++ b/titles/wacca/schema/__init__.py @@ -3,4 +3,4 @@ from titles.wacca.schema.score import WaccaScoreData from titles.wacca.schema.item import WaccaItemData from titles.wacca.schema.static import WaccaStaticData -__all__ = ["WaccaProfileData", "WaccaScoreData", "WaccaItemData", "WaccaStaticData"] \ No newline at end of file +__all__ = ["WaccaProfileData", "WaccaScoreData", "WaccaItemData", "WaccaStaticData"] diff --git a/titles/wacca/schema/item.py b/titles/wacca/schema/item.py index 76e901f..2341afa 100644 --- a/titles/wacca/schema/item.py +++ b/titles/wacca/schema/item.py @@ -12,132 +12,158 @@ item = Table( "wacca_item", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("item_id", Integer, nullable=False), - Column("type", Integer, nullable=False), + Column("type", Integer, nullable=False), Column("acquire_date", TIMESTAMP, nullable=False, server_default=func.now()), Column("use_count", Integer, server_default="0"), UniqueConstraint("user", "item_id", "type", name="wacca_item_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) ticket = Table( "wacca_ticket", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("ticket_id", Integer, nullable=False), Column("acquire_date", TIMESTAMP, nullable=False, server_default=func.now()), Column("expire_date", TIMESTAMP), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) song_unlock = Table( "wacca_song_unlock", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("song_id", Integer, nullable=False), - Column("highest_difficulty", Integer, nullable=False), + Column("highest_difficulty", Integer, nullable=False), Column("acquire_date", TIMESTAMP, nullable=False, server_default=func.now()), UniqueConstraint("user", "song_id", name="wacca_song_unlock_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) trophy = Table( "wacca_trophy", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("trophy_id", Integer, nullable=False), Column("season", Integer, nullable=False), Column("progress", Integer, nullable=False, server_default="0"), Column("badge_type", Integer, nullable=False, server_default="0"), UniqueConstraint("user", "trophy_id", "season", name="wacca_trophy_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) -class WaccaItemData(BaseData): + +class WaccaItemData(BaseData): def get_song_unlocks(self, user_id: int) -> Optional[List[Row]]: sql = song_unlock.select(song_unlock.c.user == user_id) result = self.execute(sql) - if result is None: return None - + if result is None: + return None + return result.fetchall() def unlock_song(self, user_id: int, song_id: int, difficulty: int) -> Optional[int]: sql = insert(song_unlock).values( - user=user_id, - song_id=song_id, - highest_difficulty=difficulty + user=user_id, song_id=song_id, highest_difficulty=difficulty ) conflict = sql.on_duplicate_key_update( highest_difficulty=case( - (song_unlock.c.highest_difficulty >= difficulty, song_unlock.c.highest_difficulty), + ( + song_unlock.c.highest_difficulty >= difficulty, + song_unlock.c.highest_difficulty, + ), (song_unlock.c.highest_difficulty < difficulty, difficulty), ) ) result = self.execute(conflict) if result is None: - self.logger.error(f"{__name__} failed to unlock song! user: {user_id}, song_id: {song_id}, difficulty: {difficulty}") + self.logger.error( + f"{__name__} failed to unlock song! user: {user_id}, song_id: {song_id}, difficulty: {difficulty}" + ) return None - + return result.lastrowid def put_item(self, user_id: int, item_type: int, item_id: int) -> Optional[int]: sql = insert(item).values( - user = user_id, - item_id = item_id, - type = item_type, + user=user_id, + item_id=item_id, + type=item_type, ) - conflict = sql.on_duplicate_key_update( - use_count = item.c.use_count + 1 - ) + conflict = sql.on_duplicate_key_update(use_count=item.c.use_count + 1) result = self.execute(conflict) if result is None: - self.logger.error(f"{__name__} failed to insert item! user: {user_id}, item_id: {item_id}, item_type: {item_type}") + self.logger.error( + f"{__name__} failed to insert item! user: {user_id}, item_id: {item_id}, item_type: {item_type}" + ) return None - + return result.lastrowid - - def get_items(self, user_id: int, item_type: int = None, item_id: int = None) -> Optional[List[Row]]: + + def get_items( + self, user_id: int, item_type: int = None, item_id: int = None + ) -> Optional[List[Row]]: """ A catch-all item lookup given a profile and option item type and ID specifiers """ sql = item.select( - and_(item.c.user == user_id, - item.c.type == item_type if item_type is not None else True, - item.c.item_id == item_id if item_id is not None else True) - ) - - result = self.execute(sql) - if result is None: return None - return result.fetchall() - - def get_tickets(self, user_id: int) -> Optional[List[Row]]: - sql = select(ticket).where(ticket.c.user == user_id) - - result = self.execute(sql) - if result is None: return None - return result.fetchall() - - def add_ticket(self, user_id: int, ticket_id: int) -> None: - sql = insert(ticket).values( - user = user_id, - ticket_id = ticket_id + and_( + item.c.user == user_id, + item.c.type == item_type if item_type is not None else True, + item.c.item_id == item_id if item_id is not None else True, + ) ) result = self.execute(sql) if result is None: - self.logger.error(f"add_ticket: Failed to insert wacca ticket! user_id: {user_id} ticket_id {ticket_id}") + return None + return result.fetchall() + + def get_tickets(self, user_id: int) -> Optional[List[Row]]: + sql = select(ticket).where(ticket.c.user == user_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def add_ticket(self, user_id: int, ticket_id: int) -> None: + sql = insert(ticket).values(user=user_id, ticket_id=ticket_id) + + result = self.execute(sql) + if result is None: + self.logger.error( + f"add_ticket: Failed to insert wacca ticket! user_id: {user_id} ticket_id {ticket_id}" + ) return None return result.lastrowid - + def spend_ticket(self, id: int) -> None: sql = delete(ticket).where(ticket.c.id == id) @@ -146,32 +172,36 @@ class WaccaItemData(BaseData): self.logger.warn(f"Failed to delete ticket id {id}") return None - def get_trophies(self, user_id: int, season: int = None) -> Optional[List[Row]]: + def get_trophies(self, user_id: int, season: int = None) -> Optional[List[Row]]: if season is None: sql = select(trophy).where(trophy.c.user == user_id) else: - sql = select(trophy).where(and_(trophy.c.user == user_id, trophy.c.season == season)) - + sql = select(trophy).where( + and_(trophy.c.user == user_id, trophy.c.season == season) + ) + result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - - def update_trophy(self, user_id: int, trophy_id: int, season: int, progress: int, badge_type: int) -> Optional[int]: + + def update_trophy( + self, user_id: int, trophy_id: int, season: int, progress: int, badge_type: int + ) -> Optional[int]: sql = insert(trophy).values( - user = user_id, - trophy_id = trophy_id, - season = season, - progress = progress, - badge_type = badge_type + user=user_id, + trophy_id=trophy_id, + season=season, + progress=progress, + badge_type=badge_type, ) - conflict = sql.on_duplicate_key_update( - progress = progress - ) + conflict = sql.on_duplicate_key_update(progress=progress) result = self.execute(conflict) if result is None: - self.logger.error(f"update_trophy: Failed to insert wacca trophy! user_id: {user_id} trophy_id: {trophy_id} progress {progress}") + self.logger.error( + f"update_trophy: Failed to insert wacca trophy! user_id: {user_id} trophy_id: {trophy_id} progress {progress}" + ) return None return result.lastrowid - diff --git a/titles/wacca/schema/profile.py b/titles/wacca/schema/profile.py index c2a15f6..27111be 100644 --- a/titles/wacca/schema/profile.py +++ b/titles/wacca/schema/profile.py @@ -12,7 +12,11 @@ profile = Table( "wacca_profile", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("version", Integer), Column("username", String(8), nullable=False), Column("xp", Integer, server_default="0"), @@ -20,7 +24,7 @@ profile = Table( Column("wp_total", Integer, server_default="0"), Column("wp_spent", Integer, server_default="0"), Column("dan_type", Integer, server_default="0"), - Column("dan_level", Integer, server_default="0"), + Column("dan_level", Integer, server_default="0"), Column("title_0", Integer, server_default="0"), Column("title_1", Integer, server_default="0"), Column("title_2", Integer, server_default="0"), @@ -48,14 +52,18 @@ profile = Table( Column("last_login_date", TIMESTAMP, server_default=func.now()), Column("gate_tutorial_flags", JSON), UniqueConstraint("user", "version", name="wacca_profile_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) option = Table( "wacca_option", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("opt_id", Integer, nullable=False), Column("value", Integer, nullable=False), UniqueConstraint("user", "opt_id", name="wacca_option_uk"), @@ -64,38 +72,59 @@ option = Table( bingo = Table( "wacca_bingo", metadata, - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + primary_key=True, + nullable=False, + ), Column("page_number", Integer, nullable=False), Column("page_progress", JSON, nullable=False), UniqueConstraint("user", "page_number", name="wacca_bingo_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) friend = Table( "wacca_friend", metadata, - Column("profile_sender", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column("profile_reciever", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "profile_sender", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column( + "profile_reciever", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("is_accepted", Boolean, server_default="0"), - PrimaryKeyConstraint('profile_sender', 'profile_reciever', name='arcade_owner_pk'), - mysql_charset='utf8mb4' + PrimaryKeyConstraint("profile_sender", "profile_reciever", name="arcade_owner_pk"), + mysql_charset="utf8mb4", ) favorite = Table( "wacca_favorite_song", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("song_id", Integer, nullable=False), UniqueConstraint("user", "song_id", name="wacca_favorite_song_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) gate = Table( "wacca_gate", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("gate_id", Integer, nullable=False), Column("page", Integer, nullable=False, server_default="0"), Column("progress", Integer, nullable=False, server_default="0"), @@ -106,68 +135,87 @@ gate = Table( UniqueConstraint("user", "gate_id", name="wacca_gate_uk"), ) + class WaccaProfileData(BaseData): - def create_profile(self, aime_id: int, username: str, version: int) -> Optional[int]: + def create_profile( + self, aime_id: int, username: str, version: int + ) -> Optional[int]: """ Given a game version, aime id, and username, create a profile and return it's ID """ - sql = insert(profile).values( - user=aime_id, - username=username, - version=version - ) + sql = insert(profile).values(user=aime_id, username=username, version=version) - conflict = sql.on_duplicate_key_update( - username = sql.inserted.username - ) + conflict = sql.on_duplicate_key_update(username=sql.inserted.username) result = self.execute(conflict) if result is None: - self.logger.error(f"{__name__} Failed to insert wacca profile! aime id: {aime_id} username: {username}") + self.logger.error( + f"{__name__} Failed to insert wacca profile! aime id: {aime_id} username: {username}" + ) return None return result.lastrowid - - def update_profile_playtype(self, profile_id: int, play_type: int, game_version: str) -> None: + + def update_profile_playtype( + self, profile_id: int, play_type: int, game_version: str + ) -> None: sql = profile.update(profile.c.id == profile_id).values( - playcount_single = profile.c.playcount_single + 1 if play_type == 1 else profile.c.playcount_single, - - playcount_multi_vs = profile.c.playcount_multi_vs + 1 if play_type == 2 else profile.c.playcount_multi_vs, - - playcount_multi_coop = profile.c.playcount_multi_coop + 1 if play_type == 3 else profile.c.playcount_multi_coop, - - playcount_stageup = profile.c.playcount_stageup + 1 if play_type == 4 else profile.c.playcount_stageup, - - last_game_ver = game_version, - ) - - result = self.execute(sql) - if result is None: - self.logger.error(f"update_profile: failed to update profile! profile: {profile_id}") - return None - - def update_profile_lastplayed(self, profile_id: int, last_song_id: int, last_song_difficulty: int, last_folder_order: int, - last_folder_id: int, last_song_order: int) -> None: - sql = profile.update(profile.c.id == profile_id).values( - last_song_id = last_song_id, - last_song_difficulty = last_song_difficulty, - last_folder_order = last_folder_order, - last_folder_id = last_folder_id, - last_song_order = last_song_order - ) - result = self.execute(sql) - if result is None: - self.logger.error(f"update_profile_lastplayed: failed to update profile! profile: {profile_id}") - return None - - def update_profile_dan(self, profile_id: int, dan_level: int, dan_type: int) -> Optional[int]: - sql = profile.update(profile.c.id == profile_id).values( - dan_level = dan_level, - dan_type = dan_type + playcount_single=profile.c.playcount_single + 1 + if play_type == 1 + else profile.c.playcount_single, + playcount_multi_vs=profile.c.playcount_multi_vs + 1 + if play_type == 2 + else profile.c.playcount_multi_vs, + playcount_multi_coop=profile.c.playcount_multi_coop + 1 + if play_type == 3 + else profile.c.playcount_multi_coop, + playcount_stageup=profile.c.playcount_stageup + 1 + if play_type == 4 + else profile.c.playcount_stageup, + last_game_ver=game_version, ) result = self.execute(sql) if result is None: - self.logger.warn(f"update_profile_dan: Failed to update! profile {profile_id}") + self.logger.error( + f"update_profile: failed to update profile! profile: {profile_id}" + ) + return None + + def update_profile_lastplayed( + self, + profile_id: int, + last_song_id: int, + last_song_difficulty: int, + last_folder_order: int, + last_folder_id: int, + last_song_order: int, + ) -> None: + sql = profile.update(profile.c.id == profile_id).values( + last_song_id=last_song_id, + last_song_difficulty=last_song_difficulty, + last_folder_order=last_folder_order, + last_folder_id=last_folder_id, + last_song_order=last_song_order, + ) + result = self.execute(sql) + if result is None: + self.logger.error( + f"update_profile_lastplayed: failed to update profile! profile: {profile_id}" + ) + return None + + def update_profile_dan( + self, profile_id: int, dan_level: int, dan_type: int + ) -> Optional[int]: + sql = profile.update(profile.c.id == profile_id).values( + dan_level=dan_level, dan_type=dan_type + ) + + result = self.execute(sql) + if result is None: + self.logger.warn( + f"update_profile_dan: Failed to update! profile {profile_id}" + ) return None return result.lastrowid @@ -180,11 +228,14 @@ class WaccaProfileData(BaseData): elif profile_id > 0: sql = profile.select(profile.c.id == profile_id) else: - self.logger.error(f"get_profile: Bad arguments!! profile_id {profile_id} aime_id {aime_id}") + self.logger.error( + f"get_profile: Bad arguments!! profile_id {profile_id} aime_id {aime_id}" + ) return None - + result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() def get_options(self, user_id: int, option_id: int = None) -> Optional[List[Row]]: @@ -192,71 +243,83 @@ class WaccaProfileData(BaseData): Get a specific user option for a profile, or all of them if none specified """ sql = option.select( - and_(option.c.user == user_id, - option.c.opt_id == option_id if option_id is not None else True) + and_( + option.c.user == user_id, + option.c.opt_id == option_id if option_id is not None else True, + ) ) - + result = self.execute(sql) - if result is None: return None + if result is None: + return None if option_id is not None: return result.fetchone() else: return result.fetchall() - - def update_option(self, user_id: int, option_id: int, value: int) -> Optional[int]: - sql = insert(option).values( - user = user_id, - opt_id = option_id, - value = value - ) - conflict = sql.on_duplicate_key_update( - value = value - ) + def update_option(self, user_id: int, option_id: int, value: int) -> Optional[int]: + sql = insert(option).values(user=user_id, opt_id=option_id, value=value) + + conflict = sql.on_duplicate_key_update(value=value) result = self.execute(conflict) if result is None: - self.logger.error(f"{__name__} failed to insert option! profile: {user_id}, option: {option_id}, value: {value}") + self.logger.error( + f"{__name__} failed to insert option! profile: {user_id}, option: {option_id}, value: {value}" + ) return None - + return result.lastrowid - + def add_favorite_song(self, user_id: int, song_id: int) -> Optional[int]: - sql = favorite.insert().values( - user=user_id, - song_id=song_id - ) + sql = favorite.insert().values(user=user_id, song_id=song_id) result = self.execute(sql) - if result is None: - self.logger.error(f"{__name__} failed to insert favorite! profile: {user_id}, song_id: {song_id}") + if result is None: + self.logger.error( + f"{__name__} failed to insert favorite! profile: {user_id}, song_id: {song_id}" + ) return None return result.lastrowid def remove_favorite_song(self, user_id: int, song_id: int) -> None: - sql = favorite.delete(and_(favorite.c.user == user_id, favorite.c.song_id == song_id)) + sql = favorite.delete( + and_(favorite.c.user == user_id, favorite.c.song_id == song_id) + ) result = self.execute(sql) - if result is None: - self.logger.error(f"{__name__} failed to remove favorite! profile: {user_id}, song_id: {song_id}") + if result is None: + self.logger.error( + f"{__name__} failed to remove favorite! profile: {user_id}, song_id: {song_id}" + ) return None def get_favorite_songs(self, user_id: int) -> Optional[List[Row]]: sql = favorite.select(favorite.c.user == user_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - + def get_gates(self, user_id: int) -> Optional[List[Row]]: sql = select(gate).where(gate.c.user == user_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - - def update_gate(self, user_id: int, gate_id: int, page: int, progress: int, loop: int, mission_flag: int, - total_points: int) -> Optional[int]: + + def update_gate( + self, + user_id: int, + gate_id: int, + page: int, + progress: int, + loop: int, + mission_flag: int, + total_points: int, + ) -> Optional[int]: sql = insert(gate).values( user=user_id, gate_id=gate_id, @@ -264,7 +327,7 @@ class WaccaProfileData(BaseData): progress=progress, loops=loop, mission_flag=mission_flag, - total_points=total_points + total_points=total_points, ) conflict = sql.on_duplicate_key_update( @@ -276,16 +339,19 @@ class WaccaProfileData(BaseData): ) result = self.execute(conflict) - if result is None: - self.logger.error(f"{__name__} failed to update gate! user: {user_id}, gate_id: {gate_id}") + if result is None: + self.logger.error( + f"{__name__} failed to update gate! user: {user_id}, gate_id: {gate_id}" + ) return None return result.lastrowid - + def get_friends(self, user_id: int) -> Optional[List[Row]]: sql = friend.select(friend.c.profile_sender == user_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() def profile_to_aime_user(self, profile_id: int) -> Optional[int]: @@ -293,136 +359,159 @@ class WaccaProfileData(BaseData): result = self.execute(sql) if result is None: - self.logger.info(f"profile_to_aime_user: No user found for profile {profile_id}") + self.logger.info( + f"profile_to_aime_user: No user found for profile {profile_id}" + ) return None this_profile = result.fetchone() if this_profile is None: - self.logger.info(f"profile_to_aime_user: No user found for profile {profile_id}") + self.logger.info( + f"profile_to_aime_user: No user found for profile {profile_id}" + ) return None - return this_profile['user'] - - def session_login(self, profile_id: int, is_new_day: bool, is_consec_day: bool) -> None: + return this_profile["user"] + + def session_login( + self, profile_id: int, is_new_day: bool, is_consec_day: bool + ) -> None: # TODO: Reset consec days counter sql = profile.update(profile.c.id == profile_id).values( - login_count = profile.c.login_count + 1, - login_count_consec = profile.c.login_count_consec + 1, - login_count_days = profile.c.login_count_days + 1 if is_new_day else profile.c.login_count_days, - login_count_days_consec = profile.c.login_count_days_consec + 1 if is_new_day and is_consec_day else profile.c.login_count_days_consec, - login_count_today = 1 if is_new_day else profile.c.login_count_today + 1, - last_login_date = func.now() - ) - - result = self.execute(sql) - if result is None: - self.logger.error(f"session_login: failed to update profile! profile: {profile_id}") - return None - - def session_logout(self, profile_id: int) -> None: - sql = profile.update(profile.c.id == id).values( - login_count_consec = 0 - ) - - result = self.execute(sql) - if result is None: - self.logger.error(f"{__name__} failed to update profile! profile: {profile_id}") - return None - - def add_xp(self, profile_id: int, xp: int) -> None: - sql = profile.update(profile.c.id == profile_id).values( - xp = profile.c.xp + xp + login_count=profile.c.login_count + 1, + login_count_consec=profile.c.login_count_consec + 1, + login_count_days=profile.c.login_count_days + 1 + if is_new_day + else profile.c.login_count_days, + login_count_days_consec=profile.c.login_count_days_consec + 1 + if is_new_day and is_consec_day + else profile.c.login_count_days_consec, + login_count_today=1 if is_new_day else profile.c.login_count_today + 1, + last_login_date=func.now(), ) result = self.execute(sql) if result is None: - self.logger.error(f"add_xp: Failed to update profile! profile_id {profile_id} xp {xp}") + self.logger.error( + f"session_login: failed to update profile! profile: {profile_id}" + ) + return None + + def session_logout(self, profile_id: int) -> None: + sql = profile.update(profile.c.id == id).values(login_count_consec=0) + + result = self.execute(sql) + if result is None: + self.logger.error( + f"{__name__} failed to update profile! profile: {profile_id}" + ) + return None + + def add_xp(self, profile_id: int, xp: int) -> None: + sql = profile.update(profile.c.id == profile_id).values(xp=profile.c.xp + xp) + + result = self.execute(sql) + if result is None: + self.logger.error( + f"add_xp: Failed to update profile! profile_id {profile_id} xp {xp}" + ) return None def add_wp(self, profile_id: int, wp: int) -> None: sql = profile.update(profile.c.id == profile_id).values( - wp = profile.c.wp + wp, - wp_total = profile.c.wp_total + wp, + wp=profile.c.wp + wp, + wp_total=profile.c.wp_total + wp, ) result = self.execute(sql) if result is None: - self.logger.error(f"add_wp: Failed to update profile! profile_id {profile_id} wp {wp}") + self.logger.error( + f"add_wp: Failed to update profile! profile_id {profile_id} wp {wp}" + ) return None def spend_wp(self, profile_id: int, wp: int) -> None: sql = profile.update(profile.c.id == profile_id).values( - wp = profile.c.wp - wp, - wp_spent = profile.c.wp_spent + wp, + wp=profile.c.wp - wp, + wp_spent=profile.c.wp_spent + wp, ) result = self.execute(sql) if result is None: - self.logger.error(f"spend_wp: Failed to update profile! profile_id {profile_id} wp {wp}") + self.logger.error( + f"spend_wp: Failed to update profile! profile_id {profile_id} wp {wp}" + ) return None - + def activate_vip(self, profile_id: int, expire_time) -> None: sql = profile.update(profile.c.id == profile_id).values( - vip_expire_time = expire_time + vip_expire_time=expire_time ) result = self.execute(sql) if result is None: - self.logger.error(f"activate_vip: Failed to update profile! profile_id {profile_id} expire_time {expire_time}") + self.logger.error( + f"activate_vip: Failed to update profile! profile_id {profile_id} expire_time {expire_time}" + ) return None def update_user_rating(self, profile_id: int, new_rating: int) -> None: - sql = profile.update(profile.c.id == profile_id).values( - rating = new_rating - ) + sql = profile.update(profile.c.id == profile_id).values(rating=new_rating) result = self.execute(sql) if result is None: - self.logger.error(f"update_user_rating: Failed to update profile! profile_id {profile_id} new_rating {new_rating}") + self.logger.error( + f"update_user_rating: Failed to update profile! profile_id {profile_id} new_rating {new_rating}" + ) return None def update_bingo(self, aime_id: int, page: int, progress: int) -> Optional[int]: sql = insert(bingo).values( - user=aime_id, - page_number=page, - page_progress=progress - ) - - conflict = sql.on_duplicate_key_update( - page_number=page, - page_progress=progress + user=aime_id, page_number=page, page_progress=progress ) + conflict = sql.on_duplicate_key_update(page_number=page, page_progress=progress) + result = self.execute(conflict) - if result is None: + if result is None: self.logger.error(f"put_bingo: failed to update! aime_id: {aime_id}") return None return result.lastrowid - + def get_bingo(self, aime_id: int) -> Optional[List[Row]]: - sql = select(bingo).where(bingo.c.user==aime_id) + sql = select(bingo).where(bingo.c.user == aime_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() - + def get_bingo_page(self, aime_id: int, page: Dict) -> Optional[List[Row]]: - sql = select(bingo).where(and_(bingo.c.user==aime_id, bingo.c.page_number==page)) + sql = select(bingo).where( + and_(bingo.c.user == aime_id, bingo.c.page_number == page) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() def update_vip_time(self, profile_id: int, time_left) -> None: - sql = profile.update(profile.c.id == profile_id).values(vip_expire_time = time_left) + sql = profile.update(profile.c.id == profile_id).values( + vip_expire_time=time_left + ) result = self.execute(sql) if result is None: self.logger.error(f"Failed to update VIP time for profile {profile_id}") - + def update_tutorial_flags(self, profile_id: int, flags: Dict) -> None: - sql = profile.update(profile.c.id == profile_id).values(gate_tutorial_flags = flags) - + sql = profile.update(profile.c.id == profile_id).values( + gate_tutorial_flags=flags + ) + result = self.execute(sql) if result is None: - self.logger.error(f"Failed to update tutorial flags for profile {profile_id}") \ No newline at end of file + self.logger.error( + f"Failed to update tutorial flags for profile {profile_id}" + ) diff --git a/titles/wacca/schema/score.py b/titles/wacca/schema/score.py index 4b0c26c..250740f 100644 --- a/titles/wacca/schema/score.py +++ b/titles/wacca/schema/score.py @@ -13,9 +13,13 @@ best_score = Table( "wacca_score_best", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("song_id", Integer), - Column("chart_id", Integer), + Column("chart_id", Integer), Column("score", Integer), Column("play_ct", Integer), Column("clear_ct", Integer), @@ -39,14 +43,18 @@ best_score = Table( Column("lowest_miss_ct", Integer), Column("rating", Integer), UniqueConstraint("user", "song_id", "chart_id", name="wacca_score_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) playlog = Table( "wacca_score_playlog", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("song_id", Integer), Column("chart_id", Integer), Column("score", Integer), @@ -61,14 +69,18 @@ playlog = Table( Column("late_ct", Integer), Column("season", Integer), Column("date_scored", TIMESTAMP, server_default=func.now()), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) stageup = Table( "wacca_score_stageup", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), Column("version", Integer), Column("stage_id", Integer), Column("clear_status", Integer), @@ -77,19 +89,29 @@ stageup = Table( Column("song2_score", Integer), Column("song3_score", Integer), Column("play_ct", Integer, server_default="1"), - UniqueConstraint("user", "stage_id", name="wacca_score_stageup_uk"), - mysql_charset='utf8mb4' + UniqueConstraint("user", "stage_id", name="wacca_score_stageup_uk"), + mysql_charset="utf8mb4", ) + class WaccaScoreData(BaseData): - def put_best_score(self, user_id: int, song_id: int, chart_id: int, score: int, clear: List[int], - grade: List[int], best_combo: int, lowest_miss_ct: int) -> Optional[int]: + def put_best_score( + self, + user_id: int, + song_id: int, + chart_id: int, + score: int, + clear: List[int], + grade: List[int], + best_combo: int, + lowest_miss_ct: int, + ) -> Optional[int]: """ Update the user's best score for a chart """ while len(grade) < 13: grade.append(0) - + sql = insert(best_score).values( user=user_id, song_id=song_id, @@ -115,7 +137,7 @@ class WaccaScoreData(BaseData): grade_sssp_ct=grade[12], best_combo=best_combo, lowest_miss_ct=lowest_miss_ct, - rating=0 + rating=0, ) conflict = sql.on_duplicate_key_update( @@ -144,13 +166,30 @@ class WaccaScoreData(BaseData): result = self.execute(conflict) if result is None: - self.logger.error(f"{__name__}: failed to insert best score! profile: {user_id}, song: {song_id}, chart: {chart_id}") + self.logger.error( + f"{__name__}: failed to insert best score! profile: {user_id}, song: {song_id}, chart: {chart_id}" + ) return None - + return result.lastrowid - - def put_playlog(self, user_id: int, song_id: int, chart_id: int, this_score: int, clear: int, grade: int, max_combo: int, - marv_ct: int, great_ct: int, good_ct: int, miss_ct: int, fast_ct: int, late_ct: int, season: int) -> Optional[int]: + + def put_playlog( + self, + user_id: int, + song_id: int, + chart_id: int, + this_score: int, + clear: int, + grade: int, + max_combo: int, + marv_ct: int, + great_ct: int, + good_ct: int, + miss_ct: int, + fast_ct: int, + late_ct: int, + season: int, + ) -> Optional[int]: """ Add an entry to the user's play log """ @@ -168,85 +207,112 @@ class WaccaScoreData(BaseData): miss_ct=miss_ct, fast_ct=fast_ct, late_ct=late_ct, - season=season + season=season, ) result = self.execute(sql) if result is None: - self.logger.error(f"{__name__} failed to insert playlog! profile: {user_id}, song: {song_id}, chart: {chart_id}") + self.logger.error( + f"{__name__} failed to insert playlog! profile: {user_id}, song: {song_id}, chart: {chart_id}" + ) return None - + return result.lastrowid - def get_best_score(self, user_id: int, song_id: int, chart_id: int) -> Optional[Row]: + def get_best_score( + self, user_id: int, song_id: int, chart_id: int + ) -> Optional[Row]: sql = best_score.select( - and_(best_score.c.user == user_id, best_score.c.song_id == song_id, best_score.c.chart_id == chart_id) + and_( + best_score.c.user == user_id, + best_score.c.song_id == song_id, + best_score.c.chart_id == chart_id, + ) ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() def get_best_scores(self, user_id: int) -> Optional[List[Row]]: - sql = best_score.select( - best_score.c.user == user_id - ) + sql = best_score.select(best_score.c.user == user_id) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - def update_song_rating(self, user_id: int, song_id: int, chart_id: int, new_rating: int) -> None: + def update_song_rating( + self, user_id: int, song_id: int, chart_id: int, new_rating: int + ) -> None: sql = best_score.update( and_( - best_score.c.user == user_id, - best_score.c.song_id == song_id, - best_score.c.chart_id == chart_id - )).values( - rating = new_rating + best_score.c.user == user_id, + best_score.c.song_id == song_id, + best_score.c.chart_id == chart_id, ) + ).values(rating=new_rating) result = self.execute(sql) - if result is None: - self.logger.error(f"update_song_rating: failed to update rating! user_id: {user_id} song_id: {song_id} chart_id {chart_id} new_rating {new_rating}") + if result is None: + self.logger.error( + f"update_song_rating: failed to update rating! user_id: {user_id} song_id: {song_id} chart_id {chart_id} new_rating {new_rating}" + ) return None - def put_stageup(self, user_id: int, version: int, stage_id: int, clear_status: int, clear_song_ct: int, score1: int, - score2: int, score3: int) -> Optional[int]: + def put_stageup( + self, + user_id: int, + version: int, + stage_id: int, + clear_status: int, + clear_song_ct: int, + score1: int, + score2: int, + score3: int, + ) -> Optional[int]: sql = insert(stageup).values( - user = user_id, - version = version, - stage_id = stage_id, - clear_status = clear_status, - clear_song_ct = clear_song_ct, - song1_score = score1, - song2_score = score2, - song3_score = score3, + user=user_id, + version=version, + stage_id=stage_id, + clear_status=clear_status, + clear_song_ct=clear_song_ct, + song1_score=score1, + song2_score=score2, + song3_score=score3, ) conflict = sql.on_duplicate_key_update( - clear_status = clear_status, - clear_song_ct = clear_song_ct, - song1_score = score1, - song2_score = score2, - song3_score = score3, - play_ct = stageup.c.play_ct + 1 + clear_status=clear_status, + clear_song_ct=clear_song_ct, + song1_score=score1, + song2_score=score2, + song3_score=score3, + play_ct=stageup.c.play_ct + 1, ) result = self.execute(conflict) if result is None: - self.logger.warn(f"put_stageup: failed to update! user_id: {user_id} version: {version} stage_id: {stage_id}") + self.logger.warn( + f"put_stageup: failed to update! user_id: {user_id} version: {version} stage_id: {stage_id}" + ) return None return result.lastrowid def get_stageup(self, user_id: int, version: int) -> Optional[List[Row]]: - sql = select(stageup).where(and_(stageup.c.user==user_id, stageup.c.version==version)) + sql = select(stageup).where( + and_(stageup.c.user == user_id, stageup.c.version == version) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchall() - - def get_stageup_stage(self, user_id: int, version: int, stage_id: int) -> Optional[Row]: + + def get_stageup_stage( + self, user_id: int, version: int, stage_id: int + ) -> Optional[Row]: sql = select(stageup).where( and_( stageup.c.user == user_id, @@ -256,5 +322,6 @@ class WaccaScoreData(BaseData): ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() diff --git a/titles/wacca/schema/static.py b/titles/wacca/schema/static.py index a5e8b41..d09a440 100644 --- a/titles/wacca/schema/static.py +++ b/titles/wacca/schema/static.py @@ -23,46 +23,62 @@ music = Table( Column("chartDesigner", String(255)), Column("jacketFile", String(255)), UniqueConstraint("version", "songId", "chartId", name="wacca_static_music_uk"), - mysql_charset='utf8mb4' + mysql_charset="utf8mb4", ) + class WaccaStaticData(BaseData): - def put_music(self, version: int, song_id: int, chart_id: int, title: str, artist: str, bpm: str, - difficulty: float, chart_designer: str, jacket: str) -> Optional[int]: + def put_music( + self, + version: int, + song_id: int, + chart_id: int, + title: str, + artist: str, + bpm: str, + difficulty: float, + chart_designer: str, + jacket: str, + ) -> Optional[int]: sql = insert(music).values( - version = version, - songId = song_id, - chartId = chart_id, - title = title, - artist = artist, - bpm = bpm, - difficulty = difficulty, - chartDesigner = chart_designer, - jacketFile = jacket + version=version, + songId=song_id, + chartId=chart_id, + title=title, + artist=artist, + bpm=bpm, + difficulty=difficulty, + chartDesigner=chart_designer, + jacketFile=jacket, ) conflict = sql.on_duplicate_key_update( - title = title, - artist = artist, - bpm = bpm, - difficulty = difficulty, - chartDesigner = chart_designer, - jacketFile = jacket + title=title, + artist=artist, + bpm=bpm, + difficulty=difficulty, + chartDesigner=chart_designer, + jacketFile=jacket, ) result = self.execute(conflict) - if result is None: + if result is None: self.logger.warn(f"Failed to insert music {song_id} chart {chart_id}") return None return result.lastrowid - def get_music_chart(self, version: int, song_id: int, chart_id: int) -> Optional[List[Row]]: - sql = select(music).where(and_( - music.c.version == version, - music.c.songId == song_id, - music.c.chartId == chart_id - )) + def get_music_chart( + self, version: int, song_id: int, chart_id: int + ) -> Optional[List[Row]]: + sql = select(music).where( + and_( + music.c.version == version, + music.c.songId == song_id, + music.c.chartId == chart_id, + ) + ) result = self.execute(sql) - if result is None: return None + if result is None: + return None return result.fetchone() From dafc0300507a7a1780225eacba95f11a463902ad Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Thu, 9 Mar 2023 12:02:02 -0500 Subject: [PATCH 29/63] wacca: add .partition() --- titles/wacca/index.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/titles/wacca/index.py b/titles/wacca/index.py index 2a4a0cf..c922fab 100644 --- a/titles/wacca/index.py +++ b/titles/wacca/index.py @@ -94,20 +94,24 @@ class WaccaServlet: version_full = Version(req_json["appVersion"]) except: self.logger.error( - f"Failed to parse request toi {request.uri} -> {request.content.getvalue()}" + f"Failed to parse request to {url_path} -> {request.content.getvalue()}" ) resp = BaseResponse() resp.status = 1 resp.message = "不正なリクエスト エラーです" return end(resp.make()) - url_split = url_path.split("/") - start_req_idx = url_split.index("api") + 1 + if "/api/" in url_path: + func_to_find = ( + "handle_" + url_path.partition("api/")[2].replace("/", "_") + "_request" + ) - func_to_find = "handle_" - for x in range(len(url_split) - start_req_idx): - func_to_find += f"{url_split[x + start_req_idx]}_" - func_to_find += "request" + else: + self.logger.error(f"Malformed url {url_path}") + resp = BaseResponse() + resp.status = 1 + resp.message = "Bad URL" + return end(resp.make()) ver_search = int(version_full) From e9ffd954354a6ce4dce80592f1a8cec898c2f038 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Thu, 9 Mar 2023 12:17:10 -0500 Subject: [PATCH 30/63] implement dict.get() --- core/allnet.py | 32 ++++++++++++++------------------ core/mucha.py | 45 +++++++++++++++++++-------------------------- titles/diva/read.py | 2 +- 3 files changed, 34 insertions(+), 45 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index 4c0c542..c11fe67 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -346,20 +346,16 @@ class AllnetPowerOnRequest: def __init__(self, req: Dict) -> None: if req is None: raise AllnetRequestException("Request processing failed") - self.game_id: str = req["game_id"] if "game_id" in req else "" - self.ver: str = req["ver"] if "ver" in req else "" - self.serial: str = req["serial"] if "serial" in req else "" - self.ip: str = req["ip"] if "ip" in req else "" - self.firm_ver: str = req["firm_ver"] if "firm_ver" in req else "" - self.boot_ver: str = req["boot_ver"] if "boot_ver" in req else "" - self.encode: str = req["encode"] if "encode" in req else "" - - try: - self.hops = int(req["hops"]) if "hops" in req else 0 - self.format_ver = int(req["format_ver"]) if "format_ver" in req else 2 - self.token = int(req["token"]) if "token" in req else 0 - except ValueError as e: - raise AllnetRequestException(f"Failed to parse int: {e}") + self.game_id: str = req.get("game_id", "") + self.ver: str = req.get("ver", "") + self.serial: str = req.get("serial", "") + self.ip: str = req.get("ip", "") + self.firm_ver: str = req.get("firm_ver", "") + self.boot_ver: str = req.get("boot_ver", "") + self.encode: str = req.get("encode", "") + self.hops = int(req.get("hops", "0")) + self.format_ver = int(req.get("hops", "2")) + self.token = int(req.get("hops", "0")) class AllnetPowerOnResponse3: @@ -413,10 +409,10 @@ class AllnetPowerOnResponse2: class AllnetDownloadOrderRequest: def __init__(self, req: Dict) -> None: - self.game_id = req["game_id"] if "game_id" in req else "" - self.ver = req["ver"] if "ver" in req else "" - self.serial = req["serial"] if "serial" in req else "" - self.encode = req["encode"] if "encode" in req else "" + self.game_id = req.get("game_id", "") + self.ver = req.get("ver", "") + self.serial = req.get("serial", "") + self.encode = req.get("encode", "") class AllnetDownloadOrderResponse: diff --git a/core/mucha.py b/core/mucha.py index 1b73b84..e058b12 100644 --- a/core/mucha.py +++ b/core/mucha.py @@ -132,24 +132,19 @@ class MuchaServlet: class MuchaAuthRequest: def __init__(self, request: Dict) -> None: - self.gameVer = ( - "" if "gameVer" not in request else request["gameVer"] - ) # gameCd + boardType + countryCd + version - self.sendDate = ( - "" if "sendDate" not in request else request["sendDate"] - ) # %Y%m%d - self.serialNum = "" if "serialNum" not in request else request["serialNum"] - self.gameCd = "" if "gameCd" not in request else request["gameCd"] - self.boardType = "" if "boardType" not in request else request["boardType"] - self.boardId = "" if "boardId" not in request else request["boardId"] - self.mac = "" if "mac" not in request else request["mac"] - self.placeId = "" if "placeId" not in request else request["placeId"] - self.storeRouterIp = ( - "" if "storeRouterIp" not in request else request["storeRouterIp"] - ) - self.countryCd = "" if "countryCd" not in request else request["countryCd"] - self.useToken = "" if "useToken" not in request else request["useToken"] - self.allToken = "" if "allToken" not in request else request["allToken"] + # gameCd + boardType + countryCd + version + self.gameVer = request.get("gameVer", "") + self.sendDate = request.get("sendDate", "") # %Y%m%d + self.serialNum = request.get("serialNum", "") + self.gameCd = request.get("gameCd", "") + self.boardType = request.get("boardType", "") + self.boardId = request.get("boardId", "") + self.mac = request.get("mac", "") + self.placeId = request.get("placeId", "") + self.storeRouterIp = request.get("storeRouterIp", "") + self.countryCd = request.get("countryCd", "") + self.useToken = request.get("useToken", "") + self.allToken = request.get("allToken", "") class MuchaAuthResponse: @@ -199,14 +194,12 @@ class MuchaAuthResponse: class MuchaUpdateRequest: def __init__(self, request: Dict) -> None: - self.gameVer = "" if "gameVer" not in request else request["gameVer"] - self.gameCd = "" if "gameCd" not in request else request["gameCd"] - self.serialNum = "" if "serialNum" not in request else request["serialNum"] - self.countryCd = "" if "countryCd" not in request else request["countryCd"] - self.placeId = "" if "placeId" not in request else request["placeId"] - self.storeRouterIp = ( - "" if "storeRouterIp" not in request else request["storeRouterIp"] - ) + self.gameVer = request.get("gameVer", "") + self.gameCd = request.get("gameCd", "") + self.serialNum = request.get("serialNum", "") + self.countryCd = request.get("countryCd", "") + self.placeId = request.get("placeId", "") + self.storeRouterIp = request.get("storeRouterIp", "") class MuchaUpdateResponse: diff --git a/titles/diva/read.py b/titles/diva/read.py index 9c409ef..2eeacdc 100644 --- a/titles/diva/read.py +++ b/titles/diva/read.py @@ -298,6 +298,6 @@ class DivaReader(BaseReader): tree[key] = ( value if len(vector) == 1 - else self.add_branch(tree[key] if key in tree else {}, vector[1:], value) + else self.add_branch(tree.get(key, {}), vector[1:], value) ) return tree From 2033bc897f43c4dd37f225fb3a124df59afdf91e Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Thu, 9 Mar 2023 13:46:28 -0500 Subject: [PATCH 31/63] pokken: fix a crash that happens when the game is disabled --- titles/pokken/index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/pokken/index.py b/titles/pokken/index.py index bbf1204..047989b 100644 --- a/titles/pokken/index.py +++ b/titles/pokken/index.py @@ -90,7 +90,7 @@ class PokkenServlet(resource.Resource): ) if not game_cfg.server.enable: - return (False, "", "") + return (False, "") return (True, "PKFN") From b076a9a9df2dd37c0add5e2d6b4c4da5ae294235 Mon Sep 17 00:00:00 2001 From: Raymonf Date: Wed, 8 Mar 2023 16:27:25 -0500 Subject: [PATCH 32/63] chuni: Add 'handle_remove_token_api_request' for event mode Not sure if `handle_delete_token_api_request` is used in other versions, so it's duplicated to be safe. --- titles/chuni/new.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/titles/chuni/new.py b/titles/chuni/new.py index 03ad305..0d74ba6 100644 --- a/titles/chuni/new.py +++ b/titles/chuni/new.py @@ -60,6 +60,9 @@ class ChuniNew(ChuniBase): "isAou": "false", } + def handle_remove_token_api_request(self, data: Dict) -> Dict: + return { "returnCode": "1" } + def handle_delete_token_api_request(self, data: Dict) -> Dict: return {"returnCode": "1"} From a088dd82dec2b44e86989665663bf1c54d86b1af Mon Sep 17 00:00:00 2001 From: Midorica Date: Thu, 9 Mar 2023 16:59:50 -0500 Subject: [PATCH 33/63] fixing allnet power on response --- core/allnet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index c11fe67..0a63e89 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -354,8 +354,8 @@ class AllnetPowerOnRequest: self.boot_ver: str = req.get("boot_ver", "") self.encode: str = req.get("encode", "") self.hops = int(req.get("hops", "0")) - self.format_ver = int(req.get("hops", "2")) - self.token = int(req.get("hops", "0")) + self.format_ver = int(req.get("format_ver", "2")) + self.token = int(req.get("token", "0")) class AllnetPowerOnResponse3: From 57ecff641a7b7cb1ffee31281b069bc34a5b5b7e Mon Sep 17 00:00:00 2001 From: Midorica Date: Thu, 9 Mar 2023 17:09:37 -0500 Subject: [PATCH 34/63] fixing Card Maker get_game_connect_api response --- titles/cm/cm136.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/titles/cm/cm136.py b/titles/cm/cm136.py index 5feeca4..fb5b6b5 100644 --- a/titles/cm/cm136.py +++ b/titles/cm/cm136.py @@ -26,6 +26,8 @@ class CardMaker136(CardMakerBase): ret["gameConnectList"][0]["titleUri"] = f"{uri}/SDHD/205/" ret["gameConnectList"][1]["titleUri"] = f"{uri}/SDEZ/125/" ret["gameConnectList"][2]["titleUri"] = f"{uri}/SDDT/135/" + + return ret def handle_get_game_setting_api_request(self, data: Dict) -> Dict: ret = super().handle_get_game_setting_api_request(data) From f283dd10a9143af125594439af40c8c7f4c717ac Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 9 Mar 2023 19:03:30 -0500 Subject: [PATCH 35/63] index.py: Fix log directory creation --- index.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/index.py b/index.py index d30d51f..fba6d11 100644 --- a/index.py +++ b/index.py @@ -163,6 +163,9 @@ if __name__ == "__main__": if path.exists(f"{args.config}/core.yaml"): cfg.update(yaml.safe_load(open(f"{args.config}/core.yaml"))) + if not path.exists(cfg.server.log_dir): + mkdir(cfg.server.log_dir) + logger = logging.getLogger("core") log_fmt_str = "[%(asctime)s] Core | %(levelname)s | %(message)s" log_fmt = logging.Formatter(log_fmt_str) @@ -182,9 +185,6 @@ if __name__ == "__main__": logger.setLevel(log_lv) coloredlogs.install(level=log_lv, logger=logger, fmt=log_fmt_str) - if not path.exists(cfg.server.log_dir): - mkdir(cfg.server.log_dir) - if not access(cfg.server.log_dir, W_OK): logger.error( f"Log directory {cfg.server.log_dir} NOT writable, please check permissions" From 2dd84bbe3e0da8db5f6f606b8a4d868153441180 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 10 Mar 2023 20:31:29 -0500 Subject: [PATCH 36/63] pokken: fix mucha and allnet info, fix allnet 2.00 format requests --- core/allnet.py | 12 +++--------- core/mucha.py | 2 +- index.py | 2 +- titles/pokken/index.py | 22 ++++++++++++---------- 4 files changed, 17 insertions(+), 21 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index 0a63e89..1b561b8 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -78,13 +78,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.token - or not req.serial - or not req.ip - ): + if not req.game_id or not req.ver or not req.serial or not req.ip: raise AllnetRequestException( f"Bad auth request params from {request_ip} - {vars(req)}" ) @@ -94,7 +88,7 @@ class AllnetServlet: self.logger.error(e) return b"" - if req.format_ver == 3: + if req.format_ver == "3": resp = AllnetPowerOnResponse3(req.token) else: resp = AllnetPowerOnResponse2() @@ -354,7 +348,7 @@ class AllnetPowerOnRequest: self.boot_ver: str = req.get("boot_ver", "") self.encode: str = req.get("encode", "") self.hops = int(req.get("hops", "0")) - self.format_ver = int(req.get("format_ver", "2")) + self.format_ver = req.get("format_ver", "2") self.token = int(req.get("token", "0")) diff --git a/core/mucha.py b/core/mucha.py index e058b12..9505282 100644 --- a/core/mucha.py +++ b/core/mucha.py @@ -71,7 +71,7 @@ class MuchaServlet: # TODO: Decrypt S/N resp = MuchaAuthResponse( - f"{self.config.mucha.hostname}{':' + self.config.mucha.port if self.config.server.is_develop else ''}" + f"{self.config.mucha.hostname}{':' + str(self.config.mucha.port) if self.config.server.is_develop else ''}" ) self.logger.debug(f"Mucha response {vars(resp)}") diff --git a/index.py b/index.py index fba6d11..5b8d92b 100644 --- a/index.py +++ b/index.py @@ -73,7 +73,7 @@ class HttpDispatcher(resource.Resource): "mucha_updatacheck", "/mucha/updatacheck.do", controller="mucha", - action="handle_updatacheck", + action="handle_updatecheck", conditions=dict(method=["POST"]), ) diff --git a/titles/pokken/index.py b/titles/pokken/index.py index 047989b..6efbf90 100644 --- a/titles/pokken/index.py +++ b/titles/pokken/index.py @@ -65,17 +65,17 @@ class PokkenServlet(resource.Resource): if not game_cfg.server.enable: return (False, "", "") - if core_cfg.server.is_develop: - return ( - True, - f"https://{game_cfg.server.hostname}:{game_cfg.server.port}/{game_code}/$v/", - f"{game_cfg.server.hostname}:{game_cfg.server.port}/", - ) + # if core_cfg.server.is_develop: + # return ( + # True, + # f"https://{game_cfg.server.hostname}:{game_cfg.server.port}/{game_code}/$v/", + # f"{game_cfg.server.hostname}:{game_cfg.server.port}/", + # ) return ( True, - f"https://{game_cfg.server.hostname}/{game_code}/$v/", - f"{game_cfg.server.hostname}/", + f"https://{game_cfg.server.hostname}:443/{game_code}/$v/", + f"{game_cfg.server.hostname}/SDAK/$v/", ) @classmethod @@ -92,7 +92,7 @@ class PokkenServlet(resource.Resource): if not game_cfg.server.enable: return (False, "") - return (True, "PKFN") + return (True, "PKF2") def setup(self): """ @@ -143,7 +143,9 @@ class PokkenServlet(resource.Resource): self.logger.warn(f"{e} {content}") return b"" - endpoint = jackal_pb2.MessageType(pokken_request.type).name.lower() + endpoint = jackal_pb2.MessageType.DESCRIPTOR.values_by_number[ + pokken_request.type + ].name.lower() self.logger.info(f"{endpoint} request") From edb9ec1971b8becb59e70c78996a9e86ac500a00 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 11 Mar 2023 20:02:58 -0500 Subject: [PATCH 37/63] pokken: add responnse debug logging --- titles/pokken/index.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/titles/pokken/index.py b/titles/pokken/index.py index 6efbf90..d03d619 100644 --- a/titles/pokken/index.py +++ b/titles/pokken/index.py @@ -153,4 +153,7 @@ class PokkenServlet(resource.Resource): if handler is None: self.logger.warn(f"No handler found for message type {endpoint}") return self.base.handle_noop(pokken_request) - return handler(pokken_request) + + ret = handler(pokken_request) + self.logger.debug(f"Response: {ret}") + return ret From a9f49e8d5dd0a304be4ab276a85b50d84a6214bd Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 11 Mar 2023 20:17:05 -0500 Subject: [PATCH 38/63] pokken: remove setup(), ssl config options, change matching uri --- titles/pokken/base.py | 2 +- titles/pokken/config.py | 13 ------------- titles/pokken/index.py | 43 +++++------------------------------------ 3 files changed, 6 insertions(+), 52 deletions(-) diff --git a/titles/pokken/base.py b/titles/pokken/base.py index f1f9eb3..ca4f0b1 100644 --- a/titles/pokken/base.py +++ b/titles/pokken/base.py @@ -38,7 +38,7 @@ class PokkenBase: "MatchingServer": { "host": f"https://{self.game_cfg.server.hostname}", "port": self.game_cfg.server.port_matching, - "url": "/matching", + "url": "SDAK/100/matching", }, "StunServer": { "addr": self.game_cfg.server.hostname, diff --git a/titles/pokken/config.py b/titles/pokken/config.py index 3907838..608bbd7 100644 --- a/titles/pokken/config.py +++ b/titles/pokken/config.py @@ -55,19 +55,6 @@ class PokkenServerConfig: self.__config, "pokken", "server", "port_admission", default=9004 ) - @property - def ssl_cert(self) -> str: - return CoreConfig.get_config_field( - self.__config, "pokken", "server", "ssl_cert", default="cert/pokken.crt" - ) - - @property - def ssl_key(self) -> str: - return CoreConfig.get_config_field( - self.__config, "pokken", "server", "ssl_key", default="cert/pokken.key" - ) - - class PokkenConfig(dict): def __init__(self) -> None: self.server = PokkenServerConfig(self) diff --git a/titles/pokken/index.py b/titles/pokken/index.py index d03d619..8d8ef4c 100644 --- a/titles/pokken/index.py +++ b/titles/pokken/index.py @@ -65,16 +65,9 @@ class PokkenServlet(resource.Resource): if not game_cfg.server.enable: return (False, "", "") - # if core_cfg.server.is_develop: - # return ( - # True, - # f"https://{game_cfg.server.hostname}:{game_cfg.server.port}/{game_code}/$v/", - # f"{game_cfg.server.hostname}:{game_cfg.server.port}/", - # ) - return ( True, - f"https://{game_cfg.server.hostname}:443/{game_code}/$v/", + f"https://{game_cfg.server.hostname}:{game_cfg.server.port}/{game_code}/$v/", f"{game_cfg.server.hostname}/SDAK/$v/", ) @@ -94,42 +87,16 @@ class PokkenServlet(resource.Resource): return (True, "PKF2") - def setup(self): - """ - There's currently no point in having this server on because Twisted - won't play ball with both the fact that it's TLSv1.1, and because the - types of certs that pokken will accept are too flimsy for Twisted - so it will throw a fit. Currently leaving this here in case a bypass - is discovered in the future, but it's unlikly. For now, just use NGINX. - """ - if self.game_cfg.server.enable and self.core_cfg.server.is_develop: - key_exists = path.exists(self.game_cfg.server.ssl_key) - cert_exists = path.exists(self.game_cfg.server.ssl_cert) - - if key_exists and cert_exists: - endpoints.serverFromString( - reactor, - f"ssl:{self.game_cfg.server.port}" - f":interface={self.core_cfg.server.listen_address}:privateKey={self.game_cfg.server.ssl_key}:" - f"certKey={self.game_cfg.server.ssl_cert}", - ).listen(server.Site(self)) - - self.logger.info( - f"Pokken title server ready on port {self.game_cfg.server.port}" - ) - - else: - self.logger.error( - f"Could not find cert at {self.game_cfg.server.ssl_key} or key at {self.game_cfg.server.ssl_cert}, Pokken not running." - ) + def setup(self) -> None: + # TODO: Setup matching, stun, turn and admission servers + pass def render_POST( self, request: Request, version: int = 0, endpoints: str = "" ) -> bytes: - if endpoints == "": - endpoints = request.uri.decode() if endpoints.startswith("/matching"): self.logger.info("Matching request") + self.logger.debug(request.content) content = request.content.getvalue() if content == b"": From eb51fc315c37178063d9f8d23df13f6836ef5d38 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 11 Mar 2023 20:22:56 -0500 Subject: [PATCH 39/63] pokken: update example config --- example_config/pokken.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/example_config/pokken.yaml b/example_config/pokken.yaml index e465ceb..48db520 100644 --- a/example_config/pokken.yaml +++ b/example_config/pokken.yaml @@ -6,6 +6,4 @@ server: port_matching: 9001 port_stun: 9002 port_turn: 9003 - port_admission: 9004 - ssl_cert: cert/pokken.crt - ssl_key: cert/pokken.key \ No newline at end of file + port_admission: 9004 \ No newline at end of file From e4b7809e34f6db12120cc976672e46c138875339 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 11 Mar 2023 23:42:12 -0500 Subject: [PATCH 40/63] pokken: add matching server skeleton --- example_config/pokken.yaml | 7 +++--- titles/pokken/base.py | 39 +++++++++++++++++++++++++++--- titles/pokken/config.py | 12 +++------- titles/pokken/index.py | 49 +++++++++++++++++++++++++++++++------- 4 files changed, 83 insertions(+), 24 deletions(-) diff --git a/example_config/pokken.yaml b/example_config/pokken.yaml index 48db520..7400060 100644 --- a/example_config/pokken.yaml +++ b/example_config/pokken.yaml @@ -3,7 +3,6 @@ server: enable: True loglevel: "info" port: 9000 - port_matching: 9001 - port_stun: 9002 - port_turn: 9003 - port_admission: 9004 \ No newline at end of file + port_stun: 9001 + port_turn: 9002 + port_admission: 9003 \ No newline at end of file diff --git a/titles/pokken/base.py b/titles/pokken/base.py index ca4f0b1..1fccab8 100644 --- a/titles/pokken/base.py +++ b/titles/pokken/base.py @@ -1,6 +1,6 @@ from datetime import datetime, timedelta import json -from typing import Any +from typing import Any, Dict from core.config import CoreConfig from titles.pokken.config import PokkenConfig @@ -37,8 +37,8 @@ class PokkenBase: biwa_setting = { "MatchingServer": { "host": f"https://{self.game_cfg.server.hostname}", - "port": self.game_cfg.server.port_matching, - "url": "SDAK/100/matching", + "port": self.game_cfg.server.port, + "url": "/SDAK/100/matching", }, "StunServer": { "addr": self.game_cfg.server.hostname, @@ -108,3 +108,36 @@ class PokkenBase: res.load_client_settings.CopyFrom(settings) return res.SerializeToString() + + def handle_load_ranking(self, request: jackal_pb2.CheckDiagnosisRequestData) -> bytes: + res = jackal_pb2.Response() + res.result = 1 + res.type = jackal_pb2.MessageType.LOAD_RANKING + ranking = jackal_pb2.LoadRankingResponseData() + + ranking.ranking_id = 1 + ranking.ranking_start = 0 + ranking.ranking_end = 1 + ranking.event_end = True + ranking.modify_date = int(datetime.now().timestamp() / 1000) + res.load_ranking.CopyFrom(ranking) + + def handle_matching_noop(self, data: Dict = {}, client_ip: str = "127.0.0.1") -> Dict: + return {} + + def handle_matching_start_matching(self, data: Dict = {}, client_ip: str = "127.0.0.1") -> Dict: + return {} + + def handle_matching_is_matching(self, data: Dict = {}, client_ip: str = "127.0.0.1") -> Dict: + """ + "sessionId":"12345678", + "A":{ + "pcb_id": data["data"]["must"]["pcb_id"], + "gip": client_ip + }, + "list":[] + """ + return {} + + def handle_matching_stop_matching(self, data: Dict = {}, client_ip: str = "127.0.0.1") -> Dict: + return {} \ No newline at end of file diff --git a/titles/pokken/config.py b/titles/pokken/config.py index 608bbd7..b53fc86 100644 --- a/titles/pokken/config.py +++ b/titles/pokken/config.py @@ -31,28 +31,22 @@ class PokkenServerConfig: self.__config, "pokken", "server", "port", default=9000 ) - @property - def port_matching(self) -> int: - return CoreConfig.get_config_field( - self.__config, "pokken", "server", "port_matching", default=9001 - ) - @property def port_stun(self) -> int: return CoreConfig.get_config_field( - self.__config, "pokken", "server", "port_stun", default=9002 + self.__config, "pokken", "server", "port_stun", default=9001 ) @property def port_turn(self) -> int: return CoreConfig.get_config_field( - self.__config, "pokken", "server", "port_turn", default=9003 + self.__config, "pokken", "server", "port_turn", default=9002 ) @property def port_admission(self) -> int: return CoreConfig.get_config_field( - self.__config, "pokken", "server", "port_admission", default=9004 + self.__config, "pokken", "server", "port_admission", default=9003 ) class PokkenConfig(dict): diff --git a/titles/pokken/index.py b/titles/pokken/index.py index 8d8ef4c..9bcd90b 100644 --- a/titles/pokken/index.py +++ b/titles/pokken/index.py @@ -1,11 +1,12 @@ from typing import Tuple from twisted.web.http import Request -from twisted.web import resource, server -from twisted.internet import reactor, endpoints +from twisted.web import resource +import json, ast +from datetime import datetime import yaml import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler -from titles.pokken.proto import jackal_pb2 +import inflection from os import path from google.protobuf.message import DecodeError @@ -13,6 +14,7 @@ from core.config import CoreConfig from titles.pokken.config import PokkenConfig from titles.pokken.base import PokkenBase from titles.pokken.const import PokkenConstants +from titles.pokken.proto import jackal_pb2 class PokkenServlet(resource.Resource): @@ -88,15 +90,14 @@ class PokkenServlet(resource.Resource): return (True, "PKF2") def setup(self) -> None: - # TODO: Setup matching, stun, turn and admission servers + # TODO: Setup stun, turn (UDP) and admission (WSS) servers pass def render_POST( self, request: Request, version: int = 0, endpoints: str = "" ) -> bytes: - if endpoints.startswith("/matching"): - self.logger.info("Matching request") - self.logger.debug(request.content) + if endpoints == "matching": + return self.handle_matching(request) content = request.content.getvalue() if content == b"": @@ -122,5 +123,37 @@ class PokkenServlet(resource.Resource): return self.base.handle_noop(pokken_request) ret = handler(pokken_request) - self.logger.debug(f"Response: {ret}") + #self.logger.debug(f"Response: {ret}") return ret + + def handle_matching(self, request: Request) -> bytes: + content = request.content.getvalue() + client_ip = request.getClientAddress().host + + if content is None or content == b"": + self.logger.info("Empty matching request") + return json.dumps(self.base.handle_matching_noop()).encode() + + json_content = ast.literal_eval(content.decode().replace('null', 'None').replace('true', 'True').replace('false', 'False')) + self.logger.info(f"Matching {json_content['call']} request") + self.logger.debug(json_content) + + handler = getattr(self.base, f"handle_matching_{inflection.underscore(json_content['call'])}", None) + if handler is None: + self.logger.warn(f"No handler found for message type {json_content['call']}") + return json.dumps(self.base.handle_matching_noop()).encode() + + ret = handler(json_content, client_ip) + + if ret is None: + ret = {} + if "result" not in ret: + ret["result"] = "true" + if "data" not in ret: + ret["data"] = {} + if "timestamp" not in ret: + ret["timestamp"] = int(datetime.now().timestamp() * 1000) + + self.logger.debug(f"Response {ret}") + + return json.dumps(ret).encode() From ea14f105d597b0150146bc12445a89a8a969fa4b Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 12 Mar 2023 00:26:48 -0500 Subject: [PATCH 41/63] database: skip games that lack a database member --- core/data/database.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/data/database.py b/core/data/database.py index 1af5c08..715bac6 100644 --- a/core/data/database.py +++ b/core/data/database.py @@ -71,7 +71,8 @@ class Data: games = Utils.get_all_titles() for game_dir, game_mod in games.items(): try: - title_db = game_mod.database(self.config) + if hasattr(game_mod, "database"): + game_mod.database(self.config) metadata.create_all(self.__engine.connect()) self.base.set_schema_ver( @@ -109,7 +110,8 @@ class Data: mod = importlib.import_module(f"titles.{dir}") try: - title_db = mod.database(self.config) + if hasattr(mod, "database"): + mod.database(self.config) metadata.drop_all(self.__engine.connect()) except Exception as e: From 18a95f52137d34c2f93aceb5ffdcfca1e59e0ddd Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 12 Mar 2023 01:00:51 -0500 Subject: [PATCH 42/63] add get_ip_addr util function for servers behind proxies --- core/allnet.py | 8 ++++---- core/frontend.py | 5 ++--- core/mucha.py | 10 +++++++--- core/utils.py | 5 +++++ index.py | 8 ++++++-- read.py | 3 +-- titles/chuni/index.py | 5 +++-- titles/pokken/index.py | 4 ++-- titles/wacca/index.py | 5 +++-- 9 files changed, 33 insertions(+), 20 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index 1b561b8..36ea633 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -12,8 +12,8 @@ from Crypto.Signature import PKCS1_v1_5 from time import strptime from core.config import CoreConfig -from core.data import Data from core.utils import Utils +from core.data import Data from core.const import * @@ -69,7 +69,7 @@ class AllnetServlet: ) def handle_poweron(self, request: Request, _: Dict): - request_ip = request.getClientAddress().host + request_ip = Utils.get_ip_addr(request) try: req_dict = self.allnet_req_to_dict(request.content.getvalue()) if req_dict is None: @@ -162,7 +162,7 @@ class AllnetServlet: return self.dict_to_http_form_string([vars(resp)]).encode("utf-8") def handle_dlorder(self, request: Request, _: Dict): - request_ip = request.getClientAddress().host + request_ip = Utils.get_ip_addr(request) try: req_dict = self.allnet_req_to_dict(request.content.getvalue()) if req_dict is None: @@ -255,7 +255,7 @@ class AllnetServlet: return resp_str.encode("utf-8") def handle_naomitest(self, request: Request, _: Dict) -> bytes: - self.logger.info(f"Ping from {request.getClientAddress().host}") + self.logger.info(f"Ping from {Utils.get_ip_addr(request)}") return b"naomi ok" def kvp_to_dict(self, kvp: List[str]) -> List[Dict[str, Any]]: diff --git a/core/frontend.py b/core/frontend.py index 2fdefae..e230681 100644 --- a/core/frontend.py +++ b/core/frontend.py @@ -10,9 +10,8 @@ from twisted.python.components import registerAdapter import jinja2 import bcrypt -from core.config import CoreConfig +from core import CoreConfig, Utils from core.data import Data -from core.utils import Utils class IUserSession(Interface): @@ -143,7 +142,7 @@ class FE_Gate(FE_Base): def render_POST(self, request: Request): uri = request.uri.decode() - ip = request.getClientAddress().host + ip = Utils.get_ip_addr(request) if uri == "/gate/gate.login": access_code: str = request.args[b"access_code"][0].decode() diff --git a/core/mucha.py b/core/mucha.py index 9505282..4d2388b 100644 --- a/core/mucha.py +++ b/core/mucha.py @@ -6,7 +6,7 @@ from twisted.web.http import Request from datetime import datetime import pytz -from core.config import CoreConfig +from core import CoreConfig from core.utils import Utils @@ -52,6 +52,8 @@ class MuchaServlet: def handle_boardauth(self, request: Request, _: Dict) -> bytes: req_dict = self.mucha_preprocess(request.content.getvalue()) + client_ip = Utils.get_ip_addr(request) + if req_dict is None: self.logger.error( f"Error processing mucha request {request.content.getvalue()}" @@ -61,7 +63,7 @@ class MuchaServlet: req = MuchaAuthRequest(req_dict) self.logger.debug(f"Mucha request {vars(req)}") self.logger.info( - f"Boardauth request from {request.getClientAddress().host} for {req.gameVer}" + f"Boardauth request from {client_ip} for {req.gameVer}" ) if req.gameCd not in self.mucha_registry: @@ -80,6 +82,8 @@ class MuchaServlet: def handle_updatecheck(self, request: Request, _: Dict) -> bytes: req_dict = self.mucha_preprocess(request.content.getvalue()) + client_ip = Utils.get_ip_addr(request) + if req_dict is None: self.logger.error( f"Error processing mucha request {request.content.getvalue()}" @@ -89,7 +93,7 @@ class MuchaServlet: req = MuchaUpdateRequest(req_dict) self.logger.debug(f"Mucha request {vars(req)}") self.logger.info( - f"Updatecheck request from {request.getClientAddress().host} for {req.gameVer}" + f"Updatecheck request from {client_ip} for {req.gameVer}" ) if req.gameCd not in self.mucha_registry: diff --git a/core/utils.py b/core/utils.py index 24417bb..d18289e 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,5 +1,6 @@ from typing import Dict, Any from types import ModuleType +from twisted.web.http import Request import logging import importlib from os import walk @@ -21,3 +22,7 @@ class Utils: logging.getLogger("core").error(f"get_all_titles: {dir} - {e}") raise return ret + + @classmethod + def get_ip_addr(cls, req: Request) -> str: + return req.getAllHeaders()[b"x-forwarded-for"].decode() if b"x-forwarded-for" in req.getAllHeaders() else req.getClientAddress().host diff --git a/index.py b/index.py index 5b8d92b..0852d06 100644 --- a/index.py +++ b/index.py @@ -96,9 +96,11 @@ class HttpDispatcher(resource.Resource): def render_GET(self, request: Request) -> bytes: test = self.map_get.match(request.uri.decode()) + client_ip = Utils.get_ip_addr(request) + if test is None: self.logger.debug( - f"Unknown GET endpoint {request.uri.decode()} from {request.getClientAddress().host} to port {request.getHost().port}" + f"Unknown GET endpoint {request.uri.decode()} from {client_ip} to port {request.getHost().port}" ) request.setResponseCode(404) return b"Endpoint not found." @@ -107,9 +109,11 @@ class HttpDispatcher(resource.Resource): def render_POST(self, request: Request) -> bytes: test = self.map_post.match(request.uri.decode()) + client_ip = Utils.get_ip_addr(request) + if test is None: self.logger.debug( - f"Unknown POST endpoint {request.uri.decode()} from {request.getClientAddress().host} to port {request.getHost().port}" + f"Unknown POST endpoint {request.uri.decode()} from {client_ip} to port {request.getHost().port}" ) request.setResponseCode(404) return b"Endpoint not found." diff --git a/read.py b/read.py index 538198a..341c502 100644 --- a/read.py +++ b/read.py @@ -9,8 +9,7 @@ import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler from typing import List, Optional -from core import CoreConfig -from core.utils import Utils +from core import CoreConfig, Utils class BaseReader: diff --git a/titles/chuni/index.py b/titles/chuni/index.py index a8b581e..de01346 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -11,7 +11,7 @@ from Crypto.Util.Padding import pad from os import path from typing import Tuple -from core import CoreConfig +from core import CoreConfig, Utils from titles.chuni.config import ChuniConfig from titles.chuni.const import ChuniConstants from titles.chuni.base import ChuniBase @@ -111,6 +111,7 @@ class ChuniServlet: encrtped = False internal_ver = 0 endpoint = url_split[len(url_split) - 1] + client_ip = Utils.get_ip_addr(request) if version < 105: # 1.0 internal_ver = ChuniConstants.VER_CHUNITHM @@ -179,7 +180,7 @@ class ChuniServlet: req_data = json.loads(unzip) self.logger.info( - f"v{version} {endpoint} request from {request.getClientAddress().host}" + f"v{version} {endpoint} request from {client_ip}" ) self.logger.debug(req_data) diff --git a/titles/pokken/index.py b/titles/pokken/index.py index 9bcd90b..b172cc2 100644 --- a/titles/pokken/index.py +++ b/titles/pokken/index.py @@ -10,7 +10,7 @@ import inflection from os import path from google.protobuf.message import DecodeError -from core.config import CoreConfig +from core import CoreConfig, Utils from titles.pokken.config import PokkenConfig from titles.pokken.base import PokkenBase from titles.pokken.const import PokkenConstants @@ -128,7 +128,7 @@ class PokkenServlet(resource.Resource): def handle_matching(self, request: Request) -> bytes: content = request.content.getvalue() - client_ip = request.getClientAddress().host + client_ip = Utils.get_ip_addr(request) if content is None or content == b"": self.logger.info("Empty matching request") diff --git a/titles/wacca/index.py b/titles/wacca/index.py index c922fab..0b51849 100644 --- a/titles/wacca/index.py +++ b/titles/wacca/index.py @@ -8,7 +8,7 @@ from twisted.web.http import Request from typing import Dict, Tuple from os import path -from core.config import CoreConfig +from core import CoreConfig, Utils from titles.wacca.config import WaccaConfig from titles.wacca.config import WaccaConfig from titles.wacca.const import WaccaConstants @@ -89,6 +89,7 @@ class WaccaServlet: request.responseHeaders.addRawHeader(b"X-Wacca-Hash", hash.hex().encode()) return json.dumps(resp).encode() + client_ip = Utils.get_ip_addr(request) try: req_json = json.loads(request.content.getvalue()) version_full = Version(req_json["appVersion"]) @@ -140,7 +141,7 @@ class WaccaServlet: return end(resp.make()) self.logger.info( - f"v{req_json['appVersion']} {url_path} request from {request.getClientAddress().host} with chipId {req_json['chipId']}" + f"v{req_json['appVersion']} {url_path} request from {client_ip} with chipId {req_json['chipId']}" ) self.logger.debug(req_json) From a97509bb435477268ae7b2575431f27f59da3788 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 12 Mar 2023 01:47:59 -0500 Subject: [PATCH 43/63] add X-Forwarded-For to nginx config --- example_config/nginx_example.conf | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/example_config/nginx_example.conf b/example_config/nginx_example.conf index fe6f7a7..6ffcd9c 100644 --- a/example_config/nginx_example.conf +++ b/example_config/nginx_example.conf @@ -4,6 +4,8 @@ server { server_name naominet.jp; location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass_request_headers on; proxy_pass http://localhost:8000/; } } @@ -14,6 +16,8 @@ server { server_name your.hostname.here; location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass_request_headers on; proxy_pass http://localhost:8080/; } } @@ -75,6 +79,8 @@ 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://localhost:8080/; } } @@ -95,6 +101,8 @@ 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://localhost:8080/SDBT/104/; } } @@ -131,6 +139,8 @@ server { add_header Strict-Transport-Security "max-age=63072000" always; location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass_request_headers on; proxy_pass http://localhost:8090/; } } \ No newline at end of file From 6fa0175baafc98fc8c4d92b2d0c8cbd9373999f9 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 12 Mar 2023 01:59:12 -0500 Subject: [PATCH 44/63] print cleanup, remove unused mucha options --- core/allnet.py | 2 +- core/config.py | 18 ------------------ core/mucha.py | 4 ++-- core/title.py | 2 +- example_config/core.yaml | 3 --- titles/cxb/index.py | 4 ++-- 6 files changed, 6 insertions(+), 27 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index 36ea633..2738c8e 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -65,7 +65,7 @@ class AllnetServlet: self.uri_registry[code] = (uri, host) self.logger.info( - f"Allnet serving {len(self.uri_registry)} games on port {core_cfg.allnet.port}" + f"Serving {len(self.uri_registry)} game codes port {core_cfg.allnet.port}" ) def handle_poweron(self, request: Request, _: Dict): diff --git a/core/config.py b/core/config.py index 383f053..9e152c0 100644 --- a/core/config.py +++ b/core/config.py @@ -267,24 +267,6 @@ class MuchaConfig: self.__config, "core", "mucha", "hostname", default="localhost" ) - @property - def port(self) -> int: - return CoreConfig.get_config_field( - self.__config, "core", "mucha", "port", default=8444 - ) - - @property - def ssl_cert(self) -> str: - return CoreConfig.get_config_field( - self.__config, "core", "mucha", "ssl_cert", default="cert/server.pem" - ) - - @property - def signing_key(self) -> str: - return CoreConfig.get_config_field( - self.__config, "core", "mucha", "signing_key", default="cert/billing.key" - ) - class CoreConfig(dict): def __init__(self) -> None: diff --git a/core/mucha.py b/core/mucha.py index 4d2388b..9dfef03 100644 --- a/core/mucha.py +++ b/core/mucha.py @@ -47,7 +47,7 @@ class MuchaServlet: self.mucha_registry.append(game_cd) self.logger.info( - f"Serving {len(self.mucha_registry)} games on port {self.config.mucha.port}" + f"Serving {len(self.mucha_registry)} games" ) def handle_boardauth(self, request: Request, _: Dict) -> bytes: @@ -73,7 +73,7 @@ class MuchaServlet: # TODO: Decrypt S/N resp = MuchaAuthResponse( - f"{self.config.mucha.hostname}{':' + str(self.config.mucha.port) if self.config.server.is_develop else ''}" + 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)}") diff --git a/core/title.py b/core/title.py index c9580d2..7a0a99b 100644 --- a/core/title.py +++ b/core/title.py @@ -68,7 +68,7 @@ class TitleServlet: self.logger.error(f"{folder} missing game_code or index in __init__.py") self.logger.info( - f"Serving {len(self.title_registry)} game codes on port {core_cfg.title.port}" + 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: diff --git a/example_config/core.yaml b/example_config/core.yaml index 94e89ba..561293c 100644 --- a/example_config/core.yaml +++ b/example_config/core.yaml @@ -48,6 +48,3 @@ mucha: enable: False hostname: "localhost" loglevel: "info" - port: 8444 - ssl_key: "cert/server.key" - ssl_cert: "cert/server.pem" diff --git a/titles/cxb/index.py b/titles/cxb/index.py index d6ab74b..36c762e 100644 --- a/titles/cxb/index.py +++ b/titles/cxb/index.py @@ -98,11 +98,11 @@ class CxbServlet(resource.Resource): ).listen(server.Site(CxbServlet(self.core_cfg, self.cfg_dir))) self.logger.info( - f"Crossbeats title server ready on port {self.game_cfg.server.port} & {self.game_cfg.server.port_secure}" + f"Ready on ports {self.game_cfg.server.port} & {self.game_cfg.server.port_secure}" ) else: self.logger.info( - f"Crossbeats title server ready on port {self.game_cfg.server.port}" + f"Ready on port {self.game_cfg.server.port}" ) def render_POST(self, request: Request): From 65e9ecd58c286cde744075d077c59abf808ecbda Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 12 Mar 2023 14:03:00 -0400 Subject: [PATCH 45/63] wacca: fix crash when enabling frontend with no wacca.yaml file --- titles/wacca/frontend.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/titles/wacca/frontend.py b/titles/wacca/frontend.py index e4f2be0..69ab1ee 100644 --- a/titles/wacca/frontend.py +++ b/titles/wacca/frontend.py @@ -1,6 +1,7 @@ import yaml import jinja2 from twisted.web.http import Request +from os import path from core.frontend import FE_Base from core.config import CoreConfig @@ -16,7 +17,10 @@ class WaccaFrontend(FE_Base): super().__init__(cfg, environment) self.data = WaccaData(cfg) self.game_cfg = WaccaConfig() - self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/wacca.yaml"))) + if path.exists(f"{cfg_dir}/{WaccaConstants.CONFIG_NAME}"): + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{WaccaConstants.CONFIG_NAME}")) + ) self.nav_name = "Wacca" def render_GET(self, request: Request) -> bytes: From fddf2e448a30d206fc42f0fabb4c47150a42fc0c Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 12 Mar 2023 16:30:57 -0400 Subject: [PATCH 46/63] pokken: rearrange logging, fix types --- titles/pokken/base.py | 21 ++++++++++++--------- titles/pokken/index.py | 7 ++++--- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/titles/pokken/base.py b/titles/pokken/base.py index 1fccab8..e64ea40 100644 --- a/titles/pokken/base.py +++ b/titles/pokken/base.py @@ -1,5 +1,5 @@ from datetime import datetime, timedelta -import json +import json, logging from typing import Any, Dict from core.config import CoreConfig @@ -12,6 +12,7 @@ class PokkenBase: self.core_cfg = core_cfg self.game_cfg = game_cfg self.version = 0 + self.logger = logging.getLogger("pokken") def handle_noop(self, request: Any) -> bytes: res = jackal_pb2.Response() @@ -20,20 +21,22 @@ class PokkenBase: return res.SerializeToString() - def handle_ping(self, request: jackal_pb2.PingRequestData) -> bytes: + def handle_ping(self, request: jackal_pb2.Request) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.PING + self.logger.debug(res) return res.SerializeToString() - def handle_register_pcb(self, request: jackal_pb2.RegisterPcbRequestData) -> bytes: + def handle_register_pcb(self, request: jackal_pb2.Request) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.REGISTER_PCB + self.logger.info(f"Register PCB {request.register_pcb.pcb_id}") regist_pcb = jackal_pb2.RegisterPcbResponseData() - regist_pcb.server_time = int(datetime.now().timestamp() / 1000) + regist_pcb.server_time = int(datetime.now().timestamp()) biwa_setting = { "MatchingServer": { "host": f"https://{self.game_cfg.server.hostname}", @@ -60,7 +63,7 @@ class PokkenBase: return res.SerializeToString() - def handle_save_ads(self, request: jackal_pb2.SaveAdsRequestData) -> bytes: + def handle_save_ads(self, request: jackal_pb2.Request) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.SAVE_ADS @@ -68,7 +71,7 @@ class PokkenBase: return res.SerializeToString() def handle_save_client_log( - self, request: jackal_pb2.SaveClientLogRequestData + self, request: jackal_pb2.Request ) -> bytes: res = jackal_pb2.Response() res.result = 1 @@ -77,7 +80,7 @@ class PokkenBase: return res.SerializeToString() def handle_check_diagnosis( - self, request: jackal_pb2.CheckDiagnosisRequestData + self, request: jackal_pb2.Request ) -> bytes: res = jackal_pb2.Response() res.result = 1 @@ -86,7 +89,7 @@ class PokkenBase: return res.SerializeToString() def handle_load_client_settings( - self, request: jackal_pb2.CheckDiagnosisRequestData + self, request: jackal_pb2.Request ) -> bytes: res = jackal_pb2.Response() res.result = 1 @@ -109,7 +112,7 @@ class PokkenBase: return res.SerializeToString() - def handle_load_ranking(self, request: jackal_pb2.CheckDiagnosisRequestData) -> 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 diff --git a/titles/pokken/index.py b/titles/pokken/index.py index b172cc2..42a5a0e 100644 --- a/titles/pokken/index.py +++ b/titles/pokken/index.py @@ -115,15 +115,16 @@ class PokkenServlet(resource.Resource): pokken_request.type ].name.lower() - self.logger.info(f"{endpoint} request") - handler = getattr(self.base, f"handle_{endpoint}", None) if handler is None: self.logger.warn(f"No handler found for message type {endpoint}") return self.base.handle_noop(pokken_request) + self.logger.info(f"{endpoint} request from {Utils.get_ip_addr(request)}") + self.logger.debug(pokken_request) + ret = handler(pokken_request) - #self.logger.debug(f"Response: {ret}") + self.logger.debug(f"Response: {ret}") return ret def handle_matching(self, request: Request) -> bytes: From 346e8989839eeba7f9a4750d7be9b7dcb687886e Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 12 Mar 2023 16:34:20 -0400 Subject: [PATCH 47/63] pokken: remove hanging debug log --- titles/pokken/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/titles/pokken/base.py b/titles/pokken/base.py index e64ea40..6c2bf26 100644 --- a/titles/pokken/base.py +++ b/titles/pokken/base.py @@ -26,7 +26,6 @@ class PokkenBase: res.result = 1 res.type = jackal_pb2.MessageType.PING - self.logger.debug(res) return res.SerializeToString() def handle_register_pcb(self, request: jackal_pb2.Request) -> bytes: From a791142f95f0cb2e7fa724852955777447ea017b Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 12 Mar 2023 22:37:44 -0400 Subject: [PATCH 48/63] database: add check for current_schema_version --- core/data/database.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/data/database.py b/core/data/database.py index 715bac6..0ce039e 100644 --- a/core/data/database.py +++ b/core/data/database.py @@ -71,7 +71,7 @@ class Data: games = Utils.get_all_titles() for game_dir, game_mod in games.items(): try: - if hasattr(game_mod, "database"): + if hasattr(game_mod, "database") and hasattr(game_mod, "current_schema_version"): game_mod.database(self.config) metadata.create_all(self.__engine.connect()) From 2af7751504819d96bf8b6c5e04ac2faee2e0c936 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Wed, 15 Mar 2023 20:03:22 +0000 Subject: [PATCH 49/63] Added support for maimai and Chunithm in Card Maker 1.34/1.35 (#14) Co-authored-by: Dniel97 Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/14 Co-authored-by: Dniel97 Co-committed-by: Dniel97 --- core/data/schema/versions/SDED_1_upgrade.sql | 99 ------ core/data/schema/versions/SDEZ_3_upgrade.sql | 21 ++ docs/game_specific_info.md | 351 ++++++++++++++++++ read.py | 6 +- readme.md | 5 +- titles/chuni/base.py | 8 + titles/chuni/index.py | 2 +- titles/chuni/new.py | 354 ++++++++++++++++++- titles/chuni/newplus.py | 7 + titles/chuni/schema/item.py | 156 ++++++++ titles/chuni/schema/static.py | 163 +++++++++ titles/cm/__init__.py | 2 + titles/cm/{cm136.py => cm135.py} | 8 +- titles/cm/const.py | 4 +- titles/cm/database.py | 8 + titles/cm/index.py | 8 +- titles/cm/read.py | 182 +++++++++- titles/cm/schema/__init__.py | 1 + titles/mai2/__init__.py | 2 +- titles/mai2/base.py | 140 ++++++-- titles/mai2/index.py | 2 +- titles/mai2/schema/item.py | 174 +++++++-- titles/mai2/schema/profile.py | 6 +- titles/mai2/schema/score.py | 2 +- titles/mai2/schema/static.py | 39 ++ titles/mai2/universe.py | 174 +++++++++ titles/mai2/universeplus.py | 11 +- titles/ongeki/base.py | 6 +- titles/ongeki/bright.py | 11 +- titles/ongeki/brightmemory.py | 6 +- titles/ongeki/index.py | 5 +- titles/ongeki/schema/item.py | 2 +- 32 files changed, 1763 insertions(+), 202 deletions(-) delete mode 100644 core/data/schema/versions/SDED_1_upgrade.sql create mode 100644 core/data/schema/versions/SDEZ_3_upgrade.sql create mode 100644 docs/game_specific_info.md rename titles/cm/{cm136.py => cm135.py} (89%) create mode 100644 titles/cm/database.py create mode 100644 titles/cm/schema/__init__.py diff --git a/core/data/schema/versions/SDED_1_upgrade.sql b/core/data/schema/versions/SDED_1_upgrade.sql deleted file mode 100644 index a4d666e..0000000 --- a/core/data/schema/versions/SDED_1_upgrade.sql +++ /dev/null @@ -1,99 +0,0 @@ -CREATE TABLE ongeki_user_gacha ( - id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, - user INT NOT NULL, - gachaId INT NOT NULL, - totalGachaCnt INT DEFAULT 0, - ceilingGachaCnt INT DEFAULT 0, - selectPoint INT DEFAULT 0, - useSelectPoint INT DEFAULT 0, - dailyGachaCnt INT DEFAULT 0, - fiveGachaCnt INT DEFAULT 0, - elevenGachaCnt INT DEFAULT 0, - dailyGachaDate TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT ongeki_user_gacha_uk UNIQUE (user, gachaId), - CONSTRAINT ongeki_user_gacha_ibfk_1 FOREIGN KEY (user) REFERENCES aime_user (id) ON DELETE CASCADE ON UPDATE CASCADE -); - -CREATE TABLE ongeki_user_gacha_supply ( - id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, - user INT NOT NULL, - cardId INT NOT NULL, - CONSTRAINT ongeki_user_gacha_supply_uk UNIQUE (user, cardId), - CONSTRAINT ongeki_user_gacha_supply_ibfk_1 FOREIGN KEY (user) REFERENCES aime_user (id) ON DELETE CASCADE ON UPDATE CASCADE -); - -CREATE TABLE ongeki_static_gachas ( - id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, - version INT NOT NULL, - gachaId INT NOT NULL, - gachaName VARCHAR(255) NOT NULL, - kind INT NOT NULL, - type INT DEFAULT 0, - isCeiling BOOLEAN DEFAULT 0, - maxSelectPoint INT DEFAULT 0, - ceilingCnt INT DEFAULT 10, - changeRateCnt1 INT DEFAULT 0, - changeRateCnt2 INT DEFAULT 0, - startDate TIMESTAMP DEFAULT '2018-01-01 00:00:00.0', - endDate TIMESTAMP DEFAULT '2038-01-01 00:00:00.0', - noticeStartDate TIMESTAMP DEFAULT '2018-01-01 00:00:00.0', - noticeEndDate TIMESTAMP DEFAULT '2038-01-01 00:00:00.0', - convertEndDate TIMESTAMP DEFAULT '2038-01-01 00:00:00.0', - CONSTRAINT ongeki_static_gachas_uk UNIQUE (version, gachaId, gachaName) -); - -CREATE TABLE ongeki_static_gacha_cards ( - id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, - gachaId INT NOT NULL, - cardId INT NOT NULL, - rarity INT NOT NULL, - weight INT DEFAULT 1, - isPickup BOOLEAN DEFAULT 0, - isSelect BOOLEAN DEFAULT 1, - CONSTRAINT ongeki_static_gacha_cards_uk UNIQUE (gachaId, cardId) -); - - -CREATE TABLE ongeki_static_cards ( - id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, - version INT NOT NULL, - cardId INT NOT NULL, - name VARCHAR(255) NOT NULL, - charaId INT NOT NULL, - nickName VARCHAR(255), - school VARCHAR(255) NOT NULL, - attribute VARCHAR(5) NOT NULL, - gakunen VARCHAR(255) NOT NULL, - rarity INT NOT NULL, - levelParam VARCHAR(255) NOT NULL, - skillId INT NOT NULL, - choKaikaSkillId INT NOT NULL, - cardNumber VARCHAR(255), - CONSTRAINT ongeki_static_cards_uk UNIQUE (version, cardId) -) CHARACTER SET utf8mb4; - -CREATE TABLE ongeki_user_print_detail ( - id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, - user INT NOT NULL, - cardId INT NOT NULL, - cardType INT DEFAULT 0, - printDate TIMESTAMP NOT NULL, - serialId VARCHAR(20) NOT NULL, - placeId INT NOT NULL, - clientId VARCHAR(11) NOT NULL, - printerSerialId VARCHAR(20) NOT NULL, - isHolograph BOOLEAN DEFAULT 0, - isAutographed BOOLEAN DEFAULT 0, - printOption1 BOOLEAN DEFAULT 1, - printOption2 BOOLEAN DEFAULT 1, - printOption3 BOOLEAN DEFAULT 1, - printOption4 BOOLEAN DEFAULT 1, - printOption5 BOOLEAN DEFAULT 1, - printOption6 BOOLEAN DEFAULT 1, - printOption7 BOOLEAN DEFAULT 1, - printOption8 BOOLEAN DEFAULT 1, - printOption9 BOOLEAN DEFAULT 1, - printOption10 BOOLEAN DEFAULT 0, - FOREIGN KEY (user) REFERENCES aime_user(id) ON DELETE CASCADE ON UPDATE CASCADE, - CONSTRAINT ongeki_user_print_detail_uk UNIQUE (serialId) -) CHARACTER SET utf8mb4; \ No newline at end of file diff --git a/core/data/schema/versions/SDEZ_3_upgrade.sql b/core/data/schema/versions/SDEZ_3_upgrade.sql new file mode 100644 index 0000000..c13e1fe --- /dev/null +++ b/core/data/schema/versions/SDEZ_3_upgrade.sql @@ -0,0 +1,21 @@ +ALTER TABLE mai2_item_card +CHANGE COLUMN card_id cardId INT NOT NULL AFTER user, +CHANGE COLUMN card_kind cardTypeId INT NOT NULL, +CHANGE COLUMN chara_id charaId INT NOT NULL, +CHANGE COLUMN map_id mapId INT NOT NULL, +CHANGE COLUMN startDate startDate TIMESTAMP NULL DEFAULT '2018-01-01 00:00:00', +CHANGE COLUMN endDate endDate TIMESTAMP NULL DEFAULT '2038-01-01 00:00:00'; + +ALTER TABLE mai2_item_item +CHANGE COLUMN item_id itemId INT NOT NULL AFTER user, +CHANGE COLUMN item_kind itemKind INT NOT NULL, +CHANGE COLUMN is_valid isValid TINYINT(1) NOT NULL DEFAULT '1'; + +ALTER TABLE mai2_item_character +CHANGE COLUMN character_id characterId INT NOT NULL, +CHANGE COLUMN use_count useCount INT NOT NULL DEFAULT '0'; + +ALTER TABLE mai2_item_charge +CHANGE COLUMN charge_id chargeId INT NOT NULL, +CHANGE COLUMN purchase_date purchaseDate TIMESTAMP NOT NULL, +CHANGE COLUMN valid_date validDate TIMESTAMP NOT NULL; diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md new file mode 100644 index 0000000..8af5541 --- /dev/null +++ b/docs/game_specific_info.md @@ -0,0 +1,351 @@ +# ARTEMiS Games Documentation + +Below are all supported games with supported version ids in order to use +the corresponding importer and database upgrades. + +**Important: The described database upgrades are only required if you are using an old database schema, f.e. still +using the megaime database. Clean installations always create the latest database structure!** + +# Table of content + +- [Supported Games](#Supported-Games) + - [Chunithm](#Chunithm) + - [crossbeats REV.](#crossbeats-REV) + - [maimai DX](#maimai-DX) + - [O.N.G.E.K.I.](#ONGEKI) + - [Card Maker](#Card-Maker) + - [WACCA](#WACCA) + + +# Supported Games + +Games listed below have been tested and confirmed working. + +## Chunithm + +### SDBT + +| Version ID | Version Name | +|------------|--------------------| +| 0 | Chunithm | +| 1 | Chunithm+ | +| 2 | Chunithm Air | +| 3 | Chunithm Air + | +| 4 | Chunithm Star | +| 5 | Chunithm Star + | +| 6 | Chunithm Amazon | +| 7 | Chunithm Amazon + | +| 8 | Chunithm Crystal | +| 9 | Chunithm Crystal + | +| 10 | Chunithm Paradise | + +### SDHD/SDBT + +| Version ID | Version Name | +|------------|-----------------| +| 11 | Chunithm New!! | +| 12 | Chunithm New!!+ | + + +### Importer + +In order to use the importer locate your game installation folder and execute: + +```shell +python read.py --series SDBT --version --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder +``` + +The importer for Chunithm will import: Events, Music, Charge Items and Avatar Accesories. + +### Database upgrade + +Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see +which version is the latest, f.e. `SDBT_3_upgrade.sql`. In order to upgrade to version 3 in this case you need to +perform all previous updates as well: + +```shell +python dbutils.py --game SDBT --version 2 upgrade +python dbutils.py --game SDBT --version 3 upgrade +``` + +## crossbeats REV. + +### SDCA + +| Version ID | Version Name | +|------------|------------------------------------| +| 0 | crossbeats REV. | +| 1 | crossbeats REV. SUNRISE | +| 2 | crossbeats REV. SUNRISE S2 | +| 3 | crossbeats REV. SUNRISE S2 Omnimix | + +### Importer + +In order to use the importer you need to use the provided `Export.csv` file: + +```shell +python read.py --series SDCA --version --binfolder titles/cxb/data +``` + +The importer for crossbeats REV. will import Music. + +### Config + +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 + +### SDEZ + +| Version ID | Version Name | +|------------|-------------------------| +| 0 | maimai DX | +| 1 | maimai DX PLUS | +| 2 | maimai DX Splash | +| 3 | maimai DX Splash PLUS | +| 4 | maimai DX Universe | +| 5 | maimai DX Universe PLUS | + +### Importer + +In order to use the importer locate your game installation folder and execute: + +```shell +python read.py --series SDEZ --version --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder +``` + +The importer for maimai DX will import Events, Music and Tickets. + +**NOTE: It is required to use the importer because the game will +crash without it!** + +### Database upgrade + +Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see which version is the latest, f.e. `SDEZ_2_upgrade.sql`. In order to upgrade to version 2 in this case you need to perform all previous updates as well: + +```shell +python dbutils.py --game SDEZ --version 2 upgrade +``` + +## Hatsune Miku Project Diva + +### SBZV + +| Version ID | Version Name | +|------------|---------------------------------| +| 0 | Project Diva Arcade | +| 1 | Project Diva Arcade Future Tone | + + +### Importer + +In order to use the importer locate your game installation folder and execute: + +```shell +python read.py --series SBZV --version --binfolder /path/to/game/data/diva --optfolder /path/to/game/data/diva/mdata +``` + +The importer for Project Diva Arcade will all required data in order to use +the Shop, Modules and Customizations. + +### Config + +Config file is located in `config/diva.yaml`. + +| Option | Info | +|----------------------|-------------------------------------------------------------------------------------------------| +| `unlock_all_modules` | Unlocks all modules (costumes) by default, if set to `False` all modules need to be purchased | +| `unlock_all_items` | Unlocks all items (customizations) by default, if set to `False` all items need to be purchased | + + +### Database upgrade + +Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see +which version is the latest, f.e. `SBZV_4_upgrade.sql`. In order to upgrade to version 4 in this case you need to +perform all previous updates as well: + +```shell +python dbutils.py --game SBZV --version 2 upgrade +python dbutils.py --game SBZV --version 3 upgrade +python dbutils.py --game SBZV --version 4 upgrade +``` + +## O.N.G.E.K.I. + +### SDDT + +| Version ID | Version Name | +|------------|----------------------------| +| 0 | O.N.G.E.K.I. | +| 1 | O.N.G.E.K.I. + | +| 2 | O.N.G.E.K.I. Summer | +| 3 | O.N.G.E.K.I. Summer + | +| 4 | O.N.G.E.K.I. Red | +| 5 | O.N.G.E.K.I. Red + | +| 6 | O.N.G.E.K.I. Bright | +| 7 | O.N.G.E.K.I. Bright Memory | + + +### Importer + +In order to use the importer locate your game installation folder and execute: + +```shell +python read.py --series SDDT --version --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder +``` + +The importer for O.N.G.E.K.I. will all all Cards, Music and Events. + +**NOTE: The Importer is required for Card Maker.** + +### Config + +Config file is located in `config/ongeki.yaml`. + +| Option | Info | +|------------------|----------------------------------------------------------------------------------------------------------------| +| `enabled_gachas` | Enter all gacha IDs for Card Maker to work, other than default may not work due to missing cards added to them | + +Note: 1149 and higher are only for Card Maker 1.35 and higher and will be ignored on lower versions. + +### Database upgrade + +Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see +which version is the latest, f.e. `SDDT_4_upgrade.sql`. In order to upgrade to version 4 in this case you need to +perform all previous updates as well: + +```shell +python dbutils.py --game SDDT --version 2 upgrade +python dbutils.py --game SDDT --version 3 upgrade +python dbutils.py --game SDDT --version 4 upgrade +``` + +## Card Maker + +### SDED + +| Version ID | Version Name | +|------------|-----------------| +| 0 | Card Maker 1.34 | +| 1 | Card Maker 1.35 | + + +### Support status + +* Card Maker 1.34: + * Chunithm New!!: Yes + * maimai DX Universe: Yes + * O.N.G.E.K.I. Bright: Yes + +* Card Maker 1.35: + * Chunithm New!!+: Yes + * maimai DX Universe PLUS: Yes + * O.N.G.E.K.I. Bright Memory: Yes + + +### Importer + +In order to use the importer you need to use the provided `.csv` files (which are required for O.N.G.E.K.I.) and the +option folders: + +```shell +python read.py --series SDED --version --binfolder titles/cm/cm_data --optfolder /path/to/cardmaker/option/folder +``` + +**If you haven't already executed the O.N.G.E.K.I. importer, make sure you import all cards!** + +```shell +python read.py --series SDDT --version --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder +``` + +Also make sure to import all maimai and Chunithm data as well: + +```shell +python read.py --series SDED --version --binfolder /path/to/cardmaker/CardMaker_Data +``` + +The importer for Card Maker will import all required Gachas (Banners) and cards (for maimai/Chunithm) and the hardcoded +Cards for each Gacha (O.N.G.E.K.I. only). + +**NOTE: Without executing the importer Card Maker WILL NOT work!** + + +### O.N.G.E.K.I. Gachas + +Gacha "無料ガチャ" can only pull from the free cards with the following probabilities: 94%: R, 5% SR and 1% chance of +getting an SSR card + +Gacha "無料ガチャ(SR確定)" can only pull from free SR cards with prob: 92% SR and 8% chance of getting an SSR card + +Gacha "レギュラーガチャ" can pull from every card added to ongeki_static_cards with the following prob: 77% R, 20% SR +and 3% chance of getting an SSR card + +All other (limited) gachas can pull from every card added to ongeki_static_cards but with the promoted cards +(click on the green button under the banner) having a 10 times higher chance to get pulled + +### Chunithm Gachas + +All cards in Chunithm (basically just the characters) have the same rarity to it just pulls randomly from all cards +from a given gacha but made sure you cannot pull the same card twice in the same 5 times gacha roll. + +### Notes + +Card Maker 1.34 will only load an O.N.G.E.K.I. Bright profile (1.30). Card Maker 1.35 will only load an O.N.G.E.K.I. +Bright Memory profile (1.35). +The gachas inside the `ongeki.yaml` will make sure only the right gacha ids for the right CM version will be loaded. +Gacha IDs up to 1140 will be loaded for CM 1.34 and all gachas will be loaded for CM 1.35. + +**NOTE: There is currently no way to load/use the (printed) maimai DX cards!** + +## WACCA + +### SDFE + +| Version ID | Version Name | +|------------|---------------| +| 0 | WACCA | +| 1 | WACCA S | +| 2 | WACCA Lily | +| 3 | WACCA Lily R | +| 4 | WACCA Reverse | + + +### Importer + +In order to use the importer locate your game installation folder and execute: + +```shell +python read.py --series SDFE --version --binfolder /path/to/game/WindowsNoEditor/Mercury/Content +``` + +The importer for WACCA will import all Music data. + +### Config + +Config file is located in `config/wacca.yaml`. + +| Option | Info | +|--------------------|-----------------------------------------------------------------------------| +| `always_vip` | Enables/Disables VIP, if disabled it needs to be purchased manually in game | +| `infinite_tickets` | Always set the "unlock expert" tickets to 5 | +| `infinite_wp` | Sets the user WP to `999999` | +| `enabled_gates` | Enter all gate IDs which should be enabled in game | + + +### Database upgrade + +Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see which version is the latest, f.e. `SDFE_3_upgrade.sql`. In order to upgrade to version 3 in this case you need to perform all previous updates as well: + +```shell +python dbutils.py --game SDFE --version 2 upgrade +python dbutils.py --game SDFE --version 3 upgrade +``` diff --git a/read.py b/read.py index 341c502..a1bd0ab 100644 --- a/read.py +++ b/read.py @@ -4,7 +4,8 @@ import re import os import yaml from os import path -import logging, coloredlogs +import logging +import coloredlogs from logging.handlers import TimedRotatingFileHandler from typing import List, Optional @@ -134,7 +135,8 @@ if __name__ == "__main__": for dir, mod in titles.items(): if args.series in mod.game_codes: - handler = mod.reader(config, args.version, bin_arg, opt_arg, args.extra) + handler = mod.reader(config, args.version, + bin_arg, opt_arg, args.extra) handler.read() logger.info("Done") diff --git a/readme.md b/readme.md index d64cede..4afc225 100644 --- a/readme.md +++ b/readme.md @@ -17,7 +17,7 @@ Games listed below have been tested and confirmed working. Only game versions ol + Card Maker + 1.34.xx - + 1.36.xx + + 1.35.xx + Ongeki + All versions up to Bright Memory @@ -36,5 +36,8 @@ Games listed below have been tested and confirmed working. Only game versions ol ## Setup guides Follow the platform-specific guides for [windows](docs/INSTALL_WINDOWS.md) and [ubuntu](docs/INSTALL_UBUNTU.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. + ## Production guide See the [production guide](docs/prod.md) for running a production server. diff --git a/titles/chuni/base.py b/titles/chuni/base.py index f66eac8..13f423b 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -588,3 +588,11 @@ class ChuniBase: def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict: return {"returnCode": "1"} + + 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/index.py b/titles/chuni/index.py index de01346..b5648fb 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -103,7 +103,7 @@ class ChuniServlet: return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", "") def render_POST(self, request: Request, version: int, url_path: str) -> bytes: - if url_path.lower() == "/ping": + if url_path.lower() == "ping": return zlib.compress(b'{"returnCode": "1"}') req_raw = request.content.getvalue() diff --git a/titles/chuni/new.py b/titles/chuni/new.py index 0d74ba6..611c6d2 100644 --- a/titles/chuni/new.py +++ b/titles/chuni/new.py @@ -1,6 +1,6 @@ import logging from datetime import datetime, timedelta - +from random import randint from typing import Dict from core.config import CoreConfig @@ -61,8 +61,8 @@ class ChuniNew(ChuniBase): } def handle_remove_token_api_request(self, data: Dict) -> Dict: - return { "returnCode": "1" } - + return {"returnCode": "1"} + def handle_delete_token_api_request(self, data: Dict) -> Dict: return {"returnCode": "1"} @@ -122,11 +122,355 @@ class ChuniNew(ChuniBase): "playerLevel": profile["playerLevel"], "rating": profile["rating"], "headphone": profile["headphone"], - "chargeState": 0, - "userNameEx": "0", + # Enables favorites and teams + "chargeState": 1, + "userNameEx": "", "banState": 0, "classEmblemMedal": profile["classEmblemMedal"], "classEmblemBase": profile["classEmblemBase"], "battleRankId": profile["battleRankId"], } return data1 + + 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 {} + + return { + "userName": p["userName"], + "level": p["level"], + "medal": p["medal"], + "lastDataVersion": "2.00.00", + "isLogin": False, + } + + def handle_printer_login_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1} + + def handle_printer_logout_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1} + + def handle_get_game_gacha_api_request(self, data: Dict) -> Dict: + """ + returns all current active banners (gachas) + """ + game_gachas = self.data.static.get_gachas(self.version) + + # clean the database rows + game_gacha_list = [] + for gacha in game_gachas: + tmp = gacha._asdict() + tmp.pop("id") + tmp.pop("version") + tmp["startDate"] = datetime.strftime(tmp["startDate"], "%Y-%m-%d %H:%M:%S") + tmp["endDate"] = datetime.strftime(tmp["endDate"], "%Y-%m-%d %H:%M:%S") + tmp["noticeStartDate"] = datetime.strftime( + tmp["noticeStartDate"], "%Y-%m-%d %H:%M:%S" + ) + tmp["noticeEndDate"] = datetime.strftime( + tmp["noticeEndDate"], "%Y-%m-%d %H:%M:%S" + ) + + game_gacha_list.append(tmp) + + return { + "length": len(game_gacha_list), + "gameGachaList": game_gacha_list, + # no clue + "registIdList": [], + } + + 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 = self.data.static.get_gacha_cards(data["gachaId"]) + + game_gacha_card_list = [] + for gacha_card in game_gacha_cards: + tmp = gacha_card._asdict() + tmp.pop("id") + game_gacha_card_list.append(tmp) + + return { + "gachaId": data["gachaId"], + "length": len(game_gacha_card_list), + # check isPickup from the chuni_static_gachas? + "isPickup": False, + "gameGachaCardList": game_gacha_card_list, + # again no clue + "emissionList": [], + "afterCalcList": [], + "ssrBookCalcList": [], + } + + 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 {} + + profile = p._asdict() + profile.pop("id") + profile.pop("user") + profile.pop("version") + + return { + "userId": data["userId"], + "userData": profile, + "userEmoney": [ + { + "type": 0, + "emoneyCredit": 100, + "emoneyBrand": 1, + "ext1": 0, + "ext2": 0, + "ext3": 0, + } + ], + } + + def handle_get_user_gacha_api_request(self, data: Dict) -> Dict: + user_gachas = self.data.item.get_user_gachas(data["userId"]) + if user_gachas is None: + return {"userId": data["userId"], "length": 0, "userGachaList": []} + + user_gacha_list = [] + for gacha in user_gachas: + tmp = gacha._asdict() + tmp.pop("id") + tmp.pop("user") + tmp["dailyGachaDate"] = datetime.strftime(tmp["dailyGachaDate"], "%Y-%m-%d") + user_gacha_list.append(tmp) + + return { + "userId": data["userId"], + "length": len(user_gacha_list), + "userGachaList": user_gacha_list, + } + + def handle_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: + return { + "userId": data["userId"], + "length": 0, + "nextIndex": -1, + "userPrintedCardList": [], + } + + print_list = [] + next_idx = int(data["nextIndex"]) + max_ct = int(data["maxCount"]) + + for x in range(next_idx, len(user_print_list)): + tmp = user_print_list[x]._asdict() + print_list.append(tmp["cardId"]) + + if len(user_print_list) >= max_ct: + break + + if len(user_print_list) >= max_ct: + next_idx = next_idx + max_ct + else: + next_idx = -1 + + return { + "userId": data["userId"], + "length": len(print_list), + "nextIndex": next_idx, + "userPrintedCardList": print_list, + } + + def handle_get_user_card_print_error_api_request(self, data: Dict) -> Dict: + user_id = data["userId"] + + user_print_states = self.data.item.get_user_print_states( + user_id, has_completed=False + ) + + card_print_state_list = [] + for card in user_print_states: + tmp = card._asdict() + tmp["orderId"] = tmp["id"] + tmp.pop("user") + tmp["limitDate"] = datetime.strftime(tmp["limitDate"], "%Y-%m-%d") + + card_print_state_list.append(tmp) + + return { + "userId": user_id, + "length": len(card_print_state_list), + "userCardPrintStateList": card_print_state_list, + } + + def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: + return super().handle_get_user_character_api_request(data) + + def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: + return super().handle_get_user_item_api_request(data) + + 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 + times: the number of gacha rolls + characterId: the character which the user wants + """ + gacha_id = data["gachaId"] + num_rolls = data["times"] + chara_id = data["characterId"] + + rolled_cards = [] + + # characterId is set after 10 rolls, where the user can select a card + # from all gameGachaCards, therefore the correct cardId for a given + # characterId should be returned + if chara_id != -1: + # get the + 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 = self.data.static.get_gacha_cards(gacha_id) + + # get the card id for each roll + for _ in range(num_rolls): + # get the index from all possible cards + card_idx = randint(0, len(gacha_cards) - 1) + # remove the index from the cards so it wont get pulled again + card = gacha_cards.pop(card_idx) + + # remove the "id" fronm the card + tmp = card._asdict() + tmp.pop("id") + + rolled_cards.append(tmp) + + return {"length": len(rolled_cards), "gameGachaCardList": rolled_cards} + + def handle_cm_upsert_user_gacha_api_request(self, data: Dict) -> Dict: + upsert = data["cmUpsertUserGacha"] + user_id = data["userId"] + place_id = data["placeId"] + + # save the user data + user_data = upsert["userData"] + user_data.pop("rankUpChallengeResults") + user_data.pop("userEmoney") + + self.data.profile.put_profile_data(user_id, self.version, user_data) + + # save the user gacha + user_gacha = upsert["userGacha"] + gacha_id = user_gacha["gachaId"] + user_gacha.pop("gachaId") + user_gacha.pop("dailyGachaDate") + + 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"]: + self.data.item.put_item(user_id, item) + + # add every gamegachaCard to database + for card in upsert["gameGachaCardList"]: + self.data.item.put_user_print_state( + user_id, + hasCompleted=False, + placeId=place_id, + cardId=card["cardId"], + gachaId=card["gachaId"], + ) + + # retrieve every game gacha card which has been added in order to get + # the orderId for the next request + user_print_states = self.data.item.get_user_print_states_by_gacha( + user_id, gacha_id, has_completed=False + ) + card_print_state_list = [] + for card in user_print_states: + tmp = card._asdict() + tmp["orderId"] = tmp["id"] + tmp.pop("user") + tmp["limitDate"] = datetime.strftime(tmp["limitDate"], "%Y-%m-%d") + + card_print_state_list.append(tmp) + + return { + "returnCode": "1", + "apiName": "CMUpsertUserGachaApi", + "userCardPrintStateList": card_print_state_list, + } + + def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: + return { + "returnCode": 1, + "orderId": 0, + "serialId": "11111111111111111111", + "apiName": "CMUpsertUserPrintlogApi", + } + + def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: + user_print_detail = data["userPrintDetail"] + user_id = data["userId"] + + # generate random serial id + serial_id = "".join([str(randint(0, 9)) for _ in range(20)]) + + # not needed because are either zero or unset + user_print_detail.pop("orderId") + user_print_detail.pop("printNumber") + user_print_detail.pop("serialId") + user_print_detail["printDate"] = datetime.strptime( + user_print_detail["printDate"], "%Y-%m-%d" + ) + + # add the entry to the user print table with the random serialId + self.data.item.put_user_print_detail(user_id, serial_id, user_print_detail) + + return { + "returnCode": 1, + "orderId": 0, + "serialId": serial_id, + "apiName": "CMUpsertUserPrintApi", + } + + def handle_cm_upsert_user_print_subtract_api_request(self, data: Dict) -> Dict: + upsert = data["userCardPrintState"] + user_id = data["userId"] + place_id = data["placeId"] + + # save all user items + if "userItemList" in data: + for item in data["userItemList"]: + self.data.item.put_item(user_id, item) + + # set the card print state to success and use the orderId as the key + self.data.item.put_user_print_state( + user_id, + id=upsert["orderId"], + hasCompleted=True + ) + + return {"returnCode": "1", "apiName": "CMUpsertUserPrintSubtractApi"} + + 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: + self.data.item.put_user_print_state( + user_id, + id=order_id, + hasCompleted=True + ) + + return {"returnCode": "1", "apiName": "CMUpsertUserPrintCancelApi"} diff --git a/titles/chuni/newplus.py b/titles/chuni/newplus.py index 7e15985..9dec9aa 100644 --- a/titles/chuni/newplus.py +++ b/titles/chuni/newplus.py @@ -30,3 +30,10 @@ class ChuniNewPlus(ChuniNew): "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 + user_data["lastDataVersion"] = "2.05.00" + return user_data diff --git a/titles/chuni/schema/item.py b/titles/chuni/schema/item.py index cc519fa..124d7df 100644 --- a/titles/chuni/schema/item.py +++ b/titles/chuni/schema/item.py @@ -114,6 +114,76 @@ map_area = Table( mysql_charset="utf8mb4", ) +gacha = Table( + "chuni_item_gacha", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("gachaId", Integer, nullable=False), + Column("totalGachaCnt", Integer, server_default="0"), + Column("ceilingGachaCnt", Integer, server_default="0"), + Column("dailyGachaCnt", Integer, server_default="0"), + Column("fiveGachaCnt", Integer, server_default="0"), + Column("elevenGachaCnt", Integer, server_default="0"), + Column("dailyGachaDate", TIMESTAMP, nullable=False, server_default=func.now()), + UniqueConstraint("user", "gachaId", name="chuni_item_gacha_uk"), + mysql_charset="utf8mb4", +) + +print_state = Table( + "chuni_item_print_state", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("hasCompleted", Boolean, nullable=False, server_default="0"), + Column( + "limitDate", TIMESTAMP, nullable=False, server_default="2038-01-01 00:00:00.0" + ), + Column("placeId", Integer), + Column("cardId", Integer), + Column("gachaId", Integer), + UniqueConstraint("id", "user", name="chuni_item_print_state_uk"), + mysql_charset="utf8mb4", +) + +print_detail = Table( + "chuni_item_print_detail", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("cardId", Integer, nullable=False), + Column("printDate", TIMESTAMP, nullable=False), + Column("serialId", String(20), nullable=False), + Column("placeId", Integer, nullable=False), + Column("clientId", String(11), nullable=False), + Column("printerSerialId", String(20), nullable=False), + Column("printOption1", Boolean, server_default="0"), + Column("printOption2", Boolean, server_default="0"), + Column("printOption3", Boolean, server_default="0"), + Column("printOption4", Boolean, server_default="0"), + Column("printOption5", Boolean, server_default="0"), + Column("printOption6", Boolean, server_default="0"), + Column("printOption7", Boolean, server_default="0"), + Column("printOption8", Boolean, server_default="0"), + Column("printOption9", Boolean, server_default="0"), + Column("printOption10", Boolean, server_default="0"), + Column("created", String(255), server_default=""), + UniqueConstraint("serialId", name="chuni_item_print_detail_uk"), + mysql_charset="utf8mb4", +) + class ChuniItemData(BaseData): def put_character(self, user_id: int, character_data: Dict) -> Optional[int]: @@ -235,3 +305,89 @@ class ChuniItemData(BaseData): if result is None: return None return result.fetchall() + + def get_user_gachas(self, aime_id: int) -> Optional[List[Row]]: + sql = gacha.select(gacha.c.user == aime_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def 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) + + conflict = sql.on_duplicate_key_update( + user=aime_id, gachaId=gacha_id, **gacha_data + ) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_user_gacha: Failed to insert! aime_id: {aime_id}") + return None + return result.lastrowid + + def get_user_print_states( + self, aime_id: int, has_completed: bool = False + ) -> Optional[List[Row]]: + sql = print_state.select( + and_( + print_state.c.user == aime_id, + print_state.c.hasCompleted == has_completed + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + 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( + and_( + print_state.c.user == aime_id, + print_state.c.gachaId == gacha_id, + print_state.c.hasCompleted == has_completed + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + 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 = self.execute(conflict) + + if result is None: + self.logger.warn( + f"put_user_print_state: Failed to insert! aime_id: {aime_id}" + ) + return None + return result.lastrowid + + def put_user_print_detail( + self, aime_id: int, serial_id: str, user_print_data: Dict + ) -> Optional[int]: + sql = insert(print_detail).values( + user=aime_id, serialId=serial_id, **user_print_data + ) + + conflict = sql.on_duplicate_key_update( + user=aime_id, **user_print_data + ) + result = self.execute(conflict) + + if result is None: + self.logger.warn( + f"put_user_print_detail: Failed to insert! aime_id: {aime_id}" + ) + return None + return result.lastrowid \ No newline at end of file diff --git a/titles/chuni/schema/static.py b/titles/chuni/schema/static.py index 0d58c45..0784872 100644 --- a/titles/chuni/schema/static.py +++ b/titles/chuni/schema/static.py @@ -68,6 +68,60 @@ avatar = Table( mysql_charset="utf8mb4", ) +gachas = Table( + "chuni_static_gachas", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column("gachaId", Integer, nullable=False), + Column("gachaName", String(255), nullable=False), + Column("type", Integer, nullable=False, server_default="0"), + Column("kind", Integer, nullable=False, server_default="0"), + Column("isCeiling", Boolean, server_default="0"), + Column("ceilingCnt", Integer, server_default="10"), + Column("changeRateCnt1", Integer, server_default="0"), + Column("changeRateCnt2", Integer, server_default="0"), + Column("startDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"), + Column("endDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"), + Column("noticeStartDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"), + Column("noticeEndDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"), + UniqueConstraint("version", "gachaId", "gachaName", name="chuni_static_gachas_uk"), + mysql_charset="utf8mb4", +) + +cards = Table( + "chuni_static_cards", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column("cardId", Integer, nullable=False), + Column("charaName", String(255), nullable=False), + Column("charaId", Integer, nullable=False), + Column("presentName", String(255), nullable=False), + Column("rarity", Integer, server_default="2"), + Column("labelType", Integer, nullable=False), + Column("difType", Integer, nullable=False), + Column("miss", Integer, nullable=False), + Column("combo", Integer, nullable=False), + Column("chain", Integer, nullable=False), + Column("skillName", String(255), nullable=False), + UniqueConstraint("version", "cardId", name="chuni_static_cards_uk"), + mysql_charset="utf8mb4", +) + +gacha_cards = Table( + "chuni_static_gacha_cards", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("gachaId", Integer, nullable=False), + Column("cardId", Integer, nullable=False), + Column("rarity", Integer, nullable=False), + Column("weight", Integer, server_default="1"), + Column("isPickup", Boolean, server_default="0"), + UniqueConstraint("gachaId", "cardId", name="chuni_static_gacha_cards_uk"), + mysql_charset="utf8mb4", +) + class ChuniStaticData(BaseData): def put_event( @@ -265,3 +319,112 @@ class ChuniStaticData(BaseData): if result is None: return None return result.lastrowid + + def put_gacha( + self, + version: int, + gacha_id: int, + gacha_name: int, + **gacha_data, + ) -> Optional[int]: + sql = insert(gachas).values( + version=version, + gachaId=gacha_id, + gachaName=gacha_name, + **gacha_data, + ) + + conflict = sql.on_duplicate_key_update( + version=version, + gachaId=gacha_id, + gachaName=gacha_name, + **gacha_data, + ) + + result = self.execute(conflict) + if result is None: + self.logger.warn(f"Failed to insert gacha! gacha_id {gacha_id}") + return None + return result.lastrowid + + def get_gachas(self, version: int) -> Optional[List[Dict]]: + sql = gachas.select(gachas.c.version <= version).order_by( + gachas.c.gachaId.asc() + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_gacha(self, version: int, gacha_id: int) -> Optional[Dict]: + sql = gachas.select( + and_(gachas.c.version <= version, gachas.c.gachaId == gacha_id) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def put_gacha_card( + self, gacha_id: int, card_id: int, **gacha_card + ) -> Optional[int]: + sql = insert(gacha_cards).values(gachaId=gacha_id, cardId=card_id, **gacha_card) + + conflict = sql.on_duplicate_key_update( + gachaId=gacha_id, cardId=card_id, **gacha_card + ) + + result = self.execute(conflict) + if result is None: + self.logger.warn(f"Failed to insert gacha card! gacha_id {gacha_id}") + return None + return result.lastrowid + + def get_gacha_cards(self, gacha_id: int) -> Optional[List[Dict]]: + sql = gacha_cards.select(gacha_cards.c.gachaId == gacha_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_gacha_card_by_character(self, gacha_id: int, chara_id: int) -> Optional[Dict]: + sql_sub = ( + select(cards.c.cardId) + .filter( + cards.c.charaId == chara_id + ) + .scalar_subquery() + ) + + # Perform the main query, also rename the resulting column to ranking + sql = gacha_cards.select(and_( + gacha_cards.c.gachaId == gacha_id, + gacha_cards.c.cardId == sql_sub + )) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def put_card(self, version: int, card_id: int, **card_data) -> Optional[int]: + sql = insert(cards).values(version=version, cardId=card_id, **card_data) + + conflict = sql.on_duplicate_key_update(**card_data) + + result = self.execute(conflict) + if result is None: + self.logger.warn(f"Failed to insert card! card_id {card_id}") + return None + return result.lastrowid + + def get_card(self, version: int, card_id: int) -> Optional[Dict]: + sql = cards.select(and_(cards.c.version <= version, cards.c.cardId == card_id)) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() diff --git a/titles/cm/__init__.py b/titles/cm/__init__.py index ae4e9f0..1115f96 100644 --- a/titles/cm/__init__.py +++ b/titles/cm/__init__.py @@ -1,9 +1,11 @@ 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] diff --git a/titles/cm/cm136.py b/titles/cm/cm135.py similarity index 89% rename from titles/cm/cm136.py rename to titles/cm/cm135.py index fb5b6b5..782f07a 100644 --- a/titles/cm/cm136.py +++ b/titles/cm/cm135.py @@ -11,10 +11,10 @@ from titles.cm.const import CardMakerConstants from titles.cm.config import CardMakerConfig -class CardMaker136(CardMakerBase): +class CardMaker135(CardMakerBase): def __init__(self, core_cfg: CoreConfig, game_cfg: CardMakerConfig) -> None: super().__init__(core_cfg, game_cfg) - self.version = CardMakerConstants.VER_CARD_MAKER_136 + self.version = CardMakerConstants.VER_CARD_MAKER_135 def handle_get_game_connect_api_request(self, data: Dict) -> Dict: ret = super().handle_get_game_connect_api_request(data) @@ -26,13 +26,13 @@ class CardMaker136(CardMakerBase): ret["gameConnectList"][0]["titleUri"] = f"{uri}/SDHD/205/" ret["gameConnectList"][1]["titleUri"] = f"{uri}/SDEZ/125/" ret["gameConnectList"][2]["titleUri"] = f"{uri}/SDDT/135/" - + return ret 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"]["ongekiCmVersion"] = "1.35.04" + ret["gameSetting"]["ongekiCmVersion"] = "1.35.03" ret["gameSetting"]["chuniCmVersion"] = "2.05.00" ret["gameSetting"]["maimaiCmVersion"] = "1.25.00" return ret diff --git a/titles/cm/const.py b/titles/cm/const.py index c5627ee..09f289e 100644 --- a/titles/cm/const.py +++ b/titles/cm/const.py @@ -4,9 +4,9 @@ class CardMakerConstants: CONFIG_NAME = "cardmaker.yaml" VER_CARD_MAKER = 0 - VER_CARD_MAKER_136 = 1 + VER_CARD_MAKER_135 = 1 - VERSION_NAMES = ("Card Maker 1.34", "Card Maker 1.36") + VERSION_NAMES = ("Card Maker 1.34", "Card Maker 1.35") @classmethod def game_ver_to_string(cls, ver: int): diff --git a/titles/cm/database.py b/titles/cm/database.py new file mode 100644 index 0000000..1d32109 --- /dev/null +++ b/titles/cm/database.py @@ -0,0 +1,8 @@ +from core.data import Data +from core.config import CoreConfig + + +class CardMakerData(Data): + def __init__(self, cfg: CoreConfig) -> None: + super().__init__(cfg) + # empty Card Maker database diff --git a/titles/cm/index.py b/titles/cm/index.py index d082aad..d544e59 100644 --- a/titles/cm/index.py +++ b/titles/cm/index.py @@ -15,7 +15,7 @@ from core.config import CoreConfig from titles.cm.config import CardMakerConfig from titles.cm.const import CardMakerConstants from titles.cm.base import CardMakerBase -from titles.cm.cm136 import CardMaker136 +from titles.cm.cm135 import CardMaker135 class CardMakerServlet: @@ -29,7 +29,7 @@ class CardMakerServlet: self.versions = [ CardMakerBase(core_cfg, self.game_cfg), - CardMaker136(core_cfg, self.game_cfg), + CardMaker135(core_cfg, self.game_cfg), ] self.logger = logging.getLogger("cardmaker") @@ -87,8 +87,8 @@ class CardMakerServlet: if version >= 130 and version < 135: # Card Maker internal_ver = CardMakerConstants.VER_CARD_MAKER - elif version >= 135 and version < 140: # Card Maker - internal_ver = CardMakerConstants.VER_CARD_MAKER_136 + elif version >= 135 and version < 136: # Card Maker 1.35 + internal_ver = CardMakerConstants.VER_CARD_MAKER_135 if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32: # If we get a 32 character long hex string, it's a hash and we're diff --git a/titles/cm/read.py b/titles/cm/read.py index 3a4635f..f27b40b 100644 --- a/titles/cm/read.py +++ b/titles/cm/read.py @@ -12,6 +12,10 @@ 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): @@ -25,6 +29,8 @@ class CardMakerReader(BaseReader): ) -> None: super().__init__(config, version, bin_dir, opt_dir, extra) self.ongeki_data = OngekiData(config) + self.mai2_data = Mai2Data(config) + self.chuni_data = ChuniData(config) try: self.logger.info( @@ -34,15 +40,29 @@ class CardMakerReader(BaseReader): self.logger.error(f"Invalid Card Maker version {version}") exit(1) + def _get_card_maker_directory(self, directory: str) -> str: + for root, dirs, files in os.walk(directory): + for dir in dirs: + if ( + os.path.exists(f"{root}/{dir}/MU3") + and os.path.exists(f"{root}/{dir}/MAI") + and os.path.exists(f"{root}/{dir}/CHU") + ): + return f"{root}/{dir}" + def read(self) -> None: static_datas = { "static_gachas.csv": "read_ongeki_gacha_csv", "static_gacha_cards.csv": "read_ongeki_gacha_card_csv", } - data_dirs = [] - if self.bin_dir is not None: + data_dir = self._get_card_maker_directory(self.bin_dir) + + self.read_chuni_card(f"{data_dir}/CHU/Data/A000/card") + self.read_chuni_gacha(f"{data_dir}/CHU/Data/A000/gacha") + + 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) @@ -53,13 +73,163 @@ class CardMakerReader(BaseReader): ) if self.opt_dir is not None: - data_dirs += self.get_data_directories(self.opt_dir) + data_dirs = self.get_data_directories(self.opt_dir) # ONGEKI (MU3) cnnot easily access the bin data(A000.pac) # so only opt_dir will work for now for dir in data_dirs: + self.read_chuni_card(f"{dir}/CHU/card") + self.read_chuni_gacha(f"{dir}/CHU/gacha") + self.read_ongeki_gacha(f"{dir}/MU3/gacha") + def read_chuni_card(self, base_dir: str) -> None: + self.logger.info(f"Reading cards from {base_dir}...") + + version_ids = { + "v2_00": ChuniConstants.VER_CHUNITHM_NEW, + "v2_05": ChuniConstants.VER_CHUNITHM_NEW_PLUS, + # Chunithm SUN, ignore for now + "v2_10": ChuniConstants.VER_CHUNITHM_NEW_PLUS + 1 + } + + for root, dirs, files in os.walk(base_dir): + for dir in dirs: + if os.path.exists(f"{root}/{dir}/Card.xml"): + with open(f"{root}/{dir}/Card.xml", "r", encoding="utf-8") as f: + troot = ET.fromstring(f.read()) + + card_id = int(troot.find("name").find("id").text) + + chara_name = troot.find("chuniCharaName").find("str").text + chara_id = troot.find("chuniCharaName").find("id").text + version = version_ids[ + troot.find("netOpenName").find("str").text[:5] + ] + present_name = troot.find("chuniPresentName").find("str").text + rarity = int(troot.find("rareType").text) + label = int(troot.find("labelType").text) + dif = int(troot.find("difType").text) + miss = int(troot.find("miss").text) + combo = int(troot.find("combo").text) + chain = int(troot.find("chain").text) + skill_name = troot.find("skillName").text + + self.chuni_data.static.put_card( + version, + card_id, + charaName=chara_name, + charaId=chara_id, + presentName=present_name, + rarity=rarity, + labelType=label, + difType=dif, + miss=miss, + combo=combo, + chain=chain, + skillName=skill_name, + ) + + self.logger.info(f"Added chuni card {card_id}") + + def read_chuni_gacha(self, base_dir: str) -> None: + self.logger.info(f"Reading gachas from {base_dir}...") + + version_ids = { + "v2_00": ChuniConstants.VER_CHUNITHM_NEW, + "v2_05": ChuniConstants.VER_CHUNITHM_NEW_PLUS, + # Chunithm SUN, ignore for now + "v2_10": ChuniConstants.VER_CHUNITHM_NEW_PLUS + 1, + } + + for root, dirs, files in os.walk(base_dir): + for dir in dirs: + if os.path.exists(f"{root}/{dir}/Gacha.xml"): + with open(f"{root}/{dir}/Gacha.xml", "r", encoding="utf-8") as f: + troot = ET.fromstring(f.read()) + + name = troot.find("gachaName").text + gacha_id = int(troot.find("name").find("id").text) + + version = version_ids[ + troot.find("netOpenName").find("str").text[:5] + ] + ceiling_cnt = int(troot.find("ceilingNum").text) + gacha_type = int(troot.find("gachaType").text) + is_ceiling = ( + True if troot.find("ceilingType").text == "1" else False + ) + + self.chuni_data.static.put_gacha( + version, + gacha_id, + name, + type=gacha_type, + isCeiling=is_ceiling, + ceilingCnt=ceiling_cnt, + ) + + self.logger.info(f"Added chuni gacha {gacha_id}") + + for gacha_card in troot.find("infos").iter("GachaCardDataInfo"): + # get the card ID from the id element + card_id = gacha_card.find("cardName").find("id").text + + # get the weight from the weight element + weight = int(gacha_card.find("weight").text) + + # get the pickup flag from the pickup element + is_pickup = ( + True if gacha_card.find("pickup").text == "1" else False + ) + + self.chuni_data.static.put_gacha_card( + gacha_id, + card_id, + weight=weight, + rarity=2, + isPickup=is_pickup, + ) + + self.logger.info( + f"Added chuni card {card_id} to gacha {gacha_id}" + ) + + def read_mai2_card(self, base_dir: str) -> None: + self.logger.info(f"Reading cards from {base_dir}...") + + version_ids = { + "1.00": Mai2Constants.VER_MAIMAI_DX, + "1.05": Mai2Constants.VER_MAIMAI_DX_PLUS, + "1.09": Mai2Constants.VER_MAIMAI_DX_PLUS, + "1.10": Mai2Constants.VER_MAIMAI_DX_SPLASH, + "1.15": Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS, + "1.20": Mai2Constants.VER_MAIMAI_DX_UNIVERSE, + "1.25": Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS, + } + + for root, dirs, files in os.walk(base_dir): + for dir in dirs: + if os.path.exists(f"{root}/{dir}/Card.xml"): + with open(f"{root}/{dir}/Card.xml", "r", encoding="utf-8") as f: + troot = ET.fromstring(f.read()) + + name = troot.find("name").find("str").text + card_id = int(troot.find("name").find("id").text) + + version = version_ids[ + troot.find("enableVersion").find("str").text + ] + + enabled = ( + True if troot.find("disable").text == "false" else False + ) + + self.mai2_data.static.put_card( + version, card_id, name, enabled=enabled + ) + self.logger.info(f"Added mai2 card {card_id}") + def read_ongeki_gacha_csv(self, file_path: str) -> None: self.logger.info(f"Reading gachas from {file_path}...") @@ -76,7 +246,7 @@ class CardMakerReader(BaseReader): maxSelectPoint=row["maxSelectPoint"], ) - self.logger.info(f"Added gacha {row['gachaId']}") + self.logger.info(f"Added ongeki gacha {row['gachaId']}") def read_ongeki_gacha_card_csv(self, file_path: str) -> None: self.logger.info(f"Reading gacha cards from {file_path}...") @@ -93,7 +263,7 @@ class CardMakerReader(BaseReader): isSelect=True if row["isSelect"] == "1" else False, ) - self.logger.info(f"Added card {row['cardId']} to gacha") + self.logger.info(f"Added ongeki card {row['cardId']} to gacha") def read_ongeki_gacha(self, base_dir: str) -> None: self.logger.info(f"Reading gachas from {base_dir}...") @@ -152,4 +322,4 @@ class CardMakerReader(BaseReader): isCeiling=is_ceiling, maxSelectPoint=max_select_point, ) - self.logger.info(f"Added gacha {gacha_id}") + self.logger.info(f"Added ongeki gacha {gacha_id}") diff --git a/titles/cm/schema/__init__.py b/titles/cm/schema/__init__.py new file mode 100644 index 0000000..a9a2c5b --- /dev/null +++ b/titles/cm/schema/__init__.py @@ -0,0 +1 @@ +__all__ = [] diff --git a/titles/mai2/__init__.py b/titles/mai2/__init__.py index 0d9ea89..27fba3a 100644 --- a/titles/mai2/__init__.py +++ b/titles/mai2/__init__.py @@ -7,4 +7,4 @@ index = Mai2Servlet database = Mai2Data reader = Mai2Reader game_codes = [Mai2Constants.GAME_CODE] -current_schema_version = 2 +current_schema_version = 3 diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 8a48d8b..741ccb6 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -202,6 +202,16 @@ class Mai2Base: for act in v: self.data.profile.put_profile_activity(user_id, act) + if "userChargeList" in upsert and len(upsert["userChargeList"]) > 0: + for charge in upsert["userChargeList"]: + self.data.item.put_charge( + user_id, + charge["chargeId"], + charge["stock"], + datetime.strptime(charge["purchaseDate"], "%Y-%m-%d %H:%M:%S"), + datetime.strptime(charge["validDate"], "%Y-%m-%d %H:%M:%S") + ) + if upsert["isNewCharacterList"] and int(upsert["isNewCharacterList"]) > 0: for char in upsert["userCharacterList"]: self.data.item.put_character( @@ -299,10 +309,67 @@ class Mai2Base: return {"userId": data["userId"], "userOption": options_dict} def handle_get_user_card_api_request(self, data: Dict) -> Dict: - return {"userId": data["userId"], "nextIndex": 0, "userCardList": []} + user_cards = self.data.item.get_cards(data["userId"]) + if user_cards is None: + return { + "userId": data["userId"], + "nextIndex": 0, + "userCardList": [] + } + + max_ct = data["maxCount"] + next_idx = data["nextIndex"] + start_idx = next_idx + end_idx = max_ct + start_idx + + if len(user_cards[start_idx:]) > max_ct: + next_idx += max_ct + else: + next_idx = 0 + + card_list = [] + for card in user_cards: + tmp = card._asdict() + tmp.pop("id") + tmp.pop("user") + tmp["startDate"] = datetime.strftime( + tmp["startDate"], "%Y-%m-%d %H:%M:%S") + tmp["endDate"] = datetime.strftime( + tmp["endDate"], "%Y-%m-%d %H:%M:%S") + card_list.append(tmp) + + return { + "userId": data["userId"], + "nextIndex": next_idx, + "userCardList": card_list[start_idx:end_idx] + } def handle_get_user_charge_api_request(self, data: Dict) -> Dict: - return {"userId": data["userId"], "length": 0, "userChargeList": []} + user_charges = self.data.item.get_charges(data["userId"]) + if user_charges is None: + return { + "userId": data["userId"], + "length": 0, + "userChargeList": [] + } + + user_charge_list = [] + for charge in user_charges: + tmp = charge._asdict() + tmp.pop("id") + tmp.pop("user") + tmp["purchaseDate"] = datetime.strftime( + tmp["purchaseDate"], "%Y-%m-%d %H:%M:%S") + tmp["validDate"] = datetime.strftime( + tmp["validDate"], "%Y-%m-%d %H:%M:%S") + + 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) @@ -313,15 +380,13 @@ class Mai2Base: for x in range(next_idx, data["maxCount"]): try: - user_item_list.append( - { - "item_kind": user_items[x]["item_kind"], - "item_id": user_items[x]["item_id"], - "stock": user_items[x]["stock"], - "isValid": user_items[x]["is_valid"], - } - ) - except: + user_item_list.append({ + "itemKind": user_items[x]["itemKind"], + "itemId": user_items[x]["itemId"], + "stock": user_items[x]["stock"], + "isValid": user_items[x]["isValid"] + }) + except IndexError: break if len(user_item_list) == data["maxCount"]: @@ -332,21 +397,18 @@ class Mai2Base: "userId": data["userId"], "nextIndex": next_idx, "itemKind": kind, - "userItemList": user_item_list, + "userItemList": user_item_list } def handle_get_user_character_api_request(self, data: Dict) -> Dict: characters = self.data.item.get_characters(data["userId"]) + chara_list = [] for chara in characters: - chara_list.append( - { - "characterId": chara["character_id"], - "level": chara["level"], - "awakening": chara["awakening"], - "useCount": chara["use_count"], - } - ) + tmp = chara._asdict() + tmp.pop("id") + tmp.pop("user") + chara_list.append(tmp) return {"userId": data["userId"], "userCharacterList": chara_list} @@ -417,10 +479,21 @@ class Mai2Base: tmp.pop("user") mlst.append(tmp) - return {"userActivity": {"playList": plst, "musicList": mlst}} + return { + "userActivity": { + "playList": plst, + "musicList": mlst + } + } def handle_get_user_course_api_request(self, data: Dict) -> Dict: user_courses = self.data.score.get_courses(data["userId"]) + if user_courses is None: + return { + "userId": data["userId"], + "nextIndex": 0, + "userCourseList": [] + } course_list = [] for course in user_courses: @@ -429,7 +502,11 @@ class Mai2Base: tmp.pop("id") course_list.append(tmp) - return {"userId": data["userId"], "nextIndex": 0, "userCourseList": course_list} + return { + "userId": data["userId"], + "nextIndex": 0, + "userCourseList": course_list + } def handle_get_user_portrait_api_request(self, data: Dict) -> Dict: # No support for custom pfps @@ -540,18 +617,11 @@ class Mai2Base: if songs is not None: for song in songs: - music_detail_list.append( - { - "musicId": song["song_id"], - "level": song["chart_id"], - "playCount": song["play_count"], - "achievement": song["achievement"], - "comboStatus": song["combo_status"], - "syncStatus": song["sync_status"], - "deluxscoreMax": song["dx_score"], - "scoreRank": song["score_rank"], - } - ) + tmp = song._asdict() + tmp.pop("id") + tmp.pop("user") + music_detail_list.append(tmp) + if len(music_detail_list) == data["maxCount"]: next_index = data["maxCount"] + data["nextIndex"] break @@ -559,5 +629,5 @@ class Mai2Base: return { "userId": data["userId"], "nextIndex": next_index, - "userMusicList": [{"userMusicDetailList": music_detail_list}], + "userMusicList": [{"userMusicDetailList": music_detail_list}] } diff --git a/titles/mai2/index.py b/titles/mai2/index.py index 0679d1f..3cd1629 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -89,7 +89,7 @@ class Mai2Servlet: ) def render_POST(self, request: Request, version: int, url_path: str) -> bytes: - if url_path.lower() == "/ping": + if url_path.lower() == "ping": return zlib.compress(b'{"returnCode": "1"}') req_raw = request.content.getvalue() diff --git a/titles/mai2/schema/item.py b/titles/mai2/schema/item.py index 072eb3e..d64d954 100644 --- a/titles/mai2/schema/item.py +++ b/titles/mai2/schema/item.py @@ -1,5 +1,6 @@ from core.data.schema import BaseData, metadata +from datetime import datetime from typing import Optional, Dict, List from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON @@ -17,11 +18,11 @@ character = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("character_id", Integer, nullable=False), + Column("characterId", Integer, nullable=False), Column("level", Integer, nullable=False, server_default="1"), Column("awakening", Integer, nullable=False, server_default="0"), - Column("use_count", Integer, nullable=False, server_default="0"), - UniqueConstraint("user", "character_id", name="mai2_item_character_uk"), + Column("useCount", Integer, nullable=False, server_default="0"), + UniqueConstraint("user", "characterId", name="mai2_item_character_uk"), mysql_charset="utf8mb4", ) @@ -34,13 +35,13 @@ card = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("card_kind", Integer, nullable=False), - Column("card_id", Integer, nullable=False), - Column("chara_id", Integer, nullable=False), - Column("map_id", Integer, nullable=False), - Column("start_date", String(255), nullable=False), - Column("end_date", String(255), nullable=False), - UniqueConstraint("user", "card_kind", "card_id", name="mai2_item_card_uk"), + Column("cardId", Integer, nullable=False), + Column("cardTypeId", Integer, nullable=False), + Column("charaId", Integer, nullable=False), + Column("mapId", Integer, nullable=False), + Column("startDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"), + Column("endDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"), + UniqueConstraint("user", "cardId", "cardTypeId", name="mai2_item_card_uk"), mysql_charset="utf8mb4", ) @@ -53,11 +54,11 @@ item = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("item_kind", Integer, nullable=False), - Column("item_id", Integer, nullable=False), + Column("itemId", Integer, nullable=False), + Column("itemKind", Integer, nullable=False), Column("stock", Integer, nullable=False, server_default="1"), - Column("is_valid", Boolean, nullable=False, server_default="1"), - UniqueConstraint("user", "item_kind", "item_id", name="mai2_item_item_uk"), + Column("isValid", Boolean, nullable=False, server_default="1"), + UniqueConstraint("user", "itemId", "itemKind", name="mai2_item_item_uk"), mysql_charset="utf8mb4", ) @@ -139,11 +140,44 @@ charge = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("charge_id", Integer, nullable=False), + Column("chargeId", Integer, nullable=False), Column("stock", Integer, nullable=False), - Column("purchase_date", String(255), nullable=False), - Column("valid_date", String(255), nullable=False), - UniqueConstraint("user", "charge_id", name="mai2_item_charge_uk"), + Column("purchaseDate", String(255), nullable=False), + Column("validDate", String(255), nullable=False), + UniqueConstraint("user", "chargeId", name="mai2_item_charge_uk"), + mysql_charset="utf8mb4", +) + +print_detail = Table( + "mai2_item_print_detail", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("orderId", Integer), + Column("printNumber", Integer), + Column("printDate", TIMESTAMP, nullable=False, server_default=func.now()), + Column("serialId", String(20), nullable=False), + Column("placeId", Integer, nullable=False), + Column("clientId", String(11), nullable=False), + Column("printerSerialId", String(20), nullable=False), + Column("cardRomVersion", Integer), + Column("isHolograph", Boolean, server_default="1"), + Column("printOption1", Boolean, server_default="0"), + Column("printOption2", Boolean, server_default="0"), + Column("printOption3", Boolean, server_default="0"), + Column("printOption4", Boolean, server_default="0"), + Column("printOption5", Boolean, server_default="0"), + Column("printOption6", Boolean, server_default="0"), + Column("printOption7", Boolean, server_default="0"), + Column("printOption8", Boolean, server_default="0"), + Column("printOption9", Boolean, server_default="0"), + Column("printOption10", Boolean, server_default="0"), + Column("created", String(255), server_default=""), + UniqueConstraint("user", "serialId", name="mai2_item_print_detail_uk"), mysql_charset="utf8mb4", ) @@ -154,15 +188,15 @@ class Mai2ItemData(BaseData): ) -> None: sql = insert(item).values( user=user_id, - item_kind=item_kind, - item_id=item_id, + itemKind=item_kind, + itemId=item_id, stock=stock, - is_valid=is_valid, + isValid=is_valid, ) conflict = sql.on_duplicate_key_update( stock=stock, - is_valid=is_valid, + isValid=is_valid, ) result = self.execute(conflict) @@ -178,7 +212,7 @@ class Mai2ItemData(BaseData): sql = item.select(item.c.user == user_id) else: sql = item.select( - and_(item.c.user == user_id, item.c.item_kind == item_kind) + and_(item.c.user == user_id, item.c.itemKind == item_kind) ) result = self.execute(sql) @@ -190,8 +224,8 @@ class Mai2ItemData(BaseData): sql = item.select( and_( item.c.user == user_id, - item.c.item_kind == item_kind, - item.c.item_id == item_id, + item.c.itemKind == item_kind, + item.c.itemId == item_id, ) ) @@ -382,3 +416,93 @@ class Mai2ItemData(BaseData): if result is None: return None return result.fetchall() + + def put_card( + self, + user_id: int, + card_type_id: int, + card_kind: int, + chara_id: int, + map_id: int, + ) -> Optional[Row]: + sql = insert(card).values( + user=user_id, + cardId=card_type_id, + cardTypeId=card_kind, + charaId=chara_id, + mapId=map_id, + ) + + conflict = sql.on_duplicate_key_update(charaId=chara_id, mapId=map_id) + + result = self.execute(conflict) + if result is None: + self.logger.warn( + f"put_card: failed to insert card! user_id: {user_id}, kind: {kind}" + ) + return None + return result.lastrowid + + def get_cards(self, user_id: int, kind: int = None) -> Optional[Row]: + if kind is None: + sql = card.select(card.c.user == user_id) + else: + sql = card.select(and_(card.c.user == user_id, card.c.cardKind == kind)) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def put_charge( + self, + user_id: int, + charge_id: int, + stock: int, + purchase_date: datetime, + valid_date: datetime, + ) -> Optional[Row]: + sql = insert(charge).values( + user=user_id, + chargeId=charge_id, + stock=stock, + purchaseDate=purchase_date, + validDate=valid_date, + ) + + conflict = sql.on_duplicate_key_update( + stock=stock, purchaseDate=purchase_date, validDate=valid_date + ) + + result = self.execute(conflict) + if result is None: + self.logger.warn( + f"put_card: failed to insert charge! user_id: {user_id}, chargeId: {charge_id}" + ) + return None + return result.lastrowid + + def get_charges(self, user_id: int) -> Optional[Row]: + sql = charge.select(charge.c.user == user_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def put_user_print_detail( + self, aime_id: int, serial_id: str, user_print_data: Dict + ) -> Optional[int]: + sql = insert(print_detail).values( + user=aime_id, serialId=serial_id, **user_print_data + ) + + conflict = sql.on_duplicate_key_update(**user_print_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn( + f"put_user_print_detail: Failed to insert! aime_id: {aime_id}" + ) + return None + return result.lastrowid diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index 335a731..1ce8046 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -448,7 +448,9 @@ class Mai2ProfileData(BaseData): return None return result.lastrowid - def get_profile_activity(self, user_id: int, kind: int = None) -> Optional[Row]: + def get_profile_activity( + self, user_id: int, kind: int = None + ) -> Optional[List[Row]]: sql = activity.select( and_( activity.c.user == user_id, @@ -459,4 +461,4 @@ class Mai2ProfileData(BaseData): result = self.execute(sql) if result is None: return None - return result.fetchone() + return result.fetchall() diff --git a/titles/mai2/schema/score.py b/titles/mai2/schema/score.py index 15bf519..4d3291d 100644 --- a/titles/mai2/schema/score.py +++ b/titles/mai2/schema/score.py @@ -242,7 +242,7 @@ class Mai2ScoreData(BaseData): return result.lastrowid def get_courses(self, user_id: int) -> Optional[List[Row]]: - sql = course.select(best_score.c.user == user_id) + sql = course.select(course.c.user == user_id) result = self.execute(sql) if result is None: diff --git a/titles/mai2/schema/static.py b/titles/mai2/schema/static.py index 2908a47..e40e37f 100644 --- a/titles/mai2/schema/static.py +++ b/titles/mai2/schema/static.py @@ -53,6 +53,22 @@ ticket = Table( mysql_charset="utf8mb4", ) +cards = Table( + "mai2_static_cards", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column("cardId", Integer, nullable=False), + Column("cardName", String(255), nullable=False), + Column("startDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"), + Column("endDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"), + Column("noticeStartDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"), + Column("noticeEndDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"), + Column("enabled", Boolean, server_default="1"), + UniqueConstraint("version", "cardId", "cardName", name="mai2_static_cards_uk"), + mysql_charset="utf8mb4", +) + class Mai2StaticData(BaseData): def put_game_event( @@ -166,6 +182,8 @@ class Mai2StaticData(BaseData): conflict = sql.on_duplicate_key_update(price=ticket_price) + conflict = sql.on_duplicate_key_update(price=ticket_price) + result = self.execute(conflict) if result is None: self.logger.warn(f"Failed to insert charge {ticket_id} type {ticket_type}") @@ -208,3 +226,24 @@ class Mai2StaticData(BaseData): if result is None: return None return result.fetchone() + + def put_card(self, version: int, card_id: int, card_name: str, **card_data) -> int: + sql = insert(cards).values( + version=version, cardId=card_id, cardName=card_name, **card_data + ) + + conflict = sql.on_duplicate_key_update(**card_data) + + result = self.execute(conflict) + if result is None: + self.logger.warn(f"Failed to insert card {card_id}") + return None + return result.lastrowid + + def get_enabled_cards(self, version: int) -> Optional[List[Row]]: + sql = cards.select(and_(cards.c.version == version, cards.c.enabled == True)) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() diff --git a/titles/mai2/universe.py b/titles/mai2/universe.py index 56c2d3f..56b3e8f 100644 --- a/titles/mai2/universe.py +++ b/titles/mai2/universe.py @@ -1,4 +1,5 @@ from typing import Any, List, Dict +from random import randint from datetime import datetime, timedelta import pytz import json @@ -13,3 +14,176 @@ class Mai2Universe(Mai2Base): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE + + def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + p = self.data.profile.get_profile_detail(data["userId"], self.version) + if p is None: + return {} + + return { + "userName": p["userName"], + "rating": p["playerRating"], + # hardcode lastDataVersion for CardMaker 1.34 + "lastDataVersion": "1.20.00", + "isLogin": False, + "isExistSellingCard": False, + } + + def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: + # user already exists, because the preview checks that already + p = self.data.profile.get_profile_detail(data["userId"], self.version) + + cards = self.data.card.get_user_cards(data["userId"]) + if cards is None or len(cards) == 0: + # This should never happen + self.logger.error( + f"handle_get_user_data_api_request: Internal error - No cards found for user id {data['userId']}" + ) + return {} + + # get the dict representation of the row so we can modify values + user_data = p._asdict() + + # remove the values the game doesn't want + user_data.pop("id") + user_data.pop("user") + user_data.pop("version") + + return {"userId": data["userId"], "userData": user_data} + + def handle_cm_login_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1} + + def handle_cm_logout_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1} + + def handle_cm_get_selling_card_api_request(self, data: Dict) -> Dict: + selling_cards = self.data.static.get_enabled_cards(self.version) + if selling_cards is None: + return {"length": 0, "sellingCardList": []} + + selling_card_list = [] + for card in selling_cards: + tmp = card._asdict() + tmp.pop("id") + tmp.pop("version") + tmp.pop("cardName") + tmp.pop("enabled") + + tmp["startDate"] = datetime.strftime(tmp["startDate"], "%Y-%m-%d %H:%M:%S") + tmp["endDate"] = datetime.strftime(tmp["endDate"], "%Y-%m-%d %H:%M:%S") + tmp["noticeStartDate"] = datetime.strftime( + tmp["noticeStartDate"], "%Y-%m-%d %H:%M:%S" + ) + tmp["noticeEndDate"] = datetime.strftime( + tmp["noticeEndDate"], "%Y-%m-%d %H:%M:%S" + ) + + selling_card_list.append(tmp) + + return {"length": len(selling_card_list), "sellingCardList": selling_card_list} + + def handle_cm_get_user_card_api_request(self, data: Dict) -> Dict: + user_cards = self.data.item.get_cards(data["userId"]) + if user_cards is None: + return {"returnCode": 1, "length": 0, "nextIndex": 0, "userCardList": []} + + max_ct = data["maxCount"] + next_idx = data["nextIndex"] + start_idx = next_idx + end_idx = max_ct + start_idx + + if len(user_cards[start_idx:]) > max_ct: + next_idx += max_ct + else: + next_idx = 0 + + card_list = [] + for card in user_cards: + tmp = card._asdict() + tmp.pop("id") + tmp.pop("user") + + tmp["startDate"] = datetime.strftime(tmp["startDate"], "%Y-%m-%d %H:%M:%S") + tmp["endDate"] = datetime.strftime(tmp["endDate"], "%Y-%m-%d %H:%M:%S") + card_list.append(tmp) + + return { + "returnCode": 1, + "length": len(card_list[start_idx:end_idx]), + "nextIndex": next_idx, + "userCardList": card_list[start_idx:end_idx], + } + + def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: + super().handle_get_user_item_api_request(data) + + def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: + characters = self.data.item.get_characters(data["userId"]) + + chara_list = [] + for chara in characters: + chara_list.append( + { + "characterId": chara["characterId"], + # no clue why those values are even needed + "point": 0, + "count": 0, + "level": chara["level"], + "nextAwake": 0, + "nextAwakePercent": 0, + "favorite": False, + "awakening": chara["awakening"], + "useCount": chara["useCount"], + } + ) + + return { + "returnCode": 1, + "length": len(chara_list), + "userCharacterList": chara_list, + } + + def handle_cm_get_user_card_print_error_api_request(self, data: Dict) -> Dict: + return {"length": 0, "userPrintDetailList": []} + + def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: + user_id = data["userId"] + upsert = data["userPrintDetail"] + + # set a random card serial number + serial_id = "".join([str(randint(0, 9)) for _ in range(20)]) + + user_card = upsert["userCard"] + self.data.item.put_card( + user_id, + user_card["cardId"], + user_card["cardTypeId"], + user_card["charaId"], + user_card["mapId"], + ) + + # properly format userPrintDetail for the database + upsert.pop("userCard") + upsert.pop("serialId") + upsert["printDate"] = datetime.strptime(upsert["printDate"], "%Y-%m-%d") + + self.data.item.put_user_print_detail(user_id, serial_id, upsert) + + return { + "returnCode": 1, + "orderId": 0, + "serialId": serial_id, + "startDate": "2018-01-01 00:00:00", + "endDate": "2038-01-01 00:00:00", + } + + def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: + return { + "returnCode": 1, + "orderId": 0, + "serialId": data["userPrintlog"]["serialId"], + } + + def handle_cm_upsert_buy_card_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1} diff --git a/titles/mai2/universeplus.py b/titles/mai2/universeplus.py index 977fce9..54fe896 100644 --- a/titles/mai2/universeplus.py +++ b/titles/mai2/universeplus.py @@ -4,12 +4,19 @@ import pytz import json from core.config import CoreConfig -from titles.mai2.base import Mai2Base +from titles.mai2.universe import Mai2Universe from titles.mai2.const import Mai2Constants from titles.mai2.config import Mai2Config -class Mai2UniversePlus(Mai2Base): +class Mai2UniversePlus(Mai2Universe): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS + + def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = super().handle_cm_get_user_preview_api_request(data) + + # hardcode lastDataVersion for CardMaker 1.35 + user_data["lastDataVersion"] = "1.25.00" + return user_data diff --git a/titles/ongeki/base.py b/titles/ongeki/base.py index 4f7619c..10bb1a8 100644 --- a/titles/ongeki/base.py +++ b/titles/ongeki/base.py @@ -452,7 +452,8 @@ class OngekiBase: tmp.pop("id") items.append(tmp) - xout = kind * 10000000000 + (data["nextIndex"] % 10000000000) + len(items) + xout = kind * 10000000000 + \ + (data["nextIndex"] % 10000000000) + len(items) if len(items) < data["maxCount"] or data["maxCount"] == 0: nextIndex = 0 @@ -851,7 +852,8 @@ class OngekiBase: ) if "userOption" in upsert and len(upsert["userOption"]) > 0: - 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"]: diff --git a/titles/ongeki/bright.py b/titles/ongeki/bright.py index 4b2a06f..23eeb6c 100644 --- a/titles/ongeki/bright.py +++ b/titles/ongeki/bright.py @@ -93,7 +93,12 @@ class OngekiBright(OngekiBase): 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 {} + return { + "userId": data["userId"], + "length": 0, + "nextIndex": 0, + "userCharacterList": [] + } max_ct = data["maxCount"] next_idx = data["nextIndex"] @@ -543,7 +548,7 @@ class OngekiBright(OngekiBase): "returnCode": 1, "orderId": 0, "serialId": "11111111111111111111", - "apiName": "CMUpsertUserPrintPlaylogApi", + "apiName": "CMUpsertUserPrintPlaylogApi" } def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: @@ -551,7 +556,7 @@ class OngekiBright(OngekiBase): "returnCode": 1, "orderId": 0, "serialId": "11111111111111111111", - "apiName": "CMUpsertUserPrintlogApi", + "apiName": "CMUpsertUserPrintlogApi" } def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: diff --git a/titles/ongeki/brightmemory.py b/titles/ongeki/brightmemory.py index 954d0e5..6e2548b 100644 --- a/titles/ongeki/brightmemory.py +++ b/titles/ongeki/brightmemory.py @@ -142,8 +142,8 @@ class OngekiBrightMemory(OngekiBright): user_data = super().handle_cm_get_user_data_api_request(data) # hardcode Card Maker version for now - # Card Maker 1.34.00 = 1.30.01 - # Card Maker 1.36.00 = 1.35.04 - user_data["userData"]["compatibleCmVersion"] = "1.35.04" + # Card Maker 1.34 = 1.30.01 + # Card Maker 1.35 = 1.35.03 + user_data["userData"]["compatibleCmVersion"] = "1.35.03" return user_data diff --git a/titles/ongeki/index.py b/titles/ongeki/index.py index 07c8ff2..7927d84 100644 --- a/titles/ongeki/index.py +++ b/titles/ongeki/index.py @@ -3,7 +3,8 @@ import json import inflection import yaml import string -import logging, coloredlogs +import logging +import coloredlogs import zlib from logging.handlers import TimedRotatingFileHandler from os import path @@ -93,7 +94,7 @@ class OngekiServlet: ) def render_POST(self, request: Request, version: int, url_path: str) -> bytes: - if url_path.lower() == "/ping": + if url_path.lower() == "ping": return zlib.compress(b'{"returnCode": 1}') req_raw = request.content.getvalue() diff --git a/titles/ongeki/schema/item.py b/titles/ongeki/schema/item.py index d406597..d826fba 100644 --- a/titles/ongeki/schema/item.py +++ b/titles/ongeki/schema/item.py @@ -706,7 +706,7 @@ class OngekiItemData(BaseData): ) conflict = sql.on_duplicate_key_update( - user=aime_id, serialId=serial_id, **user_print_data + user=aime_id, **user_print_data ) result = self.execute(conflict) From 8b718c601f752ace2826a7b16d4e0497d7c62bb7 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 16 Mar 2023 21:27:03 -0400 Subject: [PATCH 50/63] chuni: add method hashing support --- titles/chuni/index.py | 76 ++++++++++++++++++++++++++++++------------- 1 file changed, 54 insertions(+), 22 deletions(-) diff --git a/titles/chuni/index.py b/titles/chuni/index.py index b5648fb..dccdd66 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -8,8 +8,10 @@ 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 +from typing import Tuple, Dict from core import CoreConfig, Utils from titles.chuni.config import ChuniConfig @@ -33,27 +35,43 @@ class ChuniServlet: def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: self.core_cfg = core_cfg self.game_cfg = ChuniConfig() + self.hash_table: Dict[Dict[str, str]] = {} if path.exists(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"): self.game_cfg.update( yaml.safe_load(open(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}")) ) self.versions = [ - ChuniBase(core_cfg, self.game_cfg), - ChuniPlus(core_cfg, self.game_cfg), - ChuniAir(core_cfg, self.game_cfg), - ChuniAirPlus(core_cfg, self.game_cfg), - ChuniStar(core_cfg, self.game_cfg), - ChuniStarPlus(core_cfg, self.game_cfg), - ChuniAmazon(core_cfg, self.game_cfg), - ChuniAmazonPlus(core_cfg, self.game_cfg), - ChuniCrystal(core_cfg, self.game_cfg), - ChuniCrystalPlus(core_cfg, self.game_cfg), - ChuniParadise(core_cfg, self.game_cfg), - ChuniNew(core_cfg, self.game_cfg), - ChuniNewPlus(core_cfg, self.game_cfg), + ChuniBase, + ChuniPlus, + ChuniAir, + ChuniAirPlus, + ChuniStar, + ChuniStarPlus, + ChuniAmazon, + ChuniAmazonPlus, + ChuniCrystal, + ChuniCrystalPlus, + ChuniParadise, + ChuniNew, + ChuniNewPlus, ] + for version, keys in self.game_cfg.crypto.keys.items(): + if len(keys) < 3: + continue + + self.hash_table[version] = {} + + method_list = [method for method in dir(self.versions[version]) if not method.startswith('__')] + for method in method_list: + method_fixed = inflection.camelize(method) + hash = PBKDF2(method_fixed, bytes.fromhex(keys[2]), 128, count=44, hmac_hash_module=SHA1) + + self.hash_table[version][hash.hex()] = method_fixed + + self.logger.debug(f"Hashed v{version} method {method_fixed} with {bytes.fromhex(keys[2])} to get {hash.hex()}") + self.logger = logging.getLogger("chuni") if not hasattr(self.logger, "inited"): @@ -144,25 +162,38 @@ class ChuniServlet: # 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 - endpoint = request.getHeader("User-Agent").split("#")[0] + if internal_ver < ChuniConstants.VER_CHUNITHM_NEW: + 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 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 zlib.compress(b'{"stat": "0"}') + + endpoint = self.hash_table[internal_ver][endpoint.lower()] + try: crypt = AES.new( - bytes.fromhex(self.game_cfg.crypto.keys[str(internal_ver)][0]), + bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][0]), AES.MODE_CBC, - bytes.fromhex(self.game_cfg.crypto.keys[str(internal_ver)][1]), + bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]), ) req_raw = crypt.decrypt(req_raw) - except: + except Exception as e: self.logger.error( - f"Failed to decrypt v{version} request to {endpoint} -> {req_raw}" + f"Failed to decrypt v{version} request to {endpoint} -> {e}" ) return zlib.compress(b'{"stat": "0"}') encrtped = True - if not encrtped and self.game_cfg.crypto.encrypted_only: + if not encrtped and self.game_cfg.crypto.encrypted_only and internal_ver >= ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS: self.logger.error( f"Unencrypted v{version} {endpoint} request, but config is set to encrypted only: {req_raw}" ) @@ -185,14 +216,15 @@ class ChuniServlet: self.logger.debug(req_data) func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" + handler_cls = self.versions[internal_ver](self.core_cfg, self.game_cfg) - if not hasattr(self.versions[internal_ver], func_to_find): + if not hasattr(handler_cls, func_to_find): self.logger.warning(f"Unhandled v{version} request {endpoint}") resp = {"returnCode": 1} else: try: - handler = getattr(self.versions[internal_ver], func_to_find) + handler = getattr(handler_cls, func_to_find) resp = handler(req_data) except Exception as e: From 71eec6e34d1e3554e5f04cd9b1193da66de78d2e Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 16 Mar 2023 21:36:42 -0400 Subject: [PATCH 51/63] fix error if log folder not created or not writeable --- dbutils.py | 13 ++++++++++++- index.py | 12 ++++++------ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/dbutils.py b/dbutils.py index ea9555a..65b1f8e 100644 --- a/dbutils.py +++ b/dbutils.py @@ -2,7 +2,7 @@ import yaml import argparse from core.config import CoreConfig from core.data import Data -from os import path +from os import path, mkdir, access, W_OK if __name__ == "__main__": parser = argparse.ArgumentParser(description="Database utilities") @@ -33,7 +33,18 @@ if __name__ == "__main__": cfg = CoreConfig() if path.exists(f"{args.config}/core.yaml"): cfg.update(yaml.safe_load(open(f"{args.config}/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) + data = Data(cfg) + if args.action == "create": data.create_database() diff --git a/index.py b/index.py index 0852d06..13d826d 100644 --- a/index.py +++ b/index.py @@ -170,6 +170,12 @@ if __name__ == "__main__": 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) @@ -189,12 +195,6 @@ if __name__ == "__main__": logger.setLevel(log_lv) coloredlogs.install(level=log_lv, logger=logger, fmt=log_fmt_str) - if not access(cfg.server.log_dir, W_OK): - logger.error( - f"Log directory {cfg.server.log_dir} NOT writable, please check permissions" - ) - exit(1) - if not cfg.aimedb.key: logger.error("!!AIMEDB KEY BLANK, SET KEY IN CORE.YAML!!") exit(1) From a6e9e80bc7a52340578f7472302a368902a0a979 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 16 Mar 2023 21:42:43 -0400 Subject: [PATCH 52/63] chuni: fix encryption --- titles/chuni/index.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/titles/chuni/index.py b/titles/chuni/index.py index dccdd66..a47ceda 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -244,9 +244,9 @@ class ChuniServlet: padded = pad(zipped, 16) crypt = AES.new( - bytes.fromhex(self.game_cfg.crypto.keys[str(internal_ver)][0]), + bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][0]), AES.MODE_CBC, - bytes.fromhex(self.game_cfg.crypto.keys[str(internal_ver)][1]), + bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]), ) return crypt.encrypt(padded) From a7db5eed770c4d37af9dd98c3c609f02dd88618f Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 16 Mar 2023 21:56:36 -0400 Subject: [PATCH 53/63] chuni: fix encryption --- titles/chuni/index.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/titles/chuni/index.py b/titles/chuni/index.py index a47ceda..53db19f 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -57,21 +57,6 @@ class ChuniServlet: ChuniNewPlus, ] - for version, keys in self.game_cfg.crypto.keys.items(): - if len(keys) < 3: - continue - - self.hash_table[version] = {} - - method_list = [method for method in dir(self.versions[version]) if not method.startswith('__')] - for method in method_list: - method_fixed = inflection.camelize(method) - hash = PBKDF2(method_fixed, bytes.fromhex(keys[2]), 128, count=44, hmac_hash_module=SHA1) - - self.hash_table[version][hash.hex()] = method_fixed - - self.logger.debug(f"Hashed v{version} method {method_fixed} with {bytes.fromhex(keys[2])} to get {hash.hex()}") - self.logger = logging.getLogger("chuni") if not hasattr(self.logger, "inited"): @@ -97,6 +82,21 @@ class ChuniServlet: level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str ) self.logger.inited = True + + for version, keys in self.game_cfg.crypto.keys.items(): + if len(keys) < 3: + continue + + self.hash_table[version] = {} + + method_list = [method for method in dir(self.versions[version]) if not method.startswith('__')] + for method in method_list: + method_fixed = inflection.camelize(method)[6:-7] + hash = PBKDF2(method_fixed, bytes.fromhex(keys[2]), 128, count=44, hmac_hash_module=SHA1) + + self.hash_table[version][hash.hex()] = method_fixed + + self.logger.debug(f"Hashed v{version} method {method_fixed} with {bytes.fromhex(keys[2])} to get {hash.hex()}") @classmethod def get_allnet_info( From 8c5c7f31b68ca3f3b26f2b6cefcad80f8fda85ab Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 16 Mar 2023 22:31:41 -0400 Subject: [PATCH 54/63] allnet: fix setting=1 --- core/allnet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/allnet.py b/core/allnet.py index 2738c8e..67b47b3 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -371,7 +371,7 @@ class AllnetPowerOnResponse3: self.utc_time = datetime.now(tz=pytz.timezone("UTC")).strftime( "%Y-%m-%dT%H:%M:%SZ" ) - self.setting = "" + self.setting = "1" self.res_ver = "3" self.token = str(token) From 4bd1dea6bfb9d75fb2f71a437a41a6c2e361d3fe Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 17 Mar 2023 02:06:15 -0400 Subject: [PATCH 55/63] allnet: add info log to downloadorder --- core/allnet.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/allnet.py b/core/allnet.py index 67b47b3..c028a57 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -181,7 +181,9 @@ class AllnetServlet: self.logger.error(e) return b"" + self.logger.info(f"DownloadOrder from {request_ip} -> {req.game_id} v{req.ver} serial {req.serial}") resp = AllnetDownloadOrderResponse() + if not self.config.allnet.allow_online_updates: return self.dict_to_http_form_string([vars(resp)]) From 7ca4e6adb93e4fd3ef2e5ffa6665d3d8cd6aba4c Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 17 Mar 2023 02:11:49 -0400 Subject: [PATCH 56/63] fix IP address logging --- core/allnet.py | 4 ++-- core/frontend.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index c028a57..2afb8cf 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -192,7 +192,7 @@ class AllnetServlet: def handle_billing_request(self, request: Request, _: Dict): req_dict = self.billing_req_to_dict(request.content.getvalue()) - request_ip = request.getClientAddress() + 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"" @@ -225,7 +225,7 @@ class AllnetServlet: return self.dict_to_http_form_string([vars(resp)]) msg = ( - f"Billing checkin from {request.getClientIP()}: game {kc_game} keychip {kc_serial} playcount " + f"Billing checkin from {request_ip}: game {kc_game} keychip {kc_serial} playcount " f"{kc_playcount} billing_type {kc_billigtype} nearfull {kc_nearfull} playlimit {kc_playlimit}" ) self.logger.info(msg) diff --git a/core/frontend.py b/core/frontend.py index e230681..127b174 100644 --- a/core/frontend.py +++ b/core/frontend.py @@ -30,7 +30,7 @@ class UserSession(object): class FrontendServlet(resource.Resource): def getChild(self, name: bytes, request: Request): - self.logger.debug(f"{request.getClientIP()} -> {name.decode()}") + self.logger.debug(f"{Utils.get_ip_addr(request)} -> {name.decode()}") if name == b"": return self return resource.Resource.getChild(self, name, request) @@ -84,7 +84,7 @@ class FrontendServlet(resource.Resource): ) def render_GET(self, request): - self.logger.debug(f"{request.getClientIP()} -> {request.uri.decode()}") + 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, @@ -113,7 +113,7 @@ class FE_Base(resource.Resource): class FE_Gate(FE_Base): def render_GET(self, request: Request): - self.logger.debug(f"{request.getClientIP()} -> {request.uri.decode()}") + self.logger.debug(f"{Utils.get_ip_addr(request)} -> {request.uri.decode()}") uri: str = request.uri.decode() sesh = request.getSession() From 6965132e5b40b5e1ff30121596c0e69c40a8aa53 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 17 Mar 2023 02:16:49 -0400 Subject: [PATCH 57/63] chuni: fix hard error caused by not having the db set up --- titles/chuni/base.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/titles/chuni/base.py b/titles/chuni/base.py index 13f423b..3668b29 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -32,6 +32,9 @@ class ChuniBase: 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": []} charges = [] for x in range(len(game_charge_list)): @@ -52,6 +55,14 @@ class ChuniBase: 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.warn("No enabled events, did you run the reader?") + return { + "type": data["type"], + "length": 0, + "gameEventList": [], + } + event_list = [] for evt_row in game_events: tmp = {} From 5a388e2a24d680ee4d445c12417990ae619b0aa2 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 17 Mar 2023 19:53:00 -0400 Subject: [PATCH 58/63] docs: fix casing in game_specific_info.md --- docs/game_specific_info.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 8af5541..f028c6c 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -8,13 +8,13 @@ using the megaime database. Clean installations always create the latest databas # Table of content -- [Supported Games](#Supported-Games) - - [Chunithm](#Chunithm) - - [crossbeats REV.](#crossbeats-REV) - - [maimai DX](#maimai-DX) - - [O.N.G.E.K.I.](#ONGEKI) - - [Card Maker](#Card-Maker) - - [WACCA](#WACCA) +- [Supported Games](#supported-games) + - [Chunithm](#chunithm) + - [crossbeats REV.](#crossbeats-rev) + - [maimai DX](#maimai-dx) + - [O.N.G.E.K.I.](#ongeki) + - [Card Maker](#card-maker) + - [WACCA](#wacca) # Supported Games From 401623f20bcea9e48d14023b2170394ad51c664e Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 17 Mar 2023 20:03:07 -0400 Subject: [PATCH 59/63] docs: fix ongeki anchor in game_specific_info.md --- docs/game_specific_info.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index f028c6c..b09d61e 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -12,7 +12,7 @@ using the megaime database. Clean installations always create the latest databas - [Chunithm](#chunithm) - [crossbeats REV.](#crossbeats-rev) - [maimai DX](#maimai-dx) - - [O.N.G.E.K.I.](#ongeki) + - [O.N.G.E.K.I.](#o-n-g-e-k-i) - [Card Maker](#card-maker) - [WACCA](#wacca) From 6ff8c4d931a7450126427f4780bfaf3c4fc9b65a Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 18 Mar 2023 02:12:33 -0400 Subject: [PATCH 60/63] fix mai2 and diva db scripts --- core/data/schema/versions/SBZV_1_rollback.sql | 6 +++--- core/data/schema/versions/SDEZ_2_rollback.sql | 21 +++++++++++++++++++ core/data/schema/versions/SDEZ_3_upgrade.sql | 4 ++-- 3 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 core/data/schema/versions/SDEZ_2_rollback.sql diff --git a/core/data/schema/versions/SBZV_1_rollback.sql b/core/data/schema/versions/SBZV_1_rollback.sql index a7bccce..6389157 100644 --- a/core/data/schema/versions/SBZV_1_rollback.sql +++ b/core/data/schema/versions/SBZV_1_rollback.sql @@ -1,9 +1,9 @@ SET FOREIGN_KEY_CHECKS=0; -ALTER TABLE diva_score DROP COLUMN edition; -ALTER TABLE diva_playlog DROP COLUMN edition; - ALTER TABLE diva_score DROP FOREIGN KEY diva_score_ibfk_1; ALTER TABLE diva_score DROP CONSTRAINT diva_score_uk; ALTER TABLE diva_score ADD CONSTRAINT diva_score_uk UNIQUE (user, pv_id, difficulty); ALTER TABLE diva_score ADD CONSTRAINT diva_score_ibfk_1 FOREIGN KEY (user) REFERENCES aime_user(id) ON DELETE CASCADE; + +ALTER TABLE diva_score DROP COLUMN edition; +ALTER TABLE diva_playlog DROP COLUMN edition; SET FOREIGN_KEY_CHECKS=1; \ No newline at end of file diff --git a/core/data/schema/versions/SDEZ_2_rollback.sql b/core/data/schema/versions/SDEZ_2_rollback.sql new file mode 100644 index 0000000..b631711 --- /dev/null +++ b/core/data/schema/versions/SDEZ_2_rollback.sql @@ -0,0 +1,21 @@ +ALTER TABLE mai2_item_card +CHANGE COLUMN cardId card_id INT NOT NULL AFTER user, +CHANGE COLUMN cardTypeId card_kind INT NOT NULL, +CHANGE COLUMN charaId chara_id INT NOT NULL, +CHANGE COLUMN mapId map_id INT NOT NULL, +CHANGE COLUMN startDate start_date TIMESTAMP NULL DEFAULT '2018-01-01 00:00:00', +CHANGE COLUMN endDate end_date TIMESTAMP NULL DEFAULT '2038-01-01 00:00:00'; + +ALTER TABLE mai2_item_item +CHANGE COLUMN itemId item_id INT NOT NULL AFTER user, +CHANGE COLUMN itemKind item_kind INT NOT NULL, +CHANGE COLUMN isValid is_valid TINYINT(1) NOT NULL DEFAULT '1'; + +ALTER TABLE mai2_item_character +CHANGE COLUMN characterId character_id INT NOT NULL, +CHANGE COLUMN useCount use_count INT NOT NULL DEFAULT '0'; + +ALTER TABLE mai2_item_charge +CHANGE COLUMN chargeId charge_id INT NOT NULL, +CHANGE COLUMN purchaseDate purchase_date TIMESTAMP NOT NULL, +CHANGE COLUMN validDate valid_date TIMESTAMP NOT NULL; \ No newline at end of file diff --git a/core/data/schema/versions/SDEZ_3_upgrade.sql b/core/data/schema/versions/SDEZ_3_upgrade.sql index c13e1fe..bdb5906 100644 --- a/core/data/schema/versions/SDEZ_3_upgrade.sql +++ b/core/data/schema/versions/SDEZ_3_upgrade.sql @@ -3,8 +3,8 @@ CHANGE COLUMN card_id cardId INT NOT NULL AFTER user, CHANGE COLUMN card_kind cardTypeId INT NOT NULL, CHANGE COLUMN chara_id charaId INT NOT NULL, CHANGE COLUMN map_id mapId INT NOT NULL, -CHANGE COLUMN startDate startDate TIMESTAMP NULL DEFAULT '2018-01-01 00:00:00', -CHANGE COLUMN endDate endDate TIMESTAMP NULL DEFAULT '2038-01-01 00:00:00'; +CHANGE COLUMN start_date startDate TIMESTAMP NULL DEFAULT '2018-01-01 00:00:00', +CHANGE COLUMN end_date endDate TIMESTAMP NULL DEFAULT '2038-01-01 00:00:00'; ALTER TABLE mai2_item_item CHANGE COLUMN item_id itemId INT NOT NULL AFTER user, From 188be2dfc16c11efb3b60595b8e260f1c36c7a32 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 18 Mar 2023 02:12:58 -0400 Subject: [PATCH 61/63] database: add autoupgrade command --- core/data/database.py | 74 +++++++++++++++++++++++++++++++--------- core/data/schema/base.py | 9 +++++ dbutils.py | 3 ++ 3 files changed, 69 insertions(+), 17 deletions(-) diff --git a/core/data/database.py b/core/data/database.py index 0ce039e..07fe79e 100644 --- a/core/data/database.py +++ b/core/data/database.py @@ -145,25 +145,49 @@ class Data: ) return - if not os.path.exists( - f"core/data/schema/versions/{game.upper()}_{version}_{action}.sql" - ): - self.logger.error( - f"Could not find {action} script {game.upper()}_{version}_{action}.sql in core/data/schema/versions folder" - ) - 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()}_{version}_{action}.sql", - "r", - encoding="utf-8", - ) as f: - sql = f.read() + 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.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: @@ -237,3 +261,19 @@ class Data: if not cards: self.logger.info(f"Delete hanging user {user['id']}") self.user.delete_user(user["id"]) + + def autoupgrade(self) -> None: + all_games = self.base.get_all_schema_vers() + if all_games is None: + self.logger.warn("Failed to get schema versions") + + for x in all_games: + game = x["game"].upper() + update_ver = 1 + for y in range(2, 100): + if os.path.exists(f"core/data/schema/versions/{game}_{y}_upgrade.sql"): + update_ver = y + else: + break + + self.migrate_database(game, update_ver, "upgrade") \ No newline at end of file diff --git a/core/data/schema/base.py b/core/data/schema/base.py index 9899f29..f77a9aa 100644 --- a/core/data/schema/base.py +++ b/core/data/schema/base.py @@ -2,6 +2,7 @@ import json import logging from random import randrange from typing import Any, Optional, Dict, List +from sqlalchemy.engine import Row from sqlalchemy.engine.cursor import CursorResult from sqlalchemy.engine.base import Connection from sqlalchemy.sql import text, func, select @@ -80,6 +81,14 @@ class BaseData: Generate a random 5-7 digit id """ return randrange(10000, 9999999) + + def get_all_schema_vers(self) -> Optional[List[Row]]: + sql = select(schema_ver) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() def get_schema_ver(self, game: str) -> Optional[int]: sql = select(schema_ver).where(schema_ver.c.game == game) diff --git a/dbutils.py b/dbutils.py index 65b1f8e..176c67e 100644 --- a/dbutils.py +++ b/dbutils.py @@ -64,6 +64,9 @@ if __name__ == "__main__": else: data.migrate_database(args.game, int(args.version), args.action) + elif args.action == "autoupgrade": + data.autoupgrade() + elif args.action == "create-owner": data.create_owner(args.email) From 62b62db5b58c8c63b93644e5518b8e89af67e983 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 18 Mar 2023 12:26:57 -0400 Subject: [PATCH 62/63] wacca: fix favorites, purchasing and unlocking songs, incorrectly displayed grades --- titles/wacca/base.py | 23 ++++++++--------------- titles/wacca/handlers/helpers.py | 16 +++++++++++++++- titles/wacca/handlers/user_info.py | 6 +++--- titles/wacca/index.py | 2 +- 4 files changed, 27 insertions(+), 20 deletions(-) diff --git a/titles/wacca/base.py b/titles/wacca/base.py index bc0c09a..275f380 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -635,14 +635,14 @@ class WaccaBase: new_tickets.append([ticket["id"], ticket["ticket_id"], 9999999999]) for item in req.itemsUsed: - if item.itemType == WaccaConstants.ITEM_TYPES["wp"]: + if item.itemType == WaccaConstants.ITEM_TYPES["wp"] and not self.game_config.mods.infinite_wp: if current_wp >= item.quantity: current_wp -= item.quantity self.data.profile.spend_wp(req.profileId, item.quantity) else: return BaseResponse().make() - elif item.itemType == WaccaConstants.ITEM_TYPES["ticket"]: + elif item.itemType == WaccaConstants.ITEM_TYPES["ticket"] and not self.game_config.mods.infinite_tickets: for x in range(len(new_tickets)): if new_tickets[x][1] == item.itemId: self.data.item.spend_ticket(new_tickets[x][0]) @@ -880,7 +880,7 @@ class WaccaBase: user_id = profile["user"] resp.currentWp = profile["wp"] - if req.purchaseType == PurchaseType.PurchaseTypeWP: + if req.purchaseType == PurchaseType.PurchaseTypeWP and not self.game_config.mods.infinite_wp: resp.currentWp -= req.cost self.data.profile.spend_wp(req.profileId, req.cost) @@ -1070,19 +1070,12 @@ class WaccaBase: ): if item.quantity > WaccaConstants.Difficulty.HARD.value: old_score = self.data.score.get_best_score( - user_id, item.itemId, item.quantity + user_id, item.itemId, item.quantity + ) + if not old_score: + self.data.score.put_best_score( + user_id, item.itemId, item.quantity, 0, [0] * 5, [0] * 13, 0, 0 ) - if not old_score: - self.data.score.put_best_score( - user_id, - item.itemId, - item.quantity, - 0, - [0] * 5, - [0] * 13, - 0, - 0, - ) if item.quantity == 0: item.quantity = WaccaConstants.Difficulty.HARD.value diff --git a/titles/wacca/handlers/helpers.py b/titles/wacca/handlers/helpers.py index 9e9847a..02b8bd7 100644 --- a/titles/wacca/handlers/helpers.py +++ b/titles/wacca/handlers/helpers.py @@ -577,7 +577,21 @@ class SongDetailGradeCountsV2(SongDetailGradeCountsV1): self.ssspCt = counts[12] def make(self) -> List: - return super().make() + [self.spCt, self.sspCt, self.ssspCt] + return [ + self.dCt, + self.cCt, + self.bCt, + self.aCt, + self.aaCt, + self.aaaCt, + self.sCt, + self.spCt, + self.ssCt, + self.sspCt, + self.sssCt, + self.ssspCt, + self.masterCt, + ] class BestScoreDetailV1: diff --git a/titles/wacca/handlers/user_info.py b/titles/wacca/handlers/user_info.py index 8c67db9..bf6b74b 100644 --- a/titles/wacca/handlers/user_info.py +++ b/titles/wacca/handlers/user_info.py @@ -11,9 +11,9 @@ class UserInfoUpdateRequest(BaseRequest): self.profileId = int(self.params[0]) self.optsUpdated: List[UserOption] = [] self.unknown2: List = self.params[2] - self.datesUpdated: List[DateUpdate] = [] - self.favoritesAdded: List[int] = self.params[4] - self.favoritesRemoved: List[int] = self.params[5] + self.datesUpdated: List[DateUpdate] = [] + self.favoritesRemoved: List[int] = self.params[4] + self.favoritesAdded: List[int] = self.params[5] for x in self.params[1]: self.optsUpdated.append(UserOption(x[0], x[1])) diff --git a/titles/wacca/index.py b/titles/wacca/index.py index 0b51849..a59cda1 100644 --- a/titles/wacca/index.py +++ b/titles/wacca/index.py @@ -102,7 +102,7 @@ class WaccaServlet: resp.message = "不正なリクエスト エラーです" return end(resp.make()) - if "/api/" in url_path: + if "api/" in url_path: func_to_find = ( "handle_" + url_path.partition("api/")[2].replace("/", "_") + "_request" ) From dfd38778892f5d2d36e0ea3b59ad7d49fb7375a3 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 18 Mar 2023 12:55:04 -0400 Subject: [PATCH 63/63] wacca: rename locked -> lock_state in MusicUpdateDetailV1 --- titles/wacca/handlers/helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/titles/wacca/handlers/helpers.py b/titles/wacca/handlers/helpers.py index 02b8bd7..f86be2f 100644 --- a/titles/wacca/handlers/helpers.py +++ b/titles/wacca/handlers/helpers.py @@ -942,7 +942,7 @@ class MusicUpdateDetailV1: self.score = 0 self.lowestMissCount = 0 self.maxSkillPts = 0 - self.locked = 0 + self.lock_state = 0 def make(self) -> List: return [ @@ -954,7 +954,7 @@ class MusicUpdateDetailV1: self.score, self.lowestMissCount, self.maxSkillPts, - self.locked, + self.lock_state, ]